diff options
| author | Brian Howe <howe.m.brian@gmail.com> | 2024-02-27 21:07:30 -0600 |
|---|---|---|
| committer | Brian Howe <howe.m.brian@gmail.com> | 2024-02-27 21:07:30 -0600 |
| commit | 54eb81395ef8d3d4cb064b56361ce94fc72b38b5 (patch) | |
| tree | 73240b556055557b0ae034ef5d5ba60cb5cb051e | |
| parent | 7f1fec688cc1a6f7f69fa5b059af01cf9c456d3f (diff) | |
| parent | 4786901bb796c3e912f13b686571fde8d16f49c5 (diff) | |
Merge branch 'master' into bhowe34/fix-replace-missing-metadata-for-music
341 files changed, 3122 insertions, 3667 deletions
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index 39f98e063..b0684c0d4 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -259,7 +259,7 @@ jobs: publishFeedCredentials: 'NugetOrg' allowPackageConflicts: true # This ignores an error if the version already exists - - task: NuGetAuthenticate@0 + - task: NuGetAuthenticate@1 displayName: 'Authenticate to unstable Nuget feed' condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index c03564f97..81fe5add4 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "8.0.0", + "version": "8.0.1", "commands": [ "dotnet-ef" ] diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..063901c80 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,28 @@ +{ + "name": "Development Jellyfin Server", + "image":"mcr.microsoft.com/devcontainers/dotnet:8.0-jammy", + // restores nuget packages, installs the dotnet workloads and installs the dev https certificate + "postStartCommand": "dotnet restore; dotnet workload update; dotnet dev-certs https --trust", + // reads the extensions list and installs them + "postAttachCommand": "cat .vscode/extensions.json | jq -r .recommendations[] | xargs -n 1 code --install-extension", + "features": { + "ghcr.io/devcontainers/features/dotnet:2": { + "version": "none", + "dotnetRuntimeVersions": "8.0", + "aspNetCoreRuntimeVersions": "8.0" + }, + "ghcr.io/devcontainers-contrib/features/apt-packages:1": { + "preserve_apt_list": false, + "packages": ["libfontconfig1"] + }, + "ghcr.io/devcontainers/features/docker-in-docker:2": { + "dockerDashComposeVersion": "v2" + }, + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/eitsupi/devcontainer-features/jq-likes:2": {} + }, + "hostRequirements": { + "memory": "8gb", + "cpus": 4 + } +} diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index b52596645..d8c550e70 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '8.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2.22.8 + uses: github/codeql-action/init@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2.22.8 + uses: github/codeql-action/autobuild@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@407ffafae6a767df3e0230c3df91b6443ae8df75 # v2.22.8 + uses: github/codeql-action/analyze@0b21cf2492b6b02c465a3e5d7c473717ad7721ba # v3.23.1 diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 7d6667794..e43160562 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -25,7 +25,7 @@ jobs: - 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@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 with: name: openapi-head retention-days: 14 @@ -59,7 +59,7 @@ jobs: - 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@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0 with: name: openapi-base retention-days: 14 @@ -78,12 +78,12 @@ jobs: - openapi-base steps: - name: Download openapi-head - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: openapi-head path: openapi-head - name: Download openapi-base - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + uses: actions/download-artifact@6b208ae046db98c579e8a3aa621ab581ff575935 # v4.1.1 with: name: openapi-base path: openapi-base diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 5a0125f5f..0dacbc5c6 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -19,9 +19,9 @@ jobs: runs-on: "${{ matrix.os }}" steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 + - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - - uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4 + - uses: actions/setup-dotnet@4d6c8fcf3c8f7a60068d26b594648e99df24cee3 # v4.0.0 with: dotnet-version: ${{ env.SDK_VERSION }} @@ -34,7 +34,7 @@ jobs: --verbosity minimal - name: Merge code coverage results - uses: danielpalme/ReportGenerator-GitHub-Action@4d510cbed8a05af5aefea46c7fd6e05b95844c89 # 5 + uses: danielpalme/ReportGenerator-GitHub-Action@4d510cbed8a05af5aefea46c7fd6e05b95844c89 # 5.2.0 with: reports: "**/coverage.cobertura.xml" targetdir: "merged/" @@ -42,9 +42,3 @@ jobs: # TODO - which action / tool to use to publish code coverage results? # - name: Publish code coverage results - - - name: Publish OpenAPI Artifact - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3 - with: - name: "OpenAPI Spec" - path: "tests/Jellyfin.Server.Integration.Tests/bin/Release/net*/openapi.json" diff --git a/.github/workflows/issue-stale.yml b/.github/workflows/issue-stale.yml index 926a7fbfb..5a1ca9f7a 100644 --- a/.github/workflows/issue-stale.yml +++ b/.github/workflows/issue-stale.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0 + - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} ascending: true diff --git a/.github/workflows/project-automation.yml b/.github/workflows/project-automation.yml index 3637eb16a..d62f655b3 100644 --- a/.github/workflows/project-automation.yml +++ b/.github/workflows/project-automation.yml @@ -15,7 +15,7 @@ jobs: if: ${{ github.repository == 'jellyfin/jellyfin' }} steps: - name: Remove from 'Current Release' project - uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3 + uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0 if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport') continue-on-error: true with: @@ -24,7 +24,7 @@ jobs: repo-token: ${{ secrets.JF_BOT_TOKEN }} - name: Add to 'Release Next' project - uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3 + uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0 if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened' continue-on-error: true with: @@ -33,7 +33,7 @@ jobs: repo-token: ${{ secrets.JF_BOT_TOKEN }} - name: Add to 'Current Release' project - uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3 + uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0 if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport') continue-on-error: true with: @@ -47,7 +47,7 @@ jobs: run: echo "::set-output name=number::$(curl -s ${{ github.event.issue.comments_url }} | jq '.[] | select(.author_association == "MEMBER") | .author_association' | wc -l)" - name: Move issue to needs triage - uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3 + uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0 if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1 continue-on-error: true with: @@ -56,7 +56,7 @@ jobs: repo-token: ${{ secrets.JF_BOT_TOKEN }} - name: Add issue to triage project - uses: alex-page/github-project-automation-plus@7ffb872c64bd809d23563a130a0a97d01dfa8f43 # v0.8.3 + uses: alex-page/github-project-automation-plus@303f24a24c67ce7adf565a07e96720faf126fe36 # v0.9.0 if: github.event.issue.pull_request == '' && github.event.action == 'opened' continue-on-error: true with: diff --git a/.github/workflows/pull-request-stale.yaml b/.github/workflows/pull-request-stale.yaml index de093a988..d01b3f4a1 100644 --- a/.github/workflows/pull-request-stale.yaml +++ b/.github/workflows/pull-request-stale.yaml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest if: ${{ contains(github.repository, 'jellyfin/') }} steps: - - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0 + - uses: actions/stale@28ca1036281a5e5922ead5184a1bbf96e5fc984e # v9.0.0 with: repo-token: ${{ secrets.JF_BOT_TOKEN }} ascending: true diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 59d9452fe..d738e9fba 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,13 +1,11 @@ { - // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. - // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp - - // List of extensions which should be recommended for users of this workspace. "recommendations": [ "ms-dotnettools.csharp", - "editorconfig.editorconfig" + "editorconfig.editorconfig", + "GitHub.vscode-github-actions", + "ms-dotnettools.vscode-dotnet-runtime", + "ms-dotnettools.csdevkit" ], - // List of extensions recommended by VS Code that should not be recommended for users of this workspace. "unwantedRecommendations": [ ] diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index d208879d1..457f59e0f 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -81,6 +81,7 @@ - [Maxr1998](https://github.com/Maxr1998) - [mcarlton00](https://github.com/mcarlton00) - [mitchfizz05](https://github.com/mitchfizz05) + - [mohd-akram](https://github.com/mohd-akram) - [MrTimscampi](https://github.com/MrTimscampi) - [n8225](https://github.com/n8225) - [Nalsai](https://github.com/Nalsai) @@ -171,9 +172,10 @@ - [tallbl0nde](https://github.com/tallbl0nde) - [sleepycatcoding](https://github.com/sleepycatcoding) - [scampower3](https://github.com/scampower3) - - [Chris-Codes-It] (https://github.com/Chris-Codes-It) + - [Chris-Codes-It](https://github.com/Chris-Codes-It) - [Pithaya](https://github.com/Pithaya) - [Çağrı Sakaoğlu](https://github.com/ilovepilav) + - [Gauvino](https://github.com/Gauvino) # Emby Contributors diff --git a/Directory.Packages.props b/Directory.Packages.props index ff76252f8..dcf183494 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,52 +12,52 @@ <PackageVersion Include="BlurHashSharp" Version="1.3.0" /> <PackageVersion Include="CommandLineParser" Version="2.9.1" /> <PackageVersion Include="coverlet.collector" Version="6.0.0" /> - <PackageVersion Include="Diacritics" Version="3.3.18" /> + <PackageVersion Include="Diacritics" Version="3.3.27" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" /> - <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.0.0" /> + <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.2.0" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" /> - <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" /> + <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0.1" /> <PackageVersion Include="IDisposableAnalyzers" Version="4.0.4" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="libse" Version="3.6.13" /> <PackageVersion Include="LrcParser" Version="2023.524.0" /> - <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.1" /> - <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.0" /> + <PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.1.0" /> + <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.1" /> <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" /> - <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.0" /> + <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.1" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> - <PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.0" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" /> - <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0" /> + <PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.1" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.1" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" /> + <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.1" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="8.0.1" /> <PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="8.0.1" /> + <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.1" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.Http" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.0" /> - <PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.0" /> + <PackageVersion Include="Microsoft.Extensions.Options" Version="8.0.1" /> <PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" /> <PackageVersion Include="MimeTypes" Version="2.4.0" /> <PackageVersion Include="Mono.Nat" Version="3.0.4" /> <PackageVersion Include="Moq" Version="4.18.4" /> <PackageVersion Include="NEbml" Version="0.11.0" /> <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" /> - <PackageVersion Include="PlaylistsNET" Version="1.4.0" /> - <PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.0" /> + <PackageVersion Include="PlaylistsNET" Version="1.4.1" /> + <PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" /> <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" /> - <PackageVersion Include="prometheus-net" Version="8.2.0" /> - <PackageVersion Include="Serilog.AspNetCore" Version="8.0.0" /> + <PackageVersion Include="prometheus-net" Version="8.2.1" /> + <PackageVersion Include="Serilog.AspNetCore" Version="8.0.1" /> <PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" /> <PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.0" /> <PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" /> @@ -66,25 +66,25 @@ <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> <PackageVersion Include="SharpFuzz" Version="2.1.1" /> - <PackageVersion Include="SkiaSharp" Version="2.88.5" /> - <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.5" /> - <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" /> + <PackageVersion Include="SkiaSharp" Version="2.88.7" /> + <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.7" /> + <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.7" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> - <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" /> - <PackageVersion Include="Svg.Skia" Version="1.0.0.2" /> + <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" /> + <PackageVersion Include="Svg.Skia" Version="1.0.0.10" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageVersion Include="System.Globalization" Version="4.3.0" /> <PackageVersion Include="System.Linq.Async" Version="6.0.1" /> <PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" /> - <PackageVersion Include="System.Text.Json" Version="8.0.0" /> + <PackageVersion Include="System.Text.Json" Version="8.0.1" /> <PackageVersion Include="System.Threading.Tasks.Dataflow" Version="8.0.0" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="TMDbLib" Version="2.1.0" /> <PackageVersion Include="UTF.Unknown" Version="2.5.1" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" /> - <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.3" /> + <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.6" /> <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" /> - <PackageVersion Include="xunit" Version="2.6.1" /> + <PackageVersion Include="xunit" Version="2.6.6" /> </ItemGroup> </Project> diff --git a/Dockerfile b/Dockerfile index d3f10cd12..550c3203d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,72 +8,68 @@ FROM node:20-alpine as web-builder ARG JELLYFIN_WEB_VERSION=master RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ + && apk del curl \ && cd jellyfin-web-* \ && npm ci --no-audit --unsafe-perm \ && npm run build:production \ && mv dist /dist -FROM debian:stable-slim as app +FROM debian:bookworm-slim as app # https://askubuntu.com/questions/972516/debian-frontend-environment-variable ARG DEBIAN_FRONTEND="noninteractive" # http://stackoverflow.com/questions/48162574/ddg#49462622 ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn # https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support) +ENV NVIDIA_VISIBLE_DEVICES="all" ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility" +ENV JELLYFIN_DATA_DIR=/config +ENV JELLYFIN_CACHE_DIR=/cache + # https://github.com/intel/compute-runtime/releases -ARG GMMLIB_VERSION=22.0.2 -ARG IGC_VERSION=1.0.10395 -ARG NEO_VERSION=22.08.22549 -ARG LEVEL_ZERO_VERSION=1.3.22549 +ARG GMMLIB_VERSION=22.3.11.ci17757293 +ARG IGC_VERSION=1.0.15136.22 +ARG NEO_VERSION=23.39.27427.23 +ARG LEVEL_ZERO_VERSION=1.3.27427.23 -# 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 curl \ - && wget -O - https://repo.jellyfin.org/jellyfin_team.gpg.key | apt-key add - \ + && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl \ + && curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/debian-jellyfin.gpg \ && 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 \ - && apt-get install --no-install-recommends --no-install-suggests -y \ - mesa-va-drivers \ - jellyfin-ffmpeg5 \ - openssl \ - locales \ + && apt-get install --no-install-recommends --no-install-suggests -y mesa-va-drivers jellyfin-ffmpeg6 openssl locales \ # Intel VAAPI Tone mapping dependencies: # Prefer NEO to Beignet since the latter one doesn't support Comet Lake or newer for now. # Do not use the intel-opencl-icd package from repo since they will not build with RELEASE_WITH_REGKEYS enabled. && mkdir intel-compute-runtime \ && cd intel-compute-runtime \ - && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-gmmlib_${GMMLIB_VERSION}_amd64.deb \ - && wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-core_${IGC_VERSION}_amd64.deb \ - && wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-opencl_${IGC_VERSION}_amd64.deb \ - && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-opencl-icd_${NEO_VERSION}_amd64.deb \ - && wget https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-level-zero-gpu_${LEVEL_ZERO_VERSION}_amd64.deb \ + && curl -LO https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-core_${IGC_VERSION}_amd64.deb \ + -LO https://github.com/intel/intel-graphics-compiler/releases/download/igc-${IGC_VERSION}/intel-igc-opencl_${IGC_VERSION}_amd64.deb \ + -LO https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-level-zero-gpu_${LEVEL_ZERO_VERSION}_amd64.deb \ + -LO https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/intel-opencl-icd_${NEO_VERSION}_amd64.deb \ + -LO https://github.com/intel/compute-runtime/releases/download/${NEO_VERSION}/libigdgmm12_${GMMLIB_VERSION}_amd64.deb \ && dpkg -i *.deb \ && cd .. \ && rm -rf intel-compute-runtime \ - && apt-get remove gnupg wget -y \ + && apt-get remove gnupg -y \ && apt-get clean autoclean -y \ && apt-get autoremove -y \ && rm -rf /var/lib/apt/lists/* \ - && mkdir -p /cache /config /media \ - && chmod 777 /cache /config /media \ + && mkdir -p ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \ + && chmod 777 ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \ && 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 LC_ALL en_US.UTF-8 -ENV LANG en_US.UTF-8 -ENV LANGUAGE en_US:en +ENV LC_ALL=en_US.UTF-8 +ENV LANG=en_US.UTF-8 +ENV LANGUAGE=en_US:en FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder WORKDIR /repo COPY . . ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 -# because of changes in docker and systemd we need to not build in parallel at the moment -# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting -RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 -p:DebugSymbols=false -p:DebugType=none + +RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-x64 -p:DebugSymbols=false -p:DebugType=none FROM app @@ -83,11 +79,9 @@ COPY --from=builder /jellyfin /jellyfin COPY --from=web-builder /dist /jellyfin/jellyfin-web EXPOSE 8096 -VOLUME /cache /config -ENTRYPOINT ["./jellyfin/jellyfin", \ - "--datadir", "/config", \ - "--cachedir", "/cache", \ - "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"] +VOLUME ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} +ENTRYPOINT [ "./jellyfin/jellyfin", \ + "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg" ] HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \ - CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1 + CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1 diff --git a/Dockerfile.arm b/Dockerfile.arm index db1acc838..07039e43b 100644 --- a/Dockerfile.arm +++ b/Dockerfile.arm @@ -4,64 +4,58 @@ # https://github.com/multiarch/qemu-user-static#binfmt_misc-register ARG DOTNET_VERSION=8.0 - FROM node:20-alpine as web-builder ARG JELLYFIN_WEB_VERSION=master RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ + && apk del curl \ && cd jellyfin-web-* \ && npm ci --no-audit --unsafe-perm \ && npm run build:production \ && mv dist /dist FROM multiarch/qemu-user-static:x86_64-arm as qemu -FROM arm32v7/debian:stable-slim as app +FROM arm32v7/debian:bookworm-slim as app # https://askubuntu.com/questions/972516/debian-frontend-environment-variable ARG DEBIAN_FRONTEND="noninteractive" # http://stackoverflow.com/questions/48162574/ddg#49462622 ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn # https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support) +ENV NVIDIA_VISIBLE_DEVICES="all" ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility" +ENV JELLYFIN_DATA_DIR=/config +ENV JELLYFIN_CACHE_DIR=/cache + 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 - && \ - curl -ks https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | apt-key add - && \ - echo 'deb [arch=armhf] https://repo.jellyfin.org/debian buster main' > /etc/apt/sources.list.d/jellyfin.list && \ - echo "deb http://ppa.launchpad.net/ubuntu-raspi2/ppa/ubuntu bionic main">> /etc/apt/sources.list.d/raspbins.list && \ - apt-get update && \ - apt-get install --no-install-recommends --no-install-suggests -y \ - jellyfin-ffmpeg \ - libssl-dev \ - libfontconfig1 \ - libfreetype6 \ - vainfo \ - libva2 \ - locales \ + && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl \ + && curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/debian-jellyfin.gpg \ + && curl -fsSL https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | gpg --dearmor -o /etc/apt/trusted.gpg.d/ubuntu-jellyfin.gpg \ + && 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 \ + && apt-get install --no-install-recommends --no-install-suggests -y \ + jellyfin-ffmpeg6 libssl-dev libfontconfig1 \ + libfreetype6 vainfo libva2 locales \ && apt-get remove gnupg -y \ && apt-get clean autoclean -y \ && apt-get autoremove -y \ && rm -rf /var/lib/apt/lists/* \ - && mkdir -p /cache /config /media \ - && chmod 777 /cache /config /media \ + && mkdir -p ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \ + && chmod 777 ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \ && 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 LC_ALL en_US.UTF-8 -ENV LANG en_US.UTF-8 -ENV LANGUAGE en_US:en +ENV LC_ALL=en_US.UTF-8 +ENV LANG=en_US.UTF-8 +ENV LANGUAGE=en_US:en FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder WORKDIR /repo COPY . . ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 -# Discard objs - may cause failures if exists -RUN find . -type d -name obj | xargs -r rm -r -# Build + RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm -p:DebugSymbols=false -p:DebugType=none FROM app @@ -72,11 +66,9 @@ COPY --from=builder /jellyfin /jellyfin COPY --from=web-builder /dist /jellyfin/jellyfin-web EXPOSE 8096 -VOLUME /cache /config -ENTRYPOINT ["./jellyfin/jellyfin", \ - "--datadir", "/config", \ - "--cachedir", "/cache", \ - "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg"] +VOLUME ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} +ENTRYPOINT [ "/jellyfin/jellyfin", \ + "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg" ] HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \ - CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1 + CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1 diff --git a/Dockerfile.arm64 b/Dockerfile.arm64 index 3eb5f45fc..54023794f 100644 --- a/Dockerfile.arm64 +++ b/Dockerfile.arm64 @@ -4,58 +4,58 @@ # https://github.com/multiarch/qemu-user-static#binfmt_misc-register ARG DOTNET_VERSION=8.0 - FROM node:20-alpine as web-builder ARG JELLYFIN_WEB_VERSION=master RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \ + && apk del curl \ && cd jellyfin-web-* \ && npm ci --no-audit --unsafe-perm \ && npm run build:production \ && mv dist /dist FROM multiarch/qemu-user-static:x86_64-aarch64 as qemu -FROM arm64v8/debian:stable-slim as app +FROM arm64v8/debian:bookworm-slim as app # https://askubuntu.com/questions/972516/debian-frontend-environment-variable ARG DEBIAN_FRONTEND="noninteractive" # http://stackoverflow.com/questions/48162574/ddg#49462622 ARG APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn # https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support) +ENV NVIDIA_VISIBLE_DEVICES="all" ENV NVIDIA_DRIVER_CAPABILITIES="compute,video,utility" +ENV JELLYFIN_DATA_DIR=/config +ENV JELLYFIN_CACHE_DIR=/cache + 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 \ - ca-certificates \ - libfontconfig1 \ - libfreetype6 \ - libomxil-bellagio0 \ - libomxil-bellagio-bin \ - locales \ - curl \ +RUN apt-get update \ + && apt-get install --no-install-recommends --no-install-suggests -y ca-certificates gnupg curl \ + && curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/debian-jellyfin.gpg \ + && curl -fsSL https://keyserver.ubuntu.com/pks/lookup?op=get\&search=0x6587ffd6536b8826e88a62547876ae518cbcf2f2 | gpg --dearmor -o /etc/apt/trusted.gpg.d/ubuntu-jellyfin.gpg \ + && 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 \ + && apt-get install --no-install-recommends --no-install-suggests -y \ + jellyfin-ffmpeg6 locales libssl-dev libfontconfig1 \ + libfreetype6 libomxil-bellagio0 libomxil-bellagio-bin \ + && apt-get remove gnupg -y \ && apt-get clean autoclean -y \ && apt-get autoremove -y \ && rm -rf /var/lib/apt/lists/* \ - && mkdir -p /cache /config /media \ - && chmod 777 /cache /config /media \ + && mkdir -p ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \ + && chmod 777 ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} \ && 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 LC_ALL en_US.UTF-8 -ENV LANG en_US.UTF-8 -ENV LANGUAGE en_US:en +ENV LC_ALL=en_US.UTF-8 +ENV LANG=en_US.UTF-8 +ENV LANGUAGE=en_US:en FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION} as builder WORKDIR /repo COPY . . ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 -# Discard objs - may cause failures if exists -RUN find . -type d -name obj | xargs -r rm -r -# Build + RUN dotnet publish Jellyfin.Server --configuration Release --output="/jellyfin" --self-contained --runtime linux-arm64 -p:DebugSymbols=false -p:DebugType=none FROM app @@ -66,11 +66,9 @@ COPY --from=builder /jellyfin /jellyfin COPY --from=web-builder /dist /jellyfin/jellyfin-web EXPOSE 8096 -VOLUME /cache /config -ENTRYPOINT ["./jellyfin/jellyfin", \ - "--datadir", "/config", \ - "--cachedir", "/cache", \ - "--ffmpeg", "/usr/bin/ffmpeg"] +VOLUME ${JELLYFIN_DATA_DIR} ${JELLYFIN_CACHE_DIR} +ENTRYPOINT [ "/jellyfin/jellyfin", \ + "--ffmpeg", "/usr/lib/jellyfin-ffmpeg/ffmpeg" ] HEALTHCHECK --interval=30s --timeout=30s --start-period=10s --retries=3 \ - CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1 + CMD curl -Lk -fsS "${HEALTHCHECK_URL}" || exit 1 diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index dce56e0a4..5870fed76 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -15,7 +15,6 @@ using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; using Emby.Naming.Common; using Emby.Photos; -using Emby.Server.Implementations.Channels; using Emby.Server.Implementations.Collections; using Emby.Server.Implementations.Configuration; using Emby.Server.Implementations.Cryptography; @@ -25,7 +24,6 @@ using Emby.Server.Implementations.Dto; using Emby.Server.Implementations.HttpServer.Security; using Emby.Server.Implementations.IO; using Emby.Server.Implementations.Library; -using Emby.Server.Implementations.LiveTv; using Emby.Server.Implementations.Localization; using Emby.Server.Implementations.Playlists; using Emby.Server.Implementations.Plugins; @@ -76,6 +74,7 @@ using MediaBrowser.Controller.TV; using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.MediaEncoding.Subtitles; +using MediaBrowser.MediaEncoding.Transcoding; using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; @@ -503,8 +502,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(_xmlSerializer); - serviceCollection.AddSingleton<IStreamHelper, StreamHelper>(); - serviceCollection.AddSingleton<ICryptoProvider, CryptographyProvider>(); serviceCollection.AddSingleton<ISocketFactory, SocketFactory>(); @@ -556,8 +553,6 @@ namespace Emby.Server.Implementations serviceCollection.AddTransient(provider => new Lazy<ILiveTvManager>(provider.GetRequiredService<ILiveTvManager>)); serviceCollection.AddSingleton<IDtoService, DtoService>(); - serviceCollection.AddSingleton<IChannelManager, ChannelManager>(); - serviceCollection.AddSingleton<ISessionManager, SessionManager>(); serviceCollection.AddSingleton<ICollectionManager, CollectionManager>(); @@ -566,9 +561,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<ISyncPlayManager, SyncPlayManager>(); - serviceCollection.AddSingleton<LiveTvDtoService>(); - serviceCollection.AddSingleton<ILiveTvManager, LiveTvManager>(); - serviceCollection.AddSingleton<IUserViewManager, UserViewManager>(); serviceCollection.AddSingleton<IChapterManager, ChapterManager>(); @@ -583,7 +575,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>(); - serviceCollection.AddSingleton<TranscodingJobHelper>(); + serviceCollection.AddSingleton<ITranscodeManager, TranscodeManager>(); serviceCollection.AddScoped<MediaInfoHelper>(); serviceCollection.AddScoped<AudioHelper>(); serviceCollection.AddScoped<DynamicHlsHelper>(); @@ -703,7 +695,7 @@ namespace Emby.Server.Implementations GetExports<IMetadataSaver>(), GetExports<IExternalId>()); - Resolve<ILiveTvManager>().AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>()); + Resolve<ILiveTvManager>().AddParts(GetExports<ILiveTvService>(), GetExports<IListingsProvider>()); Resolve<IMediaSourceManager>().AddParts(GetExports<IMediaSourceProvider>()); } diff --git a/Emby.Server.Implementations/Data/SqliteExtensions.cs b/Emby.Server.Implementations/Data/SqliteExtensions.cs index 01b5fdaee..25ef57d27 100644 --- a/Emby.Server.Implementations/Data/SqliteExtensions.cs +++ b/Emby.Server.Implementations/Data/SqliteExtensions.cs @@ -104,6 +104,13 @@ namespace Emby.Server.Implementations.Data if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult)) { + // If the resulting DateTimeKind is Unspecified it is actually Utc. + // This is required downstream for the Json serializer. + if (dateTimeResult.Kind == DateTimeKind.Unspecified) + { + dateTimeResult = DateTime.SpecifyKind(dateTimeResult, DateTimeKind.Utc); + } + result = dateTimeResult; return true; } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index d0772654c..a6336f145 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -699,7 +699,7 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBindNull("@EndDate"); } - saveItemStatement.TryBind("@ChannelId", item.ChannelId.Equals(default) ? null : item.ChannelId.ToString("N", CultureInfo.InvariantCulture)); + saveItemStatement.TryBind("@ChannelId", item.ChannelId.IsEmpty() ? null : item.ChannelId.ToString("N", CultureInfo.InvariantCulture)); if (item is IHasProgramAttributes hasProgramAttributes) { @@ -729,7 +729,7 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBind("@ProductionYear", item.ProductionYear); var parentId = item.ParentId; - if (parentId.Equals(default)) + if (parentId.IsEmpty()) { saveItemStatement.TryBindNull("@ParentId"); } @@ -925,7 +925,7 @@ namespace Emby.Server.Implementations.Data { saveItemStatement.TryBind("@SeasonName", episode.SeasonName); - var nullableSeasonId = episode.SeasonId.Equals(default) ? (Guid?)null : episode.SeasonId; + var nullableSeasonId = episode.SeasonId.IsEmpty() ? (Guid?)null : episode.SeasonId; saveItemStatement.TryBind("@SeasonId", nullableSeasonId); } @@ -937,7 +937,7 @@ namespace Emby.Server.Implementations.Data if (item is IHasSeries hasSeries) { - var nullableSeriesId = hasSeries.SeriesId.Equals(default) ? (Guid?)null : hasSeries.SeriesId; + var nullableSeriesId = hasSeries.SeriesId.IsEmpty() ? (Guid?)null : hasSeries.SeriesId; saveItemStatement.TryBind("@SeriesId", nullableSeriesId); saveItemStatement.TryBind("@SeriesPresentationUniqueKey", hasSeries.SeriesPresentationUniqueKey); @@ -1010,7 +1010,7 @@ namespace Emby.Server.Implementations.Data } Guid ownerId = item.OwnerId; - if (ownerId.Equals(default)) + if (ownerId.IsEmpty()) { saveItemStatement.TryBindNull("@OwnerId"); } @@ -1266,7 +1266,7 @@ namespace Emby.Server.Implementations.Data /// <exception cref="ArgumentException"><paramr name="id"/> is <seealso cref="Guid.Empty"/>.</exception> public BaseItem RetrieveItem(Guid id) { - if (id.Equals(default)) + if (id.IsEmpty()) { throw new ArgumentException("Guid can't be empty", nameof(id)); } @@ -1970,7 +1970,7 @@ namespace Emby.Server.Implementations.Data { CheckDisposed(); - if (id.Equals(default)) + if (id.IsEmpty()) { throw new ArgumentNullException(nameof(id)); } @@ -3230,7 +3230,7 @@ namespace Emby.Server.Implementations.Data whereClauses.Add($"ChannelId in ({inClause})"); } - if (!query.ParentId.Equals(default)) + if (!query.ParentId.IsEmpty()) { whereClauses.Add("ParentId=@ParentId"); statement?.TryBind("@ParentId", query.ParentId); @@ -4452,7 +4452,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type public void DeleteItem(Guid id) { - if (id.Equals(default)) + if (id.IsEmpty()) { throw new ArgumentNullException(nameof(id)); } @@ -4583,13 +4583,13 @@ AND Type = @InternalPersonType)"); statement?.TryBind("@UserId", query.User.InternalId); } - if (!query.ItemId.Equals(default)) + if (!query.ItemId.IsEmpty()) { whereClauses.Add("ItemId=@ItemId"); statement?.TryBind("@ItemId", query.ItemId); } - if (!query.AppearsInItemId.Equals(default)) + if (!query.AppearsInItemId.IsEmpty()) { whereClauses.Add("p.Name in (Select Name from People where ItemId=@AppearsInItemId)"); statement?.TryBind("@AppearsInItemId", query.AppearsInItemId); @@ -4640,7 +4640,7 @@ AND Type = @InternalPersonType)"); private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, SqliteConnection db, SqliteCommand deleteAncestorsStatement) { - if (itemId.Equals(default)) + if (itemId.IsEmpty()) { throw new ArgumentNullException(nameof(itemId)); } @@ -5156,7 +5156,7 @@ AND Type = @InternalPersonType)"); private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, SqliteConnection db) { - if (itemId.Equals(default)) + if (itemId.IsEmpty()) { throw new ArgumentNullException(nameof(itemId)); } @@ -5228,7 +5228,7 @@ AND Type = @InternalPersonType)"); public void UpdatePeople(Guid itemId, List<PersonInfo> people) { - if (itemId.Equals(default)) + if (itemId.IsEmpty()) { throw new ArgumentNullException(nameof(itemId)); } @@ -5378,7 +5378,7 @@ AND Type = @InternalPersonType)"); { CheckDisposed(); - if (id.Equals(default)) + if (id.IsEmpty()) { throw new ArgumentNullException(nameof(id)); } @@ -5758,7 +5758,7 @@ AND Type = @InternalPersonType)"); CancellationToken cancellationToken) { CheckDisposed(); - if (id.Equals(default)) + if (id.IsEmpty()) { throw new ArgumentException("Guid can't be empty.", nameof(id)); } diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 44b97e8b8..d0d5bb81c 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -418,15 +418,6 @@ namespace Emby.Server.Implementations.Dto { dto.PlayAccess = item.GetPlayAccess(user); } - - if (options.ContainsField(ItemFields.BasicSyncInfo)) - { - var userCanSync = user is not null && user.HasPermission(PermissionKind.EnableContentDownloading); - if (userCanSync && item.SupportsExternalTransfer) - { - dto.SupportsSync = true; - } - } } private static int GetChildCount(Folder folder, User user) diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index b3344bb9f..34276355a 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -22,7 +22,6 @@ <ItemGroup> <PackageReference Include="DiscUtils.Udf" /> - <PackageReference Include="Jellyfin.XmlTv" /> <PackageReference Include="Microsoft.Data.Sqlite" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" /> <PackageReference Include="Microsoft.Extensions.Caching.Memory" /> diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index a83d7a410..83e7b230d 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Events; +using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; @@ -241,7 +242,7 @@ public sealed class LibraryChangedNotifier : IServerEntryPoint { var userIds = _sessionManager.Sessions .Select(i => i.UserId) - .Where(i => !i.Equals(default)) + .Where(i => !i.IsEmpty()) .Distinct() .ToArray(); diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs index 6e8f77977..34c722e41 100644 --- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs @@ -32,26 +32,26 @@ namespace Emby.Server.Implementations.Images switch (viewType) { - case CollectionType.Movies: + case CollectionType.movies: includeItemTypes = new[] { BaseItemKind.Movie }; break; - case CollectionType.TvShows: + case CollectionType.tvshows: includeItemTypes = new[] { BaseItemKind.Series }; break; - case CollectionType.Music: + case CollectionType.music: includeItemTypes = new[] { BaseItemKind.MusicAlbum }; break; - case CollectionType.MusicVideos: + case CollectionType.musicvideos: includeItemTypes = new[] { BaseItemKind.MusicVideo }; break; - case CollectionType.Books: + case CollectionType.books: includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook }; break; - case CollectionType.BoxSets: + case CollectionType.boxsets: includeItemTypes = new[] { BaseItemKind.BoxSet }; break; - case CollectionType.HomeVideos: - case CollectionType.Photos: + case CollectionType.homevideos: + case CollectionType.photos: includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo }; break; default: @@ -59,7 +59,7 @@ namespace Emby.Server.Implementations.Images break; } - var recursive = viewType != CollectionType.Playlists; + var recursive = viewType != CollectionType.playlists; return view.GetItemList(new InternalItemsQuery { diff --git a/Emby.Server.Implementations/Images/DynamicImageProvider.cs b/Emby.Server.Implementations/Images/DynamicImageProvider.cs index 5de53df73..6b2ae23b3 100644 --- a/Emby.Server.Implementations/Images/DynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/DynamicImageProvider.cs @@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Images var view = (UserView)item; var isUsingCollectionStrip = IsUsingCollectionStrip(view); - var recursive = isUsingCollectionStrip && view?.ViewType is not null && view.ViewType != CollectionType.BoxSets && view.ViewType != CollectionType.Playlists; + var recursive = isUsingCollectionStrip && view?.ViewType is not null && view.ViewType != CollectionType.boxsets && view.ViewType != CollectionType.playlists; var result = view.GetItemList(new InternalItemsQuery { @@ -114,9 +114,9 @@ namespace Emby.Server.Implementations.Images { CollectionType[] collectionStripViewTypes = { - CollectionType.Movies, - CollectionType.TvShows, - CollectionType.Playlists + CollectionType.movies, + CollectionType.tvshows, + CollectionType.playlists }; return view?.ViewType is not null && collectionStripViewTypes.Contains(view.ViewType.Value); diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index f40177fa7..8ae913dad 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -732,7 +732,7 @@ namespace Emby.Server.Implementations.Library Path = path }; - if (folder.Id.Equals(default)) + if (folder.Id.IsEmpty()) { if (string.IsNullOrEmpty(folder.Path)) { @@ -1219,7 +1219,7 @@ namespace Emby.Server.Implementations.Library /// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception> public BaseItem GetItemById(Guid id) { - if (id.Equals(default)) + if (id.IsEmpty()) { throw new ArgumentException("Guid can't be empty", nameof(id)); } @@ -1241,7 +1241,7 @@ namespace Emby.Server.Implementations.Library public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent) { - if (query.Recursive && !query.ParentId.Equals(default)) + if (query.Recursive && !query.ParentId.IsEmpty()) { var parent = GetItemById(query.ParentId); if (parent is not null) @@ -1272,7 +1272,7 @@ namespace Emby.Server.Implementations.Library public int GetCount(InternalItemsQuery query) { - if (query.Recursive && !query.ParentId.Equals(default)) + if (query.Recursive && !query.ParentId.IsEmpty()) { var parent = GetItemById(query.ParentId); if (parent is not null) @@ -1430,7 +1430,7 @@ namespace Emby.Server.Implementations.Library public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query) { - if (query.Recursive && !query.ParentId.Equals(default)) + if (query.Recursive && !query.ParentId.IsEmpty()) { var parent = GetItemById(query.ParentId); if (parent is not null) @@ -1486,7 +1486,7 @@ namespace Emby.Server.Implementations.Library private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true) { if (query.AncestorIds.Length == 0 && - query.ParentId.Equals(default) && + query.ParentId.IsEmpty() && query.ChannelIds.Count == 0 && query.TopParentIds.Length == 0 && string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) && @@ -1514,13 +1514,13 @@ namespace Emby.Server.Implementations.Library { if (item is UserView view) { - if (view.ViewType == CollectionType.LiveTv) + if (view.ViewType == CollectionType.livetv) { return new[] { view.Id }; } // Translate view into folders - if (!view.DisplayParentId.Equals(default)) + if (!view.DisplayParentId.IsEmpty()) { var displayParent = GetItemById(view.DisplayParentId); if (displayParent is not null) @@ -1531,7 +1531,7 @@ namespace Emby.Server.Implementations.Library return Array.Empty<Guid>(); } - if (!view.ParentId.Equals(default)) + if (!view.ParentId.IsEmpty()) { var displayParent = GetItemById(view.ParentId); if (displayParent is not null) @@ -1543,7 +1543,7 @@ namespace Emby.Server.Implementations.Library } // Handle grouping - if (user is not null && view.ViewType != CollectionType.Unknown && UserView.IsEligibleForGrouping(view.ViewType) + if (user is not null && view.ViewType != CollectionType.unknown && UserView.IsEligibleForGrouping(view.ViewType) && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0) { return GetUserRootFolder() @@ -2137,7 +2137,7 @@ namespace Emby.Server.Implementations.Library return null; } - while (!item.ParentId.Equals(default)) + while (!item.ParentId.IsEmpty()) { var parent = item.GetParent(); if (parent is null || parent is AggregateFolder) @@ -2215,7 +2215,7 @@ namespace Emby.Server.Implementations.Library CollectionType? viewType, string sortName) { - var parentIdString = parentId.Equals(default) + var parentIdString = parentId.IsEmpty() ? null : parentId.ToString("N", CultureInfo.InvariantCulture); var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty); @@ -2251,7 +2251,7 @@ namespace Emby.Server.Implementations.Library var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; - if (!refresh && !item.DisplayParentId.Equals(default)) + if (!refresh && !item.DisplayParentId.IsEmpty()) { var displayParent = GetItemById(item.DisplayParentId); refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed; @@ -2315,7 +2315,7 @@ namespace Emby.Server.Implementations.Library var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; - if (!refresh && !item.DisplayParentId.Equals(default)) + if (!refresh && !item.DisplayParentId.IsEmpty()) { var displayParent = GetItemById(item.DisplayParentId); refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed; @@ -2345,7 +2345,7 @@ namespace Emby.Server.Implementations.Library { ArgumentException.ThrowIfNullOrEmpty(name); - var parentIdString = parentId.Equals(default) + var parentIdString = parentId.IsEmpty() ? null : parentId.ToString("N", CultureInfo.InvariantCulture); var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType?.ToString() ?? string.Empty); @@ -2391,7 +2391,7 @@ namespace Emby.Server.Implementations.Library var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; - if (!refresh && !item.DisplayParentId.Equals(default)) + if (!refresh && !item.DisplayParentId.IsEmpty()) { var displayParent = GetItemById(item.DisplayParentId); refresh = displayParent is not null && displayParent.DateLastSaved > item.DateLastRefreshed; @@ -2419,7 +2419,7 @@ namespace Emby.Server.Implementations.Library return GetItemById(parentId.Value); } - if (userId.HasValue && !userId.Equals(default)) + if (!userId.IsNullOrEmpty()) { return GetUserRootFolder(); } diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 96fad9bca..c38f1af91 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -11,14 +11,16 @@ using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using EasyCaching.Core.Configurations; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using Jellyfin.Extensions.Json; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; @@ -37,6 +39,7 @@ namespace Emby.Server.Implementations.Library // Do not use a pipe here because Roku http requests to the server will fail, without any explicit error message. private const char LiveStreamIdDelimeter = '_'; + private readonly IServerApplicationHost _appHost; private readonly IItemRepository _itemRepo; private readonly IUserManager _userManager; private readonly ILibraryManager _libraryManager; @@ -55,6 +58,7 @@ namespace Emby.Server.Implementations.Library private IMediaSourceProvider[] _providers; public MediaSourceManager( + IServerApplicationHost appHost, IItemRepository itemRepo, IApplicationPaths applicationPaths, ILocalizationManager localizationManager, @@ -66,6 +70,7 @@ namespace Emby.Server.Implementations.Library IMediaEncoder mediaEncoder, IDirectoryService directoryService) { + _appHost = appHost; _itemRepo = itemRepo; _userManager = userManager; _libraryManager = libraryManager; @@ -520,10 +525,10 @@ namespace Emby.Server.Implementations.Library _logger.LogInformation("Live stream opened: {@MediaSource}", mediaSource); var clone = JsonSerializer.Deserialize<MediaSourceInfo>(json, _jsonOptions); - if (!request.UserId.Equals(default)) + if (!request.UserId.IsEmpty()) { var user = _userManager.GetUserById(request.UserId); - var item = request.ItemId.Equals(default) + var item = request.ItemId.IsEmpty() ? null : _libraryManager.GetItemById(request.ItemId); SetDefaultAudioAndSubtitleStreamIndexes(item, clone, user); @@ -799,6 +804,35 @@ namespace Emby.Server.Implementations.Library return result.Item1; } + public async Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken) + { + var stream = new MediaSourceInfo + { + EncoderPath = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveRecordings/" + info.Id + "/stream", + EncoderProtocol = MediaProtocol.Http, + Path = info.Path, + Protocol = MediaProtocol.File, + Id = info.Id, + SupportsDirectPlay = false, + SupportsDirectStream = true, + SupportsTranscoding = true, + IsInfiniteStream = true, + RequiresOpening = false, + RequiresClosing = false, + BufferMs = 0, + IgnoreDts = true, + IgnoreIndex = true + }; + + await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths) + .AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false); + + return new List<MediaSourceInfo> + { + stream + }; + } + public async Task CloseLiveStream(string id) { ArgumentException.ThrowIfNullOrEmpty(id); diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index b2439a87e..078f4ad21 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -80,7 +81,7 @@ namespace Emby.Server.Implementations.Library { return Guid.Empty; } - }).Where(i => !i.Equals(default)).ToArray(); + }).Where(i => !i.IsEmpty()).ToArray(); return GetInstantMixFromGenreIds(genreIds, user, dtoOptions); } diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index ac423ed09..dbf05c1db 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio List<FileSystemMetadata> files, CollectionType? collectionType) { - if (collectionType == CollectionType.Books) + if (collectionType == CollectionType.books) { return ResolveMultipleAudio(parent, files, true); } @@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio var collectionType = args.GetCollectionType(); - var isBooksCollectionType = collectionType == CollectionType.Books; + var isBooksCollectionType = collectionType == CollectionType.books; if (args.IsDirectory) { @@ -112,7 +112,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio MediaBrowser.Controller.Entities.Audio.Audio item = null; - var isMusicCollectionType = collectionType == CollectionType.Music; + var isMusicCollectionType = collectionType == CollectionType.music; // Use regular audio type for mixed libraries, owned items and music if (isMixedCollectionType || diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs index 06e292f4c..0bfb7fbe6 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicAlbumResolver.cs @@ -55,7 +55,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio protected override MusicAlbum Resolve(ItemResolveArgs args) { var collectionType = args.GetCollectionType(); - var isMusicMediaFolder = collectionType == CollectionType.Music; + var isMusicMediaFolder = collectionType == CollectionType.music; // If there's a collection type and it's not music, don't allow it. if (!isMusicMediaFolder) diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs index 7d6f97b12..1bdae7f62 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/MusicArtistResolver.cs @@ -65,7 +65,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio var collectionType = args.GetCollectionType(); - var isMusicMediaFolder = collectionType == CollectionType.Music; + var isMusicMediaFolder = collectionType == CollectionType.music; // If there's a collection type and it's not music, it can't be a music artist if (!isMusicMediaFolder) diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index b76bfe427..464a548ab 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -23,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books var collectionType = args.GetCollectionType(); // Only process items that are in a collection folder containing books - if (collectionType != CollectionType.Books) + if (collectionType != CollectionType.books) { return null; } diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 50fd8b877..1a210e3cc 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -31,11 +31,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies private static readonly CollectionType[] _validCollectionTypes = new[] { - CollectionType.Movies, - CollectionType.HomeVideos, - CollectionType.MusicVideos, - CollectionType.TvShows, - CollectionType.Photos + CollectionType.movies, + CollectionType.homevideos, + CollectionType.musicvideos, + CollectionType.tvshows, + CollectionType.photos }; /// <summary> @@ -100,12 +100,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies Video movie = null; var files = args.GetActualFileSystemChildren().ToList(); - if (collectionType == CollectionType.MusicVideos) + if (collectionType == CollectionType.musicvideos) { movie = FindMovie<MusicVideo>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false); } - if (collectionType == CollectionType.HomeVideos) + if (collectionType == CollectionType.homevideos) { movie = FindMovie<Video>(args, args.Path, args.Parent, files, DirectoryService, collectionType, false); } @@ -126,7 +126,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true); } - if (collectionType == CollectionType.Movies) + if (collectionType == CollectionType.movies) { movie = FindMovie<Movie>(args, args.Path, args.Parent, files, DirectoryService, collectionType, true); } @@ -147,17 +147,17 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies Video item = null; - if (collectionType == CollectionType.MusicVideos) + if (collectionType == CollectionType.musicvideos) { item = ResolveVideo<MusicVideo>(args, false); } // To find a movie file, the collection type must be movies or boxsets - else if (collectionType == CollectionType.Movies) + else if (collectionType == CollectionType.movies) { item = ResolveVideo<Movie>(args, true); } - else if (collectionType == CollectionType.HomeVideos || collectionType == CollectionType.Photos) + else if (collectionType == CollectionType.homevideos || collectionType == CollectionType.photos) { item = ResolveVideo<Video>(args, false); } @@ -195,12 +195,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return null; } - if (collectionType is CollectionType.MusicVideos) + if (collectionType is CollectionType.musicvideos) { return ResolveVideos<MusicVideo>(parent, files, true, collectionType, false); } - if (collectionType == CollectionType.HomeVideos || collectionType == CollectionType.Photos) + if (collectionType == CollectionType.homevideos || collectionType == CollectionType.photos) { return ResolveVideos<Video>(parent, files, false, collectionType, false); } @@ -221,12 +221,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return ResolveVideos<Movie>(parent, files, false, collectionType, true); } - if (collectionType == CollectionType.Movies) + if (collectionType == CollectionType.movies) { return ResolveVideos<Movie>(parent, files, true, collectionType, true); } - if (collectionType == CollectionType.TvShows) + if (collectionType == CollectionType.tvshows) { return ResolveVideos<Episode>(parent, files, false, collectionType, true); } @@ -403,7 +403,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies var multiDiscFolders = new List<FileSystemMetadata>(); var libraryOptions = args.LibraryOptions; - var supportPhotos = collectionType == CollectionType.HomeVideos && libraryOptions.EnablePhotos; + var supportPhotos = collectionType == CollectionType.homevideos && libraryOptions.EnablePhotos; var photos = new List<FileSystemMetadata>(); // Search for a folder rip @@ -459,7 +459,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies var result = ResolveVideos<T>(parent, fileSystemEntries, SupportsMultiVersion, collectionType, parseName) ?? new MultiItemResolverResult(); - var isPhotosCollection = collectionType == CollectionType.HomeVideos || collectionType == CollectionType.Photos; + var isPhotosCollection = collectionType == CollectionType.homevideos || collectionType == CollectionType.photos; if (!isPhotosCollection && result.Items.Count == 1) { var videoPath = result.Items[0].Path; diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs index 29d540700..c0b00caaf 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoAlbumResolver.cs @@ -46,8 +46,8 @@ namespace Emby.Server.Implementations.Library.Resolvers // Must be an image file within a photo collection var collectionType = args.GetCollectionType(); - if (collectionType == CollectionType.Photos - || (collectionType == CollectionType.HomeVideos && args.LibraryOptions.EnablePhotos)) + if (collectionType == CollectionType.photos + || (collectionType == CollectionType.homevideos && args.LibraryOptions.EnablePhotos)) { if (HasPhotos(args)) { diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs index d166ac37f..0934555b2 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs @@ -61,8 +61,8 @@ namespace Emby.Server.Implementations.Library.Resolvers // Must be an image file within a photo collection var collectionType = args.CollectionType; - if (collectionType == CollectionType.Photos - || (collectionType == CollectionType.HomeVideos && args.LibraryOptions.EnablePhotos)) + if (collectionType == CollectionType.photos + || (collectionType == CollectionType.homevideos && args.LibraryOptions.EnablePhotos)) { if (IsImageFile(args.Path, _imageProcessor)) { diff --git a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs index d4b3722c9..a50435ae6 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PlaylistResolver.cs @@ -23,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers private CollectionType?[] _musicPlaylistCollectionTypes = { null, - CollectionType.Music + CollectionType.music }; /// <inheritdoc/> diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs index 8274881be..5fd23c9f5 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs @@ -50,7 +50,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV // If the parent is a Season or Series and the parent is not an extras folder, then this is an Episode if the VideoResolver returns something // Also handle flat tv folders if (season is not null - || args.GetCollectionType() == CollectionType.TvShows + || args.GetCollectionType() == CollectionType.tvshows || args.HasParent<Series>()) { var episode = ResolveVideo<Episode>(args, false); diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index 2ae1138a5..1484c34bc 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -60,11 +60,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV var seriesInfo = Naming.TV.SeriesResolver.Resolve(_namingOptions, args.Path); var collectionType = args.GetCollectionType(); - if (collectionType == CollectionType.TvShows) + if (collectionType == CollectionType.tvshows) { // TODO refactor into separate class or something, this is copied from LibraryManager.GetConfiguredContentType var configuredContentType = args.GetConfiguredContentType(); - if (configuredContentType != CollectionType.TvShows) + if (configuredContentType != CollectionType.tvshows) { return new Series { diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index b916b9170..020cb517d 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query) { User user = null; - if (!query.UserId.Equals(default)) + if (!query.UserId.IsEmpty()) { user = _userManager.GetUserById(query.UserId); } @@ -177,7 +177,7 @@ namespace Emby.Server.Implementations.Library if (searchQuery.IncludeItemTypes.Length == 1 && searchQuery.IncludeItemTypes[0] == BaseItemKind.MusicArtist) { - if (!searchQuery.ParentId.Equals(default)) + if (!searchQuery.ParentId.IsEmpty()) { searchQuery.AncestorIds = new[] { searchQuery.ParentId }; searchQuery.ParentId = Guid.Empty; diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index a0a90b129..8beeb8041 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -81,6 +81,53 @@ namespace Emby.Server.Implementations.Library }); } + public void SaveUserData(User user, BaseItem item, UpdateUserItemDataDto userDataDto, UserDataSaveReason reason) + { + ArgumentNullException.ThrowIfNull(user); + ArgumentNullException.ThrowIfNull(item); + ArgumentNullException.ThrowIfNull(reason); + ArgumentNullException.ThrowIfNull(userDataDto); + + var userData = GetUserData(user, item); + + if (userDataDto.PlaybackPositionTicks.HasValue) + { + userData.PlaybackPositionTicks = userDataDto.PlaybackPositionTicks.Value; + } + + if (userDataDto.PlayCount.HasValue) + { + userData.PlayCount = userDataDto.PlayCount.Value; + } + + if (userDataDto.IsFavorite.HasValue) + { + userData.IsFavorite = userDataDto.IsFavorite.Value; + } + + if (userDataDto.Likes.HasValue) + { + userData.Likes = userDataDto.Likes.Value; + } + + if (userDataDto.Played.HasValue) + { + userData.Played = userDataDto.Played.Value; + } + + if (userDataDto.LastPlayedDate.HasValue) + { + userData.LastPlayedDate = userDataDto.LastPlayedDate.Value; + } + + if (userDataDto.Rating.HasValue) + { + userData.Rating = userDataDto.Rating.Value; + } + + SaveUserData(user, item, userData, reason, CancellationToken.None); + } + /// <summary> /// Save the provided user data for the given user. Batch operation. Does not fire any events or update the cache. /// </summary> diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index 113370fc3..83a66c8e4 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; @@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.Library var folderViewType = collectionFolder?.CollectionType; // Playlist library requires special handling because the folder only references user playlists - if (folderViewType == CollectionType.Playlists) + if (folderViewType == CollectionType.playlists) { var items = folder.GetItemList(new InternalItemsQuery(user) { @@ -99,14 +100,14 @@ namespace Emby.Server.Implementations.Library } } - foreach (var viewType in new[] { CollectionType.Movies, CollectionType.TvShows }) + foreach (var viewType in new[] { CollectionType.movies, CollectionType.tvshows }) { var parents = groupedFolders.Where(i => i.CollectionType == viewType || i.CollectionType is null) .ToList(); if (parents.Count > 0) { - var localizationKey = viewType == CollectionType.TvShows + var localizationKey = viewType == CollectionType.tvshows ? "TvShows" : "Movies"; @@ -117,7 +118,7 @@ namespace Emby.Server.Implementations.Library if (_config.Configuration.EnableFolderView) { var name = _localizationManager.GetLocalizedString("Folders"); - list.Add(_libraryManager.GetNamedView(name, CollectionType.Folders, string.Empty)); + list.Add(_libraryManager.GetNamedView(name, CollectionType.folders, string.Empty)); } if (query.IncludeExternalContent) @@ -151,7 +152,7 @@ namespace Emby.Server.Implementations.Library var index = Array.IndexOf(orders, i.Id); if (index == -1 && i is UserView view - && !view.DisplayParentId.Equals(default)) + && !view.DisplayParentId.IsEmpty()) { index = Array.IndexOf(orders, view.DisplayParentId); } @@ -253,7 +254,7 @@ namespace Emby.Server.Implementations.Library var parents = new List<BaseItem>(); - if (!parentId.Equals(default)) + if (!parentId.IsEmpty()) { var parentItem = _libraryManager.GetItemById(parentId); if (parentItem is Channel) @@ -279,7 +280,7 @@ namespace Emby.Server.Implementations.Library var isPlayed = request.IsPlayed; - if (parents.OfType<ICollectionFolder>().Any(i => i.CollectionType == CollectionType.Music)) + if (parents.OfType<ICollectionFolder>().Any(i => i.CollectionType == CollectionType.music)) { isPlayed = null; } @@ -305,11 +306,11 @@ namespace Emby.Server.Implementations.Library var hasCollectionType = parents.OfType<UserView>().ToArray(); if (hasCollectionType.Length > 0) { - if (hasCollectionType.All(i => i.CollectionType == CollectionType.Movies)) + if (hasCollectionType.All(i => i.CollectionType == CollectionType.movies)) { includeItemTypes = new[] { BaseItemKind.Movie }; } - else if (hasCollectionType.All(i => i.CollectionType == CollectionType.TvShows)) + else if (hasCollectionType.All(i => i.CollectionType == CollectionType.tvshows)) { includeItemTypes = new[] { BaseItemKind.Episode }; } @@ -324,18 +325,18 @@ namespace Emby.Server.Implementations.Library { switch (parent.CollectionType) { - case CollectionType.Books: + case CollectionType.books: mediaTypes.Add(MediaType.Book); mediaTypes.Add(MediaType.Audio); break; - case CollectionType.Music: + case CollectionType.music: mediaTypes.Add(MediaType.Audio); break; - case CollectionType.Photos: + case CollectionType.photos: mediaTypes.Add(MediaType.Photo); mediaTypes.Add(MediaType.Video); break; - case CollectionType.HomeVideos: + case CollectionType.homevideos: mediaTypes.Add(MediaType.Photo); mediaTypes.Add(MediaType.Video); break; diff --git a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs b/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs deleted file mode 100644 index 098f193fb..000000000 --- a/Emby.Server.Implementations/LiveTv/LiveTvConfigurationFactory.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections.Generic; -using MediaBrowser.Common.Configuration; -using MediaBrowser.Model.LiveTv; - -namespace Emby.Server.Implementations.LiveTv -{ - /// <summary> - /// <see cref="IConfigurationFactory" /> implementation for <see cref="LiveTvOptions" />. - /// </summary> - public class LiveTvConfigurationFactory : IConfigurationFactory - { - /// <inheritdoc /> - public IEnumerable<ConfigurationStore> GetConfigurations() - { - return new ConfigurationStore[] - { - new ConfigurationStore - { - ConfigurationType = typeof(LiveTvOptions), - Key = "livetv" - } - }; - } - } -} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/lineup.json b/Emby.Server.Implementations/Localization/Core/ab.json index 0967ef424..0967ef424 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/lineup.json +++ b/Emby.Server.Implementations/Localization/Core/ab.json diff --git a/Emby.Server.Implementations/Localization/Core/ca.json b/Emby.Server.Implementations/Localization/Core/ca.json index 26290df4d..c4d8c6947 100644 --- a/Emby.Server.Implementations/Localization/Core/ca.json +++ b/Emby.Server.Implementations/Localization/Core/ca.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractorDescription": "Extreu fotogrames clau dels fitxers de vídeo per crear llistes de reproducció HLS més precises. Aquesta tasca pot durar molt de temps.", "TaskKeyframeExtractor": "Extractor de fotogrames clau", "External": "Extern", - "HearingImpaired": "Discapacitat auditiva" + "HearingImpaired": "Discapacitat auditiva", + "TaskRefreshTrickplayImages": "Generar miniatures de línia de temps", + "TaskRefreshTrickplayImagesDescription": "Crear miniatures de línia de temps per vídeos en les biblioteques habilitades." } diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index 5da33febe..1c7bc75b5 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -83,8 +83,8 @@ "UserDeletedWithName": "Uživatel {0} byl smazán", "UserDownloadingItemWithValues": "{0} stahuje {1}", "UserLockedOutWithName": "Uživatel {0} byl odemčen", - "UserOfflineFromDevice": "{0} se odpojil od {1}", - "UserOnlineFromDevice": "{0} se připojil z {1}", + "UserOfflineFromDevice": "{0} se odpojil ze zařízení {1}", + "UserOnlineFromDevice": "{0} se připojil ze zařízení {1}", "UserPasswordChangedWithName": "Provedena změna hesla pro uživatele {0}", "UserPolicyUpdatedWithName": "Zásady uživatele pro {0} byly aktualizovány", "UserStartedPlayingItemWithValues": "{0} spustil přehrávání {1}", diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index f1dbf3c89..7a4c2067b 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -121,8 +121,8 @@ "Default": "Standard", "TaskOptimizeDatabaseDescription": "Komprimiert die Datenbank und trimmt den freien Speicherplatz. Die Ausführung dieser Aufgabe nach dem Scannen der Bibliothek oder nach anderen Änderungen, die Datenbankänderungen implizieren, kann die Leistung verbessern.", "TaskOptimizeDatabase": "Datenbank optimieren", - "TaskKeyframeExtractorDescription": "Extrahiere Keyframes aus Videodateien, um präzisere HLS-Playlisten zu erzeugen. Dieser Vorgang kann sehr lange dauern.", - "TaskKeyframeExtractor": "Keyframe Extraktor", + "TaskKeyframeExtractorDescription": "Extrahiert Keyframes aus Videodateien, um präzisere HLS-Playlisten zu erzeugen. Dieser Vorgang kann sehr lange dauern.", + "TaskKeyframeExtractor": "Keyframe-Extraktor", "External": "Extern", "HearingImpaired": "Hörgeschädigt", "TaskRefreshTrickplayImages": "Trickplay-Bilder generieren", diff --git a/Emby.Server.Implementations/Localization/Core/es_419.json b/Emby.Server.Implementations/Localization/Core/es_419.json index 3d5c04633..c6863ff36 100644 --- a/Emby.Server.Implementations/Localization/Core/es_419.json +++ b/Emby.Server.Implementations/Localization/Core/es_419.json @@ -123,5 +123,7 @@ "External": "Externo", "TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reproducción HLS más precisas. Esta tarea puede durar mucho tiempo.", "TaskKeyframeExtractor": "Extractor de Fotogramas Clave", - "HearingImpaired": "Discapacidad auditiva" + "HearingImpaired": "Discapacidad auditiva", + "TaskRefreshTrickplayImagesDescription": "Crea previsualizaciones para la barra de reproducción en las bibliotecas habilitadas.", + "TaskRefreshTrickplayImages": "Generar imágenes de la barra de reproducción" } diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json index 8e4bba25b..8364ce236 100644 --- a/Emby.Server.Implementations/Localization/Core/fa.json +++ b/Emby.Server.Implementations/Localization/Core/fa.json @@ -124,5 +124,7 @@ "TaskKeyframeExtractorDescription": "فریم های کلیدی را از فایل های ویدئویی استخراج می کند تا لیست های پخش HLS دقیق تری ایجاد کند. این کار ممکن است برای مدت طولانی اجرا شود.", "TaskKeyframeExtractor": "استخراج کننده فریم کلیدی", "External": "خارجی", - "HearingImpaired": "مشکل شنوایی" + "HearingImpaired": "مشکل شنوایی", + "TaskRefreshTrickplayImages": "تولید تصاویر Trickplay", + "TaskRefreshTrickplayImagesDescription": "تولید پیشنمایش های trickplay برای ویدیو های فعال شده در کتابخانه." } diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json index 88a4a358e..55ee1abaa 100644 --- a/Emby.Server.Implementations/Localization/Core/fil.json +++ b/Emby.Server.Implementations/Localization/Core/fil.json @@ -124,5 +124,6 @@ "TaskKeyframeExtractor": "Tagabunot ng Keyframe", "TaskKeyframeExtractorDescription": "Nagbubunot ng keyframe mula sa mga bidyo upang makabuo ng mas tumpak na HLS playlist. Maaaring matagal ito gawin.", "External": "External", - "TaskRefreshTrickplayImages": "Gumawa ng Trickplay na Imahe" + "TaskRefreshTrickplayImages": "Gumawa ng Trickplay na Imahe", + "TaskRefreshTrickplayImagesDescription": "Nanggagawa ng mga trickplay prebiyu para sa mga bidyo sa pinaganang mga aklatan." } diff --git a/Emby.Server.Implementations/Localization/Core/hy.json b/Emby.Server.Implementations/Localization/Core/hy.json new file mode 100644 index 000000000..563f84292 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/hy.json @@ -0,0 +1,39 @@ +{ + "TasksLibraryCategory": "Գրադարան", + "TasksApplicationCategory": "Հավելված", + "TaskCleanActivityLog": "Մաքրել ակտիվության մատյանը", + "Application": "Հավելված", + "AuthenticationSucceededWithUserName": "{0} հաջողությամբ վավերականացվել են", + "Books": "Գրքեր", + "CameraImageUploadedFrom": "Նոր լուսանկար է վերբեռնվել {0}-ի կողմից", + "Channels": "Ալիքներ", + "DeviceOfflineWithName": "{0}ը անջատվեց", + "External": "Արտաքին", + "FailedLoginAttemptWithUserName": "Ձախողված մուտքի փործ {0}-ի կողմից", + "Folders": "Պանակներ", + "HeaderContinueWatching": "Շարունակել դիտումը", + "Inherit": "Ժառանգել", + "ItemAddedWithName": "{0}ը ավացված է գրադարանի մեջ", + "ItemRemovedWithName": "{0}ը հեռացված է գրադարանից", + "LabelIpAddressValue": "IP հասցե` {0}", + "Movies": "Ֆիլմեր", + "Music": "Երաժշտություն", + "NameSeasonNumber": "Սեզոն {0}", + "Photos": "Լուսանկարներ", + "PluginInstalledWithName": "{0}ն տեղադրված է", + "Songs": "Երգեր", + "System": "Համակարգ", + "TvShows": "Հեռուստասերիալներ", + "User": "Օգտատեր", + "VersionNumber": "Տարբերակ {0}", + "TasksMaintenanceCategory": "Սպասարկում", + "TasksChannelsCategory": "Ինտերնետային ալիքներ", + "TaskRefreshPeople": "Թարմացնել մարդկանց", + "TaskRefreshChannels": "Թարմացնել ալիքները", + "TaskDownloadMissingSubtitles": "Ներբեռնել պակասող ենթագրերը", + "Albums": "Ալբոմներ", + "AppDeviceValues": "Հավելված` {0}, Սարք `{1}", + "ChapterNameValue": "Գլուխ {0}", + "Collections": "Հավաքածուներ", + "DeviceOnlineWithName": "{0}-ն միացված է" +} diff --git a/Emby.Server.Implementations/Localization/Core/ka.json b/Emby.Server.Implementations/Localization/Core/ka.json index dbbc81eeb..2d02522fe 100644 --- a/Emby.Server.Implementations/Localization/Core/ka.json +++ b/Emby.Server.Implementations/Localization/Core/ka.json @@ -4,9 +4,9 @@ "HeaderFavoriteAlbums": "რჩეული ალბომები", "TasksApplicationCategory": "აპლიკაცია", "Albums": "ალბომები", - "AppDeviceValues": "აპი: {0}, მოწყობილობა: {1}", + "AppDeviceValues": "აპლიკაცია: {0}, მოწყობილობა: {1}", "Application": "აპლიკაცია", - "Artists": "შემსრულებლები", + "Artists": "არტისტი", "AuthenticationSucceededWithUserName": "{0} -ის ავთენტიკაცია წარმატებულია", "Books": "წიგნები", "Forced": "ძალით", @@ -123,5 +123,7 @@ "TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული დამატებების განახლებების გადმოწერა და დაყენება.", "TaskCleanTranscodeDescription": "ერთ დღეზე უფრო ძველი ტრანსკოდირების ფაილების წაშლა.", "TaskDownloadMissingSubtitlesDescription": "მეტამონაცემებზე დაყრდნობით ინტერნეტში ნაკლული სუბტიტრების ძებნა.", - "TaskOptimizeDatabaseDescription": "ბაზს შეკუშვა და ადგილის გათავისუფლება. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს." + "TaskOptimizeDatabaseDescription": "ბაზს შეკუშვა და ადგილის გათავისუფლება. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს.", + "TaskRefreshTrickplayImagesDescription": "ქმნის trickplay წინასწარ ხედებს ვიდეოებისთვის ჩართულ ბიბლიოთეკებში.", + "TaskRefreshTrickplayImages": "Trickplay სურათების გენერირება" } diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json index 82a071309..6e58ef834 100644 --- a/Emby.Server.Implementations/Localization/Core/lv.json +++ b/Emby.Server.Implementations/Localization/Core/lv.json @@ -123,5 +123,7 @@ "External": "Ārējais", "HearingImpaired": "Ar dzirdes traucējumiem", "TaskKeyframeExtractor": "Atslēgkadru ekstraktors", - "TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs." + "TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs.", + "TaskRefreshTrickplayImages": "Ģenerēt partīšanas attēlus", + "TaskRefreshTrickplayImagesDescription": "Izveido priekšskatījumus videoklipu pārtīšanai iespējotajās bibliotēkās." } diff --git a/Emby.Server.Implementations/Localization/Core/nb.json b/Emby.Server.Implementations/Localization/Core/nb.json index 5c7dec7ef..b6c15d871 100644 --- a/Emby.Server.Implementations/Localization/Core/nb.json +++ b/Emby.Server.Implementations/Localization/Core/nb.json @@ -5,7 +5,7 @@ "Artists": "Artister", "AuthenticationSucceededWithUserName": "{0} har logget inn", "Books": "Bøker", - "CameraImageUploadedFrom": "Et nytt kamerabilde er lastet opp fra {0}", + "CameraImageUploadedFrom": "Et nytt kamerabilde har blitt lastet opp fra {0}", "Channels": "Kanaler", "ChapterNameValue": "Kapittel {0}", "Collections": "Samlinger", @@ -32,10 +32,10 @@ "LabelIpAddressValue": "IP-adresse: {0}", "LabelRunningTimeValue": "Spilletid {0}", "Latest": "Siste", - "MessageApplicationUpdated": "Jellyfin-tjeneren har blitt oppdatert", - "MessageApplicationUpdatedTo": "Jellyfin-tjeneren ble oppdatert til {0}", - "MessageNamedServerConfigurationUpdatedWithValue": "Tjenerkonfigurasjonsseksjon {0} har blitt oppdatert", - "MessageServerConfigurationUpdated": "Tjenerkonfigurasjon er oppdatert", + "MessageApplicationUpdated": "Jellyfin-serveren har blitt oppdatert", + "MessageApplicationUpdatedTo": "Jellyfin-serveren ble oppdatert til {0}", + "MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfigurasjonsseksjon {0} har blitt oppdatert", + "MessageServerConfigurationUpdated": "Serverkonfigurasjon har blitt oppdatert", "MixedContent": "Blandet innhold", "Movies": "Filmer", "Music": "Musikk", @@ -43,19 +43,19 @@ "NameInstallFailed": "Installasjonen av {0} mislyktes", "NameSeasonNumber": "Sesong {0}", "NameSeasonUnknown": "Ukjent sesong", - "NewVersionIsAvailable": "En ny versjon av Jellyfin-tjeneren er tilgjengelig for nedlasting.", + "NewVersionIsAvailable": "En ny versjon av Jellyfin Server er tilgjengelig for nedlasting.", "NotificationOptionApplicationUpdateAvailable": "En programvareoppdatering er tilgjengelig", "NotificationOptionApplicationUpdateInstalled": "Applikasjonsoppdatering installert", "NotificationOptionAudioPlayback": "Lydavspilling startet", "NotificationOptionAudioPlaybackStopped": "Lydavspilling stoppet", "NotificationOptionCameraImageUploaded": "Kamerabilde lastet opp", - "NotificationOptionInstallationFailed": "Installasjonen feilet", + "NotificationOptionInstallationFailed": "Installasjonsfeil", "NotificationOptionNewLibraryContent": "Nytt innhold lagt til", "NotificationOptionPluginError": "Programvareutvidelsesfeil", "NotificationOptionPluginInstalled": "Programvareutvidelse installert", "NotificationOptionPluginUninstalled": "Programvareutvidelse avinstallert", "NotificationOptionPluginUpdateInstalled": "Programvareutvidelsesoppdatering installert", - "NotificationOptionServerRestartRequired": "Tjeneromstart er nødvendig", + "NotificationOptionServerRestartRequired": "Serveromstart er nødvendig", "NotificationOptionTaskFailed": "Feil under utføring av planlagt oppgave", "NotificationOptionUserLockedOut": "Bruker er utestengt", "NotificationOptionVideoPlayback": "Videoavspilling startet", @@ -70,9 +70,9 @@ "ScheduledTaskFailedWithName": "{0} mislykkes", "ScheduledTaskStartedWithName": "{0} startet", "ServerNameNeedsToBeRestarted": "{0} må startes på nytt", - "Shows": "Program", + "Shows": "Serier", "Songs": "Sanger", - "StartupEmbyServerIsLoading": "Jellyfin-tjener laster. Prøv igjen snart.", + "StartupEmbyServerIsLoading": "Jellyfin Server laster. Prøv igjen snart.", "SubtitleDownloadFailureForItem": "En feil oppstå under nedlasting av undertekster for {0}", "SubtitleDownloadFailureFromForItem": "Kunne ikke laste ned undertekster fra {0} for {1}", "Sync": "Synkroniser", @@ -124,5 +124,7 @@ "TaskKeyframeExtractorDescription": "Trekker ut nøkkelbilder fra videofiler for å skape mere nøyaktige HLS-spillelister. Denne oppgaven kan ta lang tid.", "TaskKeyframeExtractor": "Nøkkelbilde-uttrekker", "External": "Ekstern", - "HearingImpaired": "Hørselshemmet" + "HearingImpaired": "Hørselshemmet", + "TaskRefreshTrickplayImages": "Generer Trickplay bilder", + "TaskRefreshTrickplayImagesDescription": "Oppretter trickplay-forhåndsvisninger for videoer i aktiverte biblioteker." } diff --git a/Emby.Server.Implementations/Localization/Core/nn.json b/Emby.Server.Implementations/Localization/Core/nn.json index 32d4f3a8b..d0c914de3 100644 --- a/Emby.Server.Implementations/Localization/Core/nn.json +++ b/Emby.Server.Implementations/Localization/Core/nn.json @@ -117,5 +117,6 @@ "TaskCleanActivityLog": "Slett aktivitetslogg", "Undefined": "Udefinert", "Forced": "Tvungen", - "Default": "Standard" + "Default": "Standard", + "External": "Ekstern" } diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json index 97062deec..1fc3cdbaa 100644 --- a/Emby.Server.Implementations/Localization/Core/sv.json +++ b/Emby.Server.Implementations/Localization/Core/sv.json @@ -43,7 +43,7 @@ "NameInstallFailed": "{0} installationen misslyckades", "NameSeasonNumber": "Säsong {0}", "NameSeasonUnknown": "Okänd säsong", - "NewVersionIsAvailable": "En ny version av Jellyfin Server är tillgänglig att hämta.", + "NewVersionIsAvailable": "En ny version av Jellyfin Server är tillgänglig för nedladdning.", "NotificationOptionApplicationUpdateAvailable": "Ny programversion tillgänglig", "NotificationOptionApplicationUpdateInstalled": "Programuppdatering installerad", "NotificationOptionAudioPlayback": "Ljuduppspelning har påbörjats", @@ -74,7 +74,7 @@ "Songs": "Låtar", "StartupEmbyServerIsLoading": "Jellyfin Server arbetar. Pröva igen snart.", "SubtitleDownloadFailureForItem": "Nerladdning av undertexter för {0} misslyckades", - "SubtitleDownloadFailureFromForItem": "Undertexter kunde inte laddas ner från {0} för {1}", + "SubtitleDownloadFailureFromForItem": "Undertexter kunde inte laddas ner från {0} till {1}", "Sync": "Synk", "System": "System", "TvShows": "TV-serier", diff --git a/Emby.Server.Implementations/Localization/Core/te.json b/Emby.Server.Implementations/Localization/Core/te.json index 24168b611..7d4422d62 100644 --- a/Emby.Server.Implementations/Localization/Core/te.json +++ b/Emby.Server.Implementations/Localization/Core/te.json @@ -38,5 +38,18 @@ "HeaderFavoriteSongs": "ఇష్టమైన పాటలు", "HeaderLiveTV": "ప్రత్యక్ష TV", "HeaderNextUp": "తదుపరి", - "HeaderRecordingGroups": "రికార్డింగ్ గుంపులు" + "HeaderRecordingGroups": "రికార్డింగ్ గుంపులు", + "MessageApplicationUpdated": "జెల్లీఫిన్ సర్వర్ అప్డేట్ చేయడం పూర్తి అయ్యింది", + "MessageApplicationUpdatedTo": "జెల్లీఫిన్ సర్వర్ {0} వెర్షన్ కి అప్డేట్ చెయ్యబడింది", + "MessageServerConfigurationUpdated": "సర్వర్ కన్ఫిగరేషన్ అప్డేట్ చేయబడింది", + "NewVersionIsAvailable": "జెల్లీఫిన్ సర్వర్ యొక్క కొత్త వెర్షన్ డౌన్లోడ్ చేసుకోవడానికి అందుబాటులో ఉంది.", + "NotificationOptionApplicationUpdateInstalled": "అప్లికేషన్ అప్డేట్ ఇన్స్టాల్ చేయబడింది", + "ItemAddedWithName": "{0} లైబ్రరీకి జోడించబడింది", + "ItemRemovedWithName": "లైబ్రరీ నుండి {0} తీసివేయబడింది", + "LabelIpAddressValue": "ఐపీ చిరునామా: {0}", + "LabelRunningTimeValue": "నడుస్తున్న సమయం: {0}", + "Latest": "తాజా", + "NameInstallFailed": "{0} ఇన్స్టాలేషన్ విఫలమైంది", + "NameSeasonUnknown": "భాగం తెలియదు", + "NotificationOptionApplicationUpdateAvailable": "అప్లికేషన్ అప్డేట్ అందుబాటులో ఉంది" } diff --git a/Emby.Server.Implementations/Localization/Core/tr.json b/Emby.Server.Implementations/Localization/Core/tr.json index a4877f4b5..d7a627d12 100644 --- a/Emby.Server.Implementations/Localization/Core/tr.json +++ b/Emby.Server.Implementations/Localization/Core/tr.json @@ -89,7 +89,7 @@ "UserPolicyUpdatedWithName": "{0} için kullanıcı politikası güncellendi", "UserStartedPlayingItemWithValues": "{0}, {2} cihazında {1} izliyor", "UserStoppedPlayingItemWithValues": "{0}, {2} cihazında {1} izlemeyi bitirdi", - "ValueHasBeenAddedToLibrary": "Medya kütüphanenize {0} eklendi", + "ValueHasBeenAddedToLibrary": "{0} medya kütüphanenize eklendi", "ValueSpecialEpisodeName": "Özel - {0}", "VersionNumber": "Sürüm {0}", "TaskCleanCache": "Önbellek Dizinini Temizle", @@ -111,7 +111,7 @@ "TaskCleanLogs": "Günlük Dizinini Temizle", "TaskRefreshLibraryDescription": "Medya kütüphanenize eklenen yeni dosyaları arar ve ortam bilgilerini yeniler.", "TaskRefreshLibrary": "Medya Kütüphanesini Tara", - "TaskRefreshChapterImagesDescription": "Sahnelere ayrılmış videolar için küçük resimler oluştur.", + "TaskRefreshChapterImagesDescription": "Bölümlere ayrılmış videolar için küçük resimler oluştur.", "TaskRefreshChapterImages": "Bölüm Resimlerini Çıkar", "TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler.", "TaskCleanActivityLog": "Etkinlik Günlüğünü Temizle", diff --git a/Emby.Server.Implementations/Localization/Core/uk.json b/Emby.Server.Implementations/Localization/Core/uk.json index bd5398f08..6f0dcfbe3 100644 --- a/Emby.Server.Implementations/Localization/Core/uk.json +++ b/Emby.Server.Implementations/Localization/Core/uk.json @@ -18,16 +18,16 @@ "HeaderContinueWatching": "Продовжити перегляд", "HeaderAlbumArtists": "Виконавці альбому", "Genres": "Жанри", - "Folders": "Каталоги", + "Folders": "Теки", "Favorites": "Обрані", "DeviceOnlineWithName": "Пристрій {0} підключився", "DeviceOfflineWithName": "Пристрій {0} відключився", - "Collections": "Добірки", - "ChapterNameValue": "Розділ {0}", + "Collections": "Колекції", + "ChapterNameValue": "Сцена {0}", "Channels": "Канали", - "CameraImageUploadedFrom": "Нова фотографія завантажена з {0}", + "CameraImageUploadedFrom": "Нову фотографію завантажено з {0}", "Books": "Книги", - "AuthenticationSucceededWithUserName": "{0} успішно автентифіковано", + "AuthenticationSucceededWithUserName": "{0} успішно авторизовано", "Artists": "Виконавці", "Application": "Додаток", "AppDeviceValues": "Додаток: {0}, Пристрій: {1}", @@ -83,7 +83,7 @@ "SubtitleDownloadFailureFromForItem": "Не вдалося завантажити субтитри з {0} для {1}", "StartupEmbyServerIsLoading": "Jellyfin Server завантажується. Будь ласка, спробуйте трішки пізніше.", "Songs": "Пісні", - "Shows": "Шоу", + "Shows": "Телепередачі", "ServerNameNeedsToBeRestarted": "{0} потрібно перезапустити", "ScheduledTaskStartedWithName": "{0} розпочато", "ScheduledTaskFailedWithName": "{0} незавершено, збій", diff --git a/Emby.Server.Implementations/Localization/Core/ur.json b/Emby.Server.Implementations/Localization/Core/ur.json new file mode 100644 index 000000000..376683041 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/ur.json @@ -0,0 +1,3 @@ +{ + "Books": "کتابیں" +} diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index e8b8c2c5f..3ab9774c2 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -1,15 +1,15 @@ { "Albums": "專輯", - "AppDeviceValues": "程式: {0}, 設備: {1}", + "AppDeviceValues": "程式:{0},設備:{1}", "Application": "應用程式", "Artists": "藝人", - "AuthenticationSucceededWithUserName": "{0} 授權成功", + "AuthenticationSucceededWithUserName": "成功授權 {0}", "Books": "書籍", "CameraImageUploadedFrom": "{0} 成功上傳一張新照片", "Channels": "頻道", "ChapterNameValue": "第 {0} 章", "Collections": "系列", - "DeviceOfflineWithName": "{0} 已斷開連接", + "DeviceOfflineWithName": "{0} 已中斷連接", "DeviceOnlineWithName": "{0} 已連接", "FailedLoginAttemptWithUserName": "{0} 登入失敗", "Favorites": "我的最愛", @@ -27,15 +27,15 @@ "HeaderRecordingGroups": "錄製組", "HomeVideos": "家庭影片", "Inherit": "繼承", - "ItemAddedWithName": "{0} 已被添加至媒體庫", + "ItemAddedWithName": "{0} 已被加入至媒體庫", "ItemRemovedWithName": "{0} 已從媒體庫移除", - "LabelIpAddressValue": "IP 地址: {0}", - "LabelRunningTimeValue": "運行時間: {0}", + "LabelIpAddressValue": "IP 地址:{0}", + "LabelRunningTimeValue": "運作時間:{0}", "Latest": "最新", "MessageApplicationUpdated": "Jellyfin 已被更新", "MessageApplicationUpdatedTo": "Jellyfin 已被更新至 {0}", "MessageNamedServerConfigurationUpdatedWithValue": "伺服器設定 {0} 已被更新", - "MessageServerConfigurationUpdated": "伺服器設定已經被更新", + "MessageServerConfigurationUpdated": "已更新伺服器設定", "MixedContent": "混合內容", "Movies": "電影", "Music": "音樂", @@ -43,23 +43,23 @@ "NameInstallFailed": "{0} 安裝失敗", "NameSeasonNumber": "第 {0} 季", "NameSeasonUnknown": "未知的季度", - "NewVersionIsAvailable": "有較新版本的 Jellyfin 可供下載。", + "NewVersionIsAvailable": "有新版本的 Jellyfin 可供下載。", "NotificationOptionApplicationUpdateAvailable": "有可用的更新", - "NotificationOptionApplicationUpdateInstalled": "應用程式已被更新", - "NotificationOptionAudioPlayback": "開始播放音訊", + "NotificationOptionApplicationUpdateInstalled": "完成更新應用程式", + "NotificationOptionAudioPlayback": "播放音訊", "NotificationOptionAudioPlaybackStopped": "停止播放音訊", - "NotificationOptionCameraImageUploaded": "相片已被上傳", + "NotificationOptionCameraImageUploaded": "相片上傳", "NotificationOptionInstallationFailed": "安裝失敗", - "NotificationOptionNewLibraryContent": "已添加新内容", - "NotificationOptionPluginError": "插件出現錯誤", - "NotificationOptionPluginInstalled": "插件已被安裝", - "NotificationOptionPluginUninstalled": "插件已被移除", - "NotificationOptionPluginUpdateInstalled": "插件已被更新", + "NotificationOptionNewLibraryContent": "新增媒體", + "NotificationOptionPluginError": "插件錯誤", + "NotificationOptionPluginInstalled": "安裝插件", + "NotificationOptionPluginUninstalled": "解除安裝插件", + "NotificationOptionPluginUpdateInstalled": "完成更新插件", "NotificationOptionServerRestartRequired": "伺服器需要重啟", - "NotificationOptionTaskFailed": "排程任務執行失敗", - "NotificationOptionUserLockedOut": "用戶已被鎖定", - "NotificationOptionVideoPlayback": "開始播放影片", - "NotificationOptionVideoPlaybackStopped": "已停止播放影片", + "NotificationOptionTaskFailed": "排程工作執行失敗", + "NotificationOptionUserLockedOut": "封鎖用戶", + "NotificationOptionVideoPlayback": "播放影片", + "NotificationOptionVideoPlaybackStopped": "停止播放影片", "Photos": "相片", "Playlists": "播放清單", "Plugin": "插件", @@ -68,7 +68,7 @@ "PluginUpdatedWithName": "已更新 {0}", "ProviderValue": "提供者:{0}", "ScheduledTaskFailedWithName": "{0} 執行失敗", - "ScheduledTaskStartedWithName": "{0} 開始執行", + "ScheduledTaskStartedWithName": "開始執行 {0}", "ServerNameNeedsToBeRestarted": "{0} 需要重啟", "Shows": "節目", "Songs": "歌曲", @@ -79,50 +79,52 @@ "System": "系統", "TvShows": "電視節目", "User": "用戶", - "UserCreatedWithName": "用戶 {0} 已被建立", + "UserCreatedWithName": "建立新用戶 {0}", "UserDeletedWithName": "用戶 {0} 已被移除", "UserDownloadingItemWithValues": "{0} 正在下載 {1}", - "UserLockedOutWithName": "使用者 {0} 已被鎖定", - "UserOfflineFromDevice": "{0} 從 {1} 斷開連接", + "UserLockedOutWithName": "用戶 {0} 已被封鎖", + "UserOfflineFromDevice": "{0} 終止了 {1} 的連接", "UserOnlineFromDevice": "{0} 從 {1} 連線", - "UserPasswordChangedWithName": "{0} 的密碼已被變改", - "UserPolicyUpdatedWithName": "使用者協議已更新為 {0}", - "UserStartedPlayingItemWithValues": "{0} 正在 {2} 上播放 {1}", - "UserStoppedPlayingItemWithValues": "{0} 已停止在 {2} 上播放 {1}", - "ValueHasBeenAddedToLibrary": "已添加 {0} 到你的媒體庫", + "UserPasswordChangedWithName": "{0} 的密碼已被更改", + "UserPolicyUpdatedWithName": "使用條款已更新為 {0}", + "UserStartedPlayingItemWithValues": "{0} 在 {2} 上播放 {1}", + "UserStoppedPlayingItemWithValues": "{0} 停止在 {2} 上播放 {1}", + "ValueHasBeenAddedToLibrary": "{0} 已被加入至你的媒體庫", "ValueSpecialEpisodeName": "特典 - {0}", "VersionNumber": "版本 {0}", - "TaskDownloadMissingSubtitles": "下載缺少的字幕", + "TaskDownloadMissingSubtitles": "下載欠缺字幕", "TaskUpdatePlugins": "更新插件", "TasksApplicationCategory": "應用程式", - "TaskRefreshLibraryDescription": "掃描媒體庫以加入新增檔案及重新載入 metadata。", + "TaskRefreshLibraryDescription": "掃描媒體庫以加入新增的檔案及重新載入元數據。", "TasksMaintenanceCategory": "維護", - "TaskDownloadMissingSubtitlesDescription": "根據元數據中的設定,在互聯網上搜索缺少的字幕。", + "TaskDownloadMissingSubtitlesDescription": "根據元數據中的設定,在網上搜尋欠缺的字幕。", "TaskRefreshChannelsDescription": "重新載入網絡頻道的資訊。", "TaskRefreshChannels": "重新載入頻道", - "TaskCleanTranscodeDescription": "刪除超過一天的轉碼文件。", - "TaskCleanTranscode": "清理轉碼目錄", + "TaskCleanTranscodeDescription": "刪除超過一天的轉碼檔案。", + "TaskCleanTranscode": "清理轉碼檔資料夾", "TaskUpdatePluginsDescription": "下載並更新能夠被自動更新的插件。", - "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的元數據。", + "TaskRefreshPeopleDescription": "更新你的媒體中有關的演員和導演的元數據。", "TaskCleanLogsDescription": "刪除超過{0}天的紀錄檔。", - "TaskCleanLogs": "清理紀錄檔目錄", + "TaskCleanLogs": "清理紀錄檔資料夾", "TaskRefreshLibrary": "掃描媒體庫", "TaskRefreshChapterImagesDescription": "為帶有章節的影片建立縮圖。", "TaskRefreshChapterImages": "提取章節圖像", "TaskCleanCacheDescription": "刪除系統不再需要的緩存文件。", - "TaskCleanCache": "清理緩存目錄", + "TaskCleanCache": "清理緩存資料夾", "TasksChannelsCategory": "網絡頻道", - "TasksLibraryCategory": "庫", + "TasksLibraryCategory": "媒體庫", "TaskRefreshPeople": "重新載入人物", "TaskCleanActivityLog": "清理活動記錄", "Undefined": "未定義", "Forced": "強制", "Default": "預設", - "TaskOptimizeDatabaseDescription": "壓縮數據庫並截斷可用空間。在掃描媒體庫或執行其他數據庫的修改後運行此任務可能會提高性能。", + "TaskOptimizeDatabaseDescription": "壓縮數據庫及釋放可用空間。完成任何會修改數據庫的工作(例如掃描媒體庫)後,執行此工作或可提升伺服器速度。", "TaskOptimizeDatabase": "最佳化數據庫", "TaskCleanActivityLogDescription": "刪除早於設定時間的活動記錄。", - "TaskKeyframeExtractorDescription": "提取關鍵幀以建立更準確的 HLS 播放列表。此工作或需要使用較長時間來完成。", - "TaskKeyframeExtractor": "關鍵幀提取器", + "TaskKeyframeExtractorDescription": "提取關鍵影格(Keyframe)以建立更準確的 HLS playlist。此工作可能需要使用較長時間來完成。", + "TaskKeyframeExtractor": "關鍵影格提取器", "External": "外部", - "HearingImpaired": "聽力障礙" + "HearingImpaired": "聽力障礙", + "TaskRefreshTrickplayImages": "建立 Trickplay 圖像", + "TaskRefreshTrickplayImagesDescription": "為已啟用 Trickplay 的媒體庫內的影片建立 Trickplay 預覽圖。" } diff --git a/Emby.Server.Implementations/Localization/countries.json b/Emby.Server.Implementations/Localization/countries.json index 22ffc5e09..0a11b3e45 100644 --- a/Emby.Server.Implementations/Localization/countries.json +++ b/Emby.Server.Implementations/Localization/countries.json @@ -696,7 +696,7 @@ "TwoLetterISORegionName": "SI" }, { - "DisplayName": "Soomaaliya", + "DisplayName": "Somalia", "Name": "SO", "ThreeLetterISORegionName": "SOM", "TwoLetterISORegionName": "SO" diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index d2e2fd7d5..aea8d6532 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -11,6 +11,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -178,7 +179,7 @@ namespace Emby.Server.Implementations.Playlists public Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId) { - var user = userId.Equals(default) ? null : _userManager.GetUserById(userId); + var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId); return AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false) { diff --git a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs index 5c616d534..f65d609c7 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistsFolder.cs @@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Playlists public override bool SupportsInheritedParentImages => false; [JsonIgnore] - public override CollectionType? CollectionType => Jellyfin.Data.Enums.CollectionType.Playlists; + public override CollectionType? CollectionType => Jellyfin.Data.Enums.CollectionType.playlists; protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user) { diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs index acd4bf905..812df8192 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs @@ -115,9 +115,10 @@ public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask List<LinkedChild>? itemsToRemove = null; foreach (var linkedChild in folder.LinkedChildren) { - if (!File.Exists(folder.Path)) + var path = linkedChild.Path; + if (!File.Exists(path)) { - _logger.LogInformation("Item in {FolderName} cannot be found at {ItemPath}", folder.Name, linkedChild.Path); + _logger.LogInformation("Item in {FolderName} cannot be found at {ItemPath}", folder.Name, path); (itemsToRemove ??= new List<LinkedChild>()).Add(linkedChild); } } diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index e8e63d286..bbb3938dc 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -189,7 +189,7 @@ namespace Emby.Server.Implementations.Session _logger); } - private void OnSessionEnded(SessionInfo info) + private async ValueTask OnSessionEnded(SessionInfo info) { EventHelper.QueueEventIfNotNull( SessionEnded, @@ -202,7 +202,7 @@ namespace Emby.Server.Implementations.Session _eventManager.Publish(new SessionEndedEventArgs(info)); - info.Dispose(); + await info.DisposeAsync().ConfigureAwait(false); } /// <inheritdoc /> @@ -301,12 +301,12 @@ namespace Emby.Server.Implementations.Session await _mediaSourceManager.CloseLiveStream(session.PlayState.LiveStreamId).ConfigureAwait(false); } - OnSessionEnded(session); + await OnSessionEnded(session).ConfigureAwait(false); } } /// <inheritdoc /> - public void ReportSessionEnded(string sessionId) + public async ValueTask ReportSessionEnded(string sessionId) { CheckDisposed(); var session = GetSession(sessionId, false); @@ -317,7 +317,7 @@ namespace Emby.Server.Implementations.Session _activeConnections.TryRemove(key, out _); - OnSessionEnded(session); + await OnSessionEnded(session).ConfigureAwait(false); } } @@ -337,7 +337,7 @@ namespace Emby.Server.Implementations.Session info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture); } - if (!info.ItemId.Equals(default) && info.Item is null && libraryItem is not null) + if (!info.ItemId.IsEmpty() && info.Item is null && libraryItem is not null) { var current = session.NowPlayingItem; @@ -529,7 +529,7 @@ namespace Emby.Server.Implementations.Session { var users = new List<User>(); - if (session.UserId.Equals(default)) + if (session.UserId.IsEmpty()) { return users; } @@ -690,7 +690,7 @@ namespace Emby.Server.Implementations.Session var session = GetSession(info.SessionId); - var libraryItem = info.ItemId.Equals(default) + var libraryItem = info.ItemId.IsEmpty() ? null : GetNowPlayingItem(session, info.ItemId); @@ -784,7 +784,7 @@ namespace Emby.Server.Implementations.Session var session = GetSession(info.SessionId); - var libraryItem = info.ItemId.Equals(default) + var libraryItem = info.ItemId.IsEmpty() ? null : GetNowPlayingItem(session, info.ItemId); @@ -923,7 +923,7 @@ namespace Emby.Server.Implementations.Session session.StopAutomaticProgress(); - var libraryItem = info.ItemId.Equals(default) + var libraryItem = info.ItemId.IsEmpty() ? null : GetNowPlayingItem(session, info.ItemId); @@ -933,7 +933,7 @@ namespace Emby.Server.Implementations.Session info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture); } - if (!info.ItemId.Equals(default) && info.Item is null && libraryItem is not null) + if (!info.ItemId.IsEmpty() && info.Item is null && libraryItem is not null) { var current = session.NowPlayingItem; @@ -1154,7 +1154,7 @@ namespace Emby.Server.Implementations.Session var session = GetSessionToRemoteControl(sessionId); - var user = session.UserId.Equals(default) ? null : _userManager.GetUserById(session.UserId); + var user = session.UserId.IsEmpty() ? null : _userManager.GetUserById(session.UserId); List<BaseItem> items; @@ -1223,7 +1223,7 @@ namespace Emby.Server.Implementations.Session { var controllingSession = GetSession(controllingSessionId); AssertCanControl(session, controllingSession); - if (!controllingSession.UserId.Equals(default)) + if (!controllingSession.UserId.IsEmpty()) { command.ControllingUserId = controllingSession.UserId; } @@ -1342,7 +1342,7 @@ namespace Emby.Server.Implementations.Session { var controllingSession = GetSession(controllingSessionId); AssertCanControl(session, controllingSession); - if (!controllingSession.UserId.Equals(default)) + if (!controllingSession.UserId.IsEmpty()) { command.ControllingUserId = controllingSession.UserId.ToString("N", CultureInfo.InvariantCulture); } @@ -1463,7 +1463,7 @@ namespace Emby.Server.Implementations.Session ArgumentException.ThrowIfNullOrEmpty(request.AppVersion); User user = null; - if (!request.UserId.Equals(default)) + if (!request.UserId.IsEmpty()) { user = _userManager.GetUserById(request.UserId); } @@ -1590,7 +1590,7 @@ namespace Emby.Server.Implementations.Session { try { - ReportSessionEnded(session.Id); + await ReportSessionEnded(session.Id).ConfigureAwait(false); } catch (Exception ex) { @@ -1670,7 +1670,6 @@ namespace Emby.Server.Implementations.Session var fields = dtoOptions.Fields.ToList(); - fields.Remove(ItemFields.BasicSyncInfo); fields.Remove(ItemFields.CanDelete); fields.Remove(ItemFields.CanDownload); fields.Remove(ItemFields.ChildCount); @@ -1767,7 +1766,7 @@ namespace Emby.Server.Implementations.Session { ArgumentNullException.ThrowIfNull(info); - var user = info.UserId.Equals(default) + var user = info.UserId.IsEmpty() ? null : _userManager.GetUserById(info.UserId); diff --git a/Emby.Server.Implementations/SyncPlay/Group.cs b/Emby.Server.Implementations/SyncPlay/Group.cs index da8f94932..a7821c0e0 100644 --- a/Emby.Server.Implementations/SyncPlay/Group.cs +++ b/Emby.Server.Implementations/SyncPlay/Group.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; +using Jellyfin.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Session; using MediaBrowser.Controller.SyncPlay; @@ -553,7 +554,7 @@ namespace Emby.Server.Implementations.SyncPlay if (playingItemRemoved) { var itemId = PlayQueue.GetPlayingItemId(); - if (!itemId.Equals(default)) + if (!itemId.IsEmpty()) { var item = _libraryManager.GetItemById(itemId); RunTimeTicks = item.RunTimeTicks ?? 0; diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index ef890aeb4..34c9e86f2 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -41,7 +42,7 @@ namespace Emby.Server.Implementations.TV } string? presentationUniqueKey = null; - if (query.SeriesId.HasValue && !query.SeriesId.Value.Equals(default)) + if (!query.SeriesId.IsNullOrEmpty()) { if (_libraryManager.GetItemById(query.SeriesId.Value) is Series series) { @@ -91,7 +92,7 @@ namespace Emby.Server.Implementations.TV string? presentationUniqueKey = null; int? limit = null; - if (request.SeriesId.HasValue && !request.SeriesId.Value.Equals(default)) + if (!request.SeriesId.IsNullOrEmpty()) { if (_libraryManager.GetItemById(request.SeriesId.Value) is Series series) { @@ -146,7 +147,7 @@ namespace Emby.Server.Implementations.TV // If viewing all next up for all series, remove first episodes // But if that returns empty, keep those first episodes (avoid completely empty view) - var alwaysEnableFirstEpisode = request.SeriesId.HasValue && !request.SeriesId.Value.Equals(default); + var alwaysEnableFirstEpisode = !request.SeriesId.IsNullOrEmpty(); var anyFound = false; return allNextUp diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 15c4cfdf0..ce3d6cab8 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -11,6 +11,7 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Events; +using Jellyfin.Extensions; using Jellyfin.Extensions.Json; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; @@ -227,7 +228,7 @@ namespace Emby.Server.Implementations.Updates availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); } - if (!id.Equals(default)) + if (!id.IsEmpty()) { availablePackages = availablePackages.Where(x => x.Id.Equals(id)); } diff --git a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs index cf3cb6905..7d0fe5589 100644 --- a/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs +++ b/Jellyfin.Api/Auth/DefaultAuthorizationPolicy/DefaultAuthorizationHandler.cs @@ -2,6 +2,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; @@ -41,7 +42,7 @@ namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy var isApiKey = context.User.GetIsApiKey(); var userId = context.User.GetUserId(); // This likely only happens during the wizard, so skip the default checks and let any other handlers do it - if (!isApiKey && userId.Equals(default)) + if (!isApiKey && userId.IsEmpty()) { return Task.CompletedTask; } diff --git a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs index 688a13bc0..965b7e7e6 100644 --- a/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs +++ b/Jellyfin.Api/Auth/FirstTimeSetupPolicy/FirstTimeSetupHandler.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; +using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; @@ -46,7 +47,7 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy } var userId = contextUser.GetUserId(); - if (userId.Equals(default)) + if (userId.IsEmpty()) { context.Fail(); return Task.CompletedTask; diff --git a/Jellyfin.Api/Controllers/ArtistsController.cs b/Jellyfin.Api/Controllers/ArtistsController.cs index e7d3e694a..8b931f162 100644 --- a/Jellyfin.Api/Controllers/ArtistsController.cs +++ b/Jellyfin.Api/Controllers/ArtistsController.cs @@ -6,6 +6,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -126,7 +127,7 @@ public class ArtistsController : BaseJellyfinApiController User? user = null; BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); - if (!userId.Value.Equals(default)) + if (!userId.IsNullOrEmpty()) { user = _userManager.GetUserById(userId.Value); } @@ -330,7 +331,7 @@ public class ArtistsController : BaseJellyfinApiController User? user = null; BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); - if (!userId.Value.Equals(default)) + if (!userId.IsNullOrEmpty()) { user = _userManager.GetUserById(userId.Value); } @@ -469,7 +470,7 @@ public class ArtistsController : BaseJellyfinApiController var item = _libraryManager.GetArtist(name, dtoOptions); - if (!userId.Value.Equals(default)) + if (!userId.IsNullOrEmpty()) { var user = _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/AudioController.cs b/Jellyfin.Api/Controllers/AudioController.cs index 5bc533086..cd09d2bfa 100644 --- a/Jellyfin.Api/Controllers/AudioController.cs +++ b/Jellyfin.Api/Controllers/AudioController.cs @@ -6,6 +6,7 @@ using Jellyfin.Api.Attributes; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Streaming; using MediaBrowser.Model.Dlna; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; diff --git a/Jellyfin.Api/Controllers/ChannelsController.cs b/Jellyfin.Api/Controllers/ChannelsController.cs index fdc16ee23..f83c71b57 100644 --- a/Jellyfin.Api/Controllers/ChannelsController.cs +++ b/Jellyfin.Api/Controllers/ChannelsController.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -126,7 +127,7 @@ public class ChannelsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) { userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); @@ -201,7 +202,7 @@ public class ChannelsController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] Guid[] channelIds) { userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/DevicesController.cs b/Jellyfin.Api/Controllers/DevicesController.cs index aa200a722..6d9ec343e 100644 --- a/Jellyfin.Api/Controllers/DevicesController.cs +++ b/Jellyfin.Api/Controllers/DevicesController.cs @@ -42,16 +42,15 @@ public class DevicesController : BaseJellyfinApiController /// <summary> /// Get Devices. /// </summary> - /// <param name="supportsSync">Gets or sets a value indicating whether [supports synchronize].</param> /// <param name="userId">Gets or sets the user identifier.</param> /// <response code="200">Devices retrieved.</response> /// <returns>An <see cref="OkResult"/> containing the list of devices.</returns> [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] bool? supportsSync, [FromQuery] Guid? userId) + public async Task<ActionResult<QueryResult<DeviceInfo>>> GetDevices([FromQuery] Guid? userId) { userId = RequestHelpers.GetUserId(User, userId); - return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false); + return await _deviceManager.GetDevicesForUser(userId).ConfigureAwait(false); } /// <summary> diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 9e9c610cc..dda1e9d56 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -9,8 +9,8 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; +using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; -using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Data.Enums; using Jellyfin.Extensions; @@ -19,6 +19,7 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Streaming; using MediaBrowser.MediaEncoding.Encoder; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; @@ -51,7 +52,7 @@ public class DynamicHlsController : BaseJellyfinApiController private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; private readonly IFileSystem _fileSystem; - private readonly TranscodingJobHelper _transcodingJobHelper; + private readonly ITranscodeManager _transcodeManager; private readonly ILogger<DynamicHlsController> _logger; private readonly EncodingHelper _encodingHelper; private readonly IDynamicHlsPlaylistGenerator _dynamicHlsPlaylistGenerator; @@ -67,7 +68,7 @@ public class DynamicHlsController : BaseJellyfinApiController /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param> + /// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param> /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param> /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> @@ -79,7 +80,7 @@ public class DynamicHlsController : BaseJellyfinApiController IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, IFileSystem fileSystem, - TranscodingJobHelper transcodingJobHelper, + ITranscodeManager transcodeManager, ILogger<DynamicHlsController> logger, DynamicHlsHelper dynamicHlsHelper, EncodingHelper encodingHelper, @@ -91,7 +92,7 @@ public class DynamicHlsController : BaseJellyfinApiController _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; _fileSystem = fileSystem; - _transcodingJobHelper = transcodingJobHelper; + _transcodeManager = transcodeManager; _logger = logger; _dynamicHlsHelper = dynamicHlsHelper; _encodingHelper = encodingHelper; @@ -283,17 +284,17 @@ public class DynamicHlsController : BaseJellyfinApiController _serverConfigurationManager, _mediaEncoder, _encodingHelper, - _transcodingJobHelper, + _transcodeManager, TranscodingJobType, cancellationToken) .ConfigureAwait(false); - TranscodingJobDto? job = null; + TranscodingJob? job = null; var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8"); if (!System.IO.File.Exists(playlistPath)) { - var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath); + var transcodingLock = _transcodeManager.GetTranscodingLock(playlistPath); await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false); try { @@ -302,11 +303,11 @@ public class DynamicHlsController : BaseJellyfinApiController // If the playlist doesn't already exist, startup ffmpeg try { - job = await _transcodingJobHelper.StartFfMpeg( + job = await _transcodeManager.StartFfMpeg( state, playlistPath, GetCommandLineArguments(playlistPath, state, true, 0), - Request, + Request.HttpContext.User.GetUserId(), TranscodingJobType, cancellationTokenSource) .ConfigureAwait(false); @@ -331,11 +332,11 @@ public class DynamicHlsController : BaseJellyfinApiController } } - job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); + job ??= _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); if (job is not null) { - _transcodingJobHelper.OnTranscodeEndRequest(job); + _transcodeManager.OnTranscodeEndRequest(job); } var playlistText = HlsHelpers.GetLivePlaylistText(playlistPath, state); @@ -1383,7 +1384,7 @@ public class DynamicHlsController : BaseJellyfinApiController _serverConfigurationManager, _mediaEncoder, _encodingHelper, - _transcodingJobHelper, + _transcodeManager, TranscodingJobType, cancellationTokenSource.Token) .ConfigureAwait(false); @@ -1421,7 +1422,7 @@ public class DynamicHlsController : BaseJellyfinApiController _serverConfigurationManager, _mediaEncoder, _encodingHelper, - _transcodingJobHelper, + _transcodeManager, TranscodingJobType, cancellationToken) .ConfigureAwait(false); @@ -1432,16 +1433,16 @@ public class DynamicHlsController : BaseJellyfinApiController var segmentExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer); - TranscodingJobDto? job; + TranscodingJob? job; if (System.IO.File.Exists(segmentPath)) { - job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); + job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); _logger.LogDebug("returning {0} [it exists, try 1]", segmentPath); return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false); } - var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath); + var transcodingLock = _transcodeManager.GetTranscodingLock(playlistPath); await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false); var released = false; var startTranscoding = false; @@ -1450,7 +1451,7 @@ public class DynamicHlsController : BaseJellyfinApiController { if (System.IO.File.Exists(segmentPath)) { - job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); + job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); transcodingLock.Release(); released = true; _logger.LogDebug("returning {0} [it exists, try 2]", segmentPath); @@ -1488,7 +1489,7 @@ public class DynamicHlsController : BaseJellyfinApiController // If the playlist doesn't already exist, startup ffmpeg try { - await _transcodingJobHelper.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false) + await _transcodeManager.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false) .ConfigureAwait(false); if (currentTranscodingIndex.HasValue) @@ -1499,11 +1500,11 @@ public class DynamicHlsController : BaseJellyfinApiController streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks; state.WaitForPath = segmentPath; - job = await _transcodingJobHelper.StartFfMpeg( + job = await _transcodeManager.StartFfMpeg( state, playlistPath, GetCommandLineArguments(playlistPath, state, false, segmentId), - Request, + Request.HttpContext.User.GetUserId(), TranscodingJobType, cancellationTokenSource).ConfigureAwait(false); } @@ -1517,7 +1518,7 @@ public class DynamicHlsController : BaseJellyfinApiController } else { - job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); + job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); if (job?.TranscodingThrottler is not null) { await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false); @@ -1534,7 +1535,7 @@ public class DynamicHlsController : BaseJellyfinApiController } _logger.LogDebug("returning {0} [general case]", segmentPath); - job ??= _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); + job ??= _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false); } @@ -1922,7 +1923,7 @@ public class DynamicHlsController : BaseJellyfinApiController string segmentPath, string segmentExtension, int segmentIndex, - TranscodingJobDto? transcodingJob, + TranscodingJob? transcodingJob, CancellationToken cancellationToken) { var segmentExists = System.IO.File.Exists(segmentPath); @@ -1991,7 +1992,7 @@ public class DynamicHlsController : BaseJellyfinApiController return GetSegmentResult(state, segmentPath, transcodingJob); } - private ActionResult GetSegmentResult(StreamState state, string segmentPath, TranscodingJobDto? transcodingJob) + private ActionResult GetSegmentResult(StreamState state, string segmentPath, TranscodingJob? transcodingJob) { var segmentEndingPositionTicks = state.Request.CurrentRuntimeTicks + state.Request.ActualSegmentLengthTicks; @@ -2001,7 +2002,7 @@ public class DynamicHlsController : BaseJellyfinApiController if (transcodingJob is not null) { transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks); - _transcodingJobHelper.OnTranscodeEndRequest(transcodingJob); + _transcodeManager.OnTranscodeEndRequest(transcodingJob); } return Task.CompletedTask; @@ -2012,7 +2013,7 @@ public class DynamicHlsController : BaseJellyfinApiController private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension) { - var job = _transcodingJobHelper.GetTranscodingJob(playlist, TranscodingJobType); + var job = _transcodeManager.GetTranscodingJob(playlist, TranscodingJobType); if (job is null || job.HasExited) { diff --git a/Jellyfin.Api/Controllers/FilterController.cs b/Jellyfin.Api/Controllers/FilterController.cs index baeb8b81a..d6e043e6a 100644 --- a/Jellyfin.Api/Controllers/FilterController.cs +++ b/Jellyfin.Api/Controllers/FilterController.cs @@ -3,6 +3,7 @@ using System.Linq; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -53,7 +54,7 @@ public class FilterController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] mediaTypes) { userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); @@ -146,7 +147,7 @@ public class FilterController : BaseJellyfinApiController [FromQuery] bool? recursive) { userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/GenresController.cs b/Jellyfin.Api/Controllers/GenresController.cs index 062e1062d..54d48aec2 100644 --- a/Jellyfin.Api/Controllers/GenresController.cs +++ b/Jellyfin.Api/Controllers/GenresController.cs @@ -6,6 +6,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -95,7 +96,7 @@ public class GenresController : BaseJellyfinApiController .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); - User? user = userId.Value.Equals(default) + User? user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); @@ -131,8 +132,8 @@ public class GenresController : BaseJellyfinApiController QueryResult<(BaseItem, ItemCounts)> result; if (parentItem is ICollectionFolder parentCollectionFolder - && (parentCollectionFolder.CollectionType == CollectionType.Music - || parentCollectionFolder.CollectionType == CollectionType.MusicVideos)) + && (parentCollectionFolder.CollectionType == CollectionType.music + || parentCollectionFolder.CollectionType == CollectionType.musicvideos)) { result = _libraryManager.GetMusicGenres(query); } @@ -172,7 +173,7 @@ public class GenresController : BaseJellyfinApiController item ??= new Genre(); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index 392d9955f..1927a332b 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -24,22 +24,22 @@ public class HlsSegmentController : BaseJellyfinApiController { private readonly IFileSystem _fileSystem; private readonly IServerConfigurationManager _serverConfigurationManager; - private readonly TranscodingJobHelper _transcodingJobHelper; + private readonly ITranscodeManager _transcodeManager; /// <summary> /// Initializes a new instance of the <see cref="HlsSegmentController"/> class. /// </summary> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> - /// <param name="transcodingJobHelper">Initialized instance of the <see cref="TranscodingJobHelper"/>.</param> + /// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param> public HlsSegmentController( IFileSystem fileSystem, IServerConfigurationManager serverConfigurationManager, - TranscodingJobHelper transcodingJobHelper) + ITranscodeManager transcodeManager) { _fileSystem = fileSystem; _serverConfigurationManager = serverConfigurationManager; - _transcodingJobHelper = transcodingJobHelper; + _transcodeManager = transcodeManager; } /// <summary> @@ -112,7 +112,7 @@ public class HlsSegmentController : BaseJellyfinApiController [FromQuery, Required] string deviceId, [FromQuery, Required] string playSessionId) { - _transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true); + _transcodeManager.KillTranscodingJobs(deviceId, playSessionId, _ => true); return NoContent(); } @@ -174,13 +174,13 @@ public class HlsSegmentController : BaseJellyfinApiController private ActionResult GetFileResult(string path, string playlistPath) { - var transcodingJob = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls); + var transcodingJob = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls); Response.OnCompleted(() => { if (transcodingJob is not null) { - _transcodingJobHelper.OnTranscodeEndRequest(transcodingJob); + _transcodeManager.OnTranscodeEndRequest(transcodingJob); } return Task.CompletedTask; diff --git a/Jellyfin.Api/Controllers/InstantMixController.cs b/Jellyfin.Api/Controllers/InstantMixController.cs index 4dc2a4253..e7ff1f986 100644 --- a/Jellyfin.Api/Controllers/InstantMixController.cs +++ b/Jellyfin.Api/Controllers/InstantMixController.cs @@ -5,6 +5,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; +using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -76,7 +77,7 @@ public class InstantMixController : BaseJellyfinApiController { var item = _libraryManager.GetItemById(id); userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -113,7 +114,7 @@ public class InstantMixController : BaseJellyfinApiController { var album = _libraryManager.GetItemById(id); userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -150,7 +151,7 @@ public class InstantMixController : BaseJellyfinApiController { var playlist = (Playlist)_libraryManager.GetItemById(id); userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -186,7 +187,7 @@ public class InstantMixController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ImageType[] enableImageTypes) { userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -223,7 +224,7 @@ public class InstantMixController : BaseJellyfinApiController { var item = _libraryManager.GetItemById(id); userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -260,7 +261,7 @@ public class InstantMixController : BaseJellyfinApiController { var item = _libraryManager.GetItemById(id); userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -334,7 +335,7 @@ public class InstantMixController : BaseJellyfinApiController { var item = _libraryManager.GetItemById(id); userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs index 4e5ed60d5..9800248c6 100644 --- a/Jellyfin.Api/Controllers/ItemUpdateController.cs +++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs @@ -171,7 +171,7 @@ public class ItemUpdateController : BaseJellyfinApiController info.ContentTypeOptions = GetContentTypeOptions(true).ToArray(); info.ContentType = configuredContentType; - if (inheritedContentType is null || inheritedContentType == CollectionType.TvShows) + if (inheritedContentType is null || inheritedContentType == CollectionType.tvshows) { info.ContentTypeOptions = info.ContentTypeOptions .Where(i => string.IsNullOrWhiteSpace(i.Value) diff --git a/Jellyfin.Api/Controllers/ItemsController.cs b/Jellyfin.Api/Controllers/ItemsController.cs index 4e46e808a..d10fba920 100644 --- a/Jellyfin.Api/Controllers/ItemsController.cs +++ b/Jellyfin.Api/Controllers/ItemsController.cs @@ -5,6 +5,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -34,6 +35,7 @@ public class ItemsController : BaseJellyfinApiController private readonly IDtoService _dtoService; private readonly ILogger<ItemsController> _logger; private readonly ISessionManager _sessionManager; + private readonly IUserDataManager _userDataRepository; /// <summary> /// Initializes a new instance of the <see cref="ItemsController"/> class. @@ -44,13 +46,15 @@ public class ItemsController : BaseJellyfinApiController /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param> /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param> + /// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param> public ItemsController( IUserManager userManager, ILibraryManager libraryManager, ILocalizationManager localization, IDtoService dtoService, ILogger<ItemsController> logger, - ISessionManager sessionManager) + ISessionManager sessionManager, + IUserDataManager userDataRepository) { _userManager = userManager; _libraryManager = libraryManager; @@ -58,6 +62,7 @@ public class ItemsController : BaseJellyfinApiController _dtoService = dtoService; _logger = logger; _sessionManager = sessionManager; + _userDataRepository = userDataRepository; } /// <summary> @@ -241,7 +246,7 @@ public class ItemsController : BaseJellyfinApiController var isApiKey = User.GetIsApiKey(); // if api key is used (auth.IsApiKey == true), then `user` will be null throughout this method userId = RequestHelpers.GetUserId(User, userId); - var user = !isApiKey && !userId.Value.Equals(default) + var user = !isApiKey && !userId.IsNullOrEmpty() ? _userManager.GetUserById(userId.Value) ?? throw new ResourceNotFoundException() : null; @@ -275,7 +280,7 @@ public class ItemsController : BaseJellyfinApiController collectionType = hasCollectionType.CollectionType; } - if (collectionType == CollectionType.Playlists) + if (collectionType == CollectionType.playlists) { recursive = true; includeItemTypes = new[] { BaseItemKind.Playlist }; @@ -836,7 +841,7 @@ public class ItemsController : BaseJellyfinApiController var ancestorIds = Array.Empty<Guid>(); var excludeFolderIds = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes); - if (parentIdGuid.Equals(default) && excludeFolderIds.Length > 0) + if (parentIdGuid.IsEmpty() && excludeFolderIds.Length > 0) { ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true) .Where(i => i is Folder) @@ -881,4 +886,64 @@ public class ItemsController : BaseJellyfinApiController itemsResult.TotalRecordCount, returnItems); } + + /// <summary> + /// Get Item User Data. + /// </summary> + /// <param name="userId">The user id.</param> + /// <param name="itemId">The item id.</param> + /// <response code="200">return item user data.</response> + /// <response code="404">Item is not found.</response> + /// <returns>Return <see cref="UserItemDataDto"/>.</returns> + [HttpGet("Users/{userId}/Items/{itemId}/UserData")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult<UserItemDataDto> GetItemUserData( + [FromRoute, Required] Guid userId, + [FromRoute, Required] Guid itemId) + { + if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) + { + return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to view this item user data."); + } + + var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException(); + var item = _libraryManager.GetItemById(itemId); + + return (item == null) ? NotFound() : _userDataRepository.GetUserDataDto(item, user); + } + + /// <summary> + /// Update Item User Data. + /// </summary> + /// <param name="userId">The user id.</param> + /// <param name="itemId">The item id.</param> + /// <param name="userDataDto">New user data object.</param> + /// <response code="200">return updated user item data.</response> + /// <response code="404">Item is not found.</response> + /// <returns>Return <see cref="UserItemDataDto"/>.</returns> + [HttpPost("Users/{userId}/Items/{itemId}/UserData")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult<UserItemDataDto> UpdateItemUserData( + [FromRoute, Required] Guid userId, + [FromRoute, Required] Guid itemId, + [FromBody, Required] UpdateUserItemDataDto userDataDto) + { + if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true)) + { + return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update this item user data."); + } + + var user = _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException(); + var item = _libraryManager.GetItemById(itemId); + if (item == null) + { + return NotFound(); + } + + _userDataRepository.SaveUserData(user, item, userDataDto, UserDataSaveReason.UpdateUserData); + + return _userDataRepository.GetUserDataDto(item, user); + } } diff --git a/Jellyfin.Api/Controllers/LibraryController.cs b/Jellyfin.Api/Controllers/LibraryController.cs index af9a93719..a0bbc961f 100644 --- a/Jellyfin.Api/Controllers/LibraryController.cs +++ b/Jellyfin.Api/Controllers/LibraryController.cs @@ -146,12 +146,12 @@ public class LibraryController : BaseJellyfinApiController [FromQuery] bool inheritFromParent = false) { userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); - var item = itemId.Equals(default) - ? (userId.Value.Equals(default) + var item = itemId.IsEmpty() + ? (userId.IsNullOrEmpty() ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); @@ -213,12 +213,12 @@ public class LibraryController : BaseJellyfinApiController [FromQuery] bool inheritFromParent = false) { userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); - var item = itemId.Equals(default) - ? (userId.Value.Equals(default) + var item = itemId.IsEmpty() + ? (userId.IsNullOrEmpty() ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); @@ -339,7 +339,7 @@ public class LibraryController : BaseJellyfinApiController { var isApiKey = User.GetIsApiKey(); var userId = User.GetUserId(); - var user = !isApiKey && !userId.Equals(default) + var user = !isApiKey && !userId.IsEmpty() ? _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException() : null; if (!isApiKey && user is null) @@ -382,7 +382,7 @@ public class LibraryController : BaseJellyfinApiController { var isApiKey = User.GetIsApiKey(); var userId = User.GetUserId(); - var user = !isApiKey && !userId.Equals(default) + var user = !isApiKey && !userId.IsEmpty() ? _userManager.GetUserById(userId) ?? throw new ResourceNotFoundException() : null; @@ -428,7 +428,7 @@ public class LibraryController : BaseJellyfinApiController [FromQuery] bool? isFavorite) { userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); @@ -471,7 +471,7 @@ public class LibraryController : BaseJellyfinApiController var baseItemDtos = new List<BaseItemDto>(); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); @@ -702,8 +702,8 @@ public class LibraryController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] ItemFields[] fields) { userId = RequestHelpers.GetUserId(User, userId); - var item = itemId.Equals(default) - ? (userId.Value.Equals(default) + var item = itemId.IsEmpty() + ? (userId.IsNullOrEmpty() ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); @@ -718,7 +718,7 @@ public class LibraryController : BaseJellyfinApiController return new QueryResult<BaseItemDto>(); } - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } @@ -927,15 +927,15 @@ public class LibraryController : BaseJellyfinApiController { return contentType switch { - CollectionType.BoxSets => new[] { "BoxSet" }, - CollectionType.Playlists => new[] { "Playlist" }, - CollectionType.Movies => new[] { "Movie" }, - CollectionType.TvShows => new[] { "Series", "Season", "Episode" }, - CollectionType.Books => new[] { "Book" }, - CollectionType.Music => new[] { "MusicArtist", "MusicAlbum", "Audio", "MusicVideo" }, - CollectionType.HomeVideos => new[] { "Video", "Photo" }, - CollectionType.Photos => new[] { "Video", "Photo" }, - CollectionType.MusicVideos => new[] { "MusicVideo" }, + CollectionType.boxsets => new[] { "BoxSet" }, + CollectionType.playlists => new[] { "Playlist" }, + CollectionType.movies => new[] { "Movie" }, + CollectionType.tvshows => new[] { "Series", "Season", "Episode" }, + CollectionType.books => new[] { "Book" }, + CollectionType.music => new[] { "MusicArtist", "MusicAlbum", "Audio", "MusicVideo" }, + CollectionType.homevideos => new[] { "Video", "Photo" }, + CollectionType.photos => new[] { "Video", "Photo" }, + CollectionType.musicvideos => new[] { "MusicVideo" }, _ => new[] { "Series", "Season", "Episode", "Movie" } }; } diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 425086895..1b2f5750f 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -10,12 +10,12 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Attributes; -using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.LiveTvDtos; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Common.Api; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; @@ -24,6 +24,8 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Streaming; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; @@ -41,43 +43,47 @@ namespace Jellyfin.Api.Controllers; public class LiveTvController : BaseJellyfinApiController { private readonly ILiveTvManager _liveTvManager; + private readonly ITunerHostManager _tunerHostManager; private readonly IUserManager _userManager; private readonly IHttpClientFactory _httpClientFactory; private readonly ILibraryManager _libraryManager; private readonly IDtoService _dtoService; private readonly IMediaSourceManager _mediaSourceManager; private readonly IConfigurationManager _configurationManager; - private readonly TranscodingJobHelper _transcodingJobHelper; + private readonly ITranscodeManager _transcodeManager; /// <summary> /// Initializes a new instance of the <see cref="LiveTvController"/> class. /// </summary> /// <param name="liveTvManager">Instance of the <see cref="ILiveTvManager"/> interface.</param> + /// <param name="tunerHostManager">Instance of the <see cref="ITunerHostManager"/> interface.</param> /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param> - /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param> + /// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param> public LiveTvController( ILiveTvManager liveTvManager, + ITunerHostManager tunerHostManager, IUserManager userManager, IHttpClientFactory httpClientFactory, ILibraryManager libraryManager, IDtoService dtoService, IMediaSourceManager mediaSourceManager, IConfigurationManager configurationManager, - TranscodingJobHelper transcodingJobHelper) + ITranscodeManager transcodeManager) { _liveTvManager = liveTvManager; + _tunerHostManager = tunerHostManager; _userManager = userManager; _httpClientFactory = httpClientFactory; _libraryManager = libraryManager; _dtoService = dtoService; _mediaSourceManager = mediaSourceManager; _configurationManager = configurationManager; - _transcodingJobHelper = transcodingJobHelper; + _transcodeManager = transcodeManager; } /// <summary> @@ -177,7 +183,7 @@ public class LiveTvController : BaseJellyfinApiController dtoOptions, CancellationToken.None); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); @@ -209,10 +215,10 @@ public class LiveTvController : BaseJellyfinApiController public ActionResult<BaseItemDto> GetChannel([FromRoute, Required] Guid channelId, [FromQuery] Guid? userId) { userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); - var item = channelId.Equals(default) + var item = channelId.IsEmpty() ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(channelId); @@ -382,7 +388,7 @@ public class LiveTvController : BaseJellyfinApiController public async Task<ActionResult<QueryResult<BaseItemDto>>> GetRecordingFolders([FromQuery] Guid? userId) { userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); var folders = await _liveTvManager.GetRecordingFoldersAsync(user).ConfigureAwait(false); @@ -405,10 +411,10 @@ public class LiveTvController : BaseJellyfinApiController public ActionResult<BaseItemDto> GetRecording([FromRoute, Required] Guid recordingId, [FromQuery] Guid? userId) { userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); - var item = recordingId.Equals(default) ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId); + var item = recordingId.IsEmpty() ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(recordingId); var dtoOptions = new DtoOptions() .AddClientFields(User); @@ -562,7 +568,7 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery] bool enableTotalRecordCount = true) { userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); @@ -589,7 +595,7 @@ public class LiveTvController : BaseJellyfinApiController GenreIds = genreIds }; - if (librarySeriesId.HasValue && !librarySeriesId.Equals(default)) + if (!librarySeriesId.IsNullOrEmpty()) { query.IsSeries = true; @@ -618,7 +624,7 @@ public class LiveTvController : BaseJellyfinApiController [Authorize(Policy = Policies.LiveTvAccess)] public async Task<ActionResult<QueryResult<BaseItemDto>>> GetPrograms([FromBody] GetProgramsDto body) { - var user = body.UserId.Equals(default) ? null : _userManager.GetUserById(body.UserId); + var user = body.UserId.IsEmpty() ? null : _userManager.GetUserById(body.UserId); var query = new InternalItemsQuery(user) { @@ -643,7 +649,7 @@ public class LiveTvController : BaseJellyfinApiController GenreIds = body.GenreIds }; - if (!body.LibrarySeriesId.Equals(default)) + if (!body.LibrarySeriesId.IsEmpty()) { query.IsSeries = true; @@ -702,7 +708,7 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery] bool enableTotalRecordCount = true) { userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); @@ -741,7 +747,7 @@ public class LiveTvController : BaseJellyfinApiController [FromQuery] Guid? userId) { userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); @@ -949,9 +955,7 @@ public class LiveTvController : BaseJellyfinApiController [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status200OK)] public async Task<ActionResult<TunerHostInfo>> AddTunerHost([FromBody] TunerHostInfo tunerHostInfo) - { - return await _liveTvManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false); - } + => await _tunerHostManager.SaveTunerHost(tunerHostInfo).ConfigureAwait(false); /// <summary> /// Deletes a tuner host. @@ -1128,10 +1132,8 @@ public class LiveTvController : BaseJellyfinApiController [HttpGet("TunerHosts/Types")] [Authorize(Policy = Policies.LiveTvAccess)] [ProducesResponseType(StatusCodes.Status200OK)] - public ActionResult<IEnumerable<NameIdPair>> GetTunerHostTypes() - { - return _liveTvManager.GetTunerHostTypes(); - } + public IEnumerable<NameIdPair> GetTunerHostTypes() + => _tunerHostManager.GetTunerHostTypes(); /// <summary> /// Discover tuners. @@ -1143,10 +1145,8 @@ public class LiveTvController : BaseJellyfinApiController [HttpGet("Tuners/Discover")] [Authorize(Policy = Policies.LiveTvManagement)] [ProducesResponseType(StatusCodes.Status200OK)] - public async Task<ActionResult<IEnumerable<TunerHostInfo>>> DiscoverTuners([FromQuery] bool newDevicesOnly = false) - { - return await _liveTvManager.DiscoverTuners(newDevicesOnly, CancellationToken.None).ConfigureAwait(false); - } + public IAsyncEnumerable<TunerHostInfo> DiscoverTuners([FromQuery] bool newDevicesOnly = false) + => _tunerHostManager.DiscoverTuners(newDevicesOnly); /// <summary> /// Gets a live tv recording stream. @@ -1171,7 +1171,7 @@ public class LiveTvController : BaseJellyfinApiController return NotFound(); } - var stream = new ProgressiveFileStream(path, null, _transcodingJobHelper); + var stream = new ProgressiveFileStream(path, null, _transcodeManager); return new FileStreamResult(stream, MimeTypes.GetMimeType(path)); } diff --git a/Jellyfin.Api/Controllers/MoviesController.cs b/Jellyfin.Api/Controllers/MoviesController.cs index e1145481f..471bcd096 100644 --- a/Jellyfin.Api/Controllers/MoviesController.cs +++ b/Jellyfin.Api/Controllers/MoviesController.cs @@ -7,6 +7,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; @@ -69,7 +70,7 @@ public class MoviesController : BaseJellyfinApiController [FromQuery] int itemLimit = 8) { userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); var dtoOptions = new DtoOptions { Fields = fields } diff --git a/Jellyfin.Api/Controllers/MusicGenresController.cs b/Jellyfin.Api/Controllers/MusicGenresController.cs index 69b904264..5411baa3e 100644 --- a/Jellyfin.Api/Controllers/MusicGenresController.cs +++ b/Jellyfin.Api/Controllers/MusicGenresController.cs @@ -6,6 +6,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -95,7 +96,7 @@ public class MusicGenresController : BaseJellyfinApiController .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, false, imageTypeLimit, enableImageTypes); - User? user = userId.Value.Equals(default) + User? user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); @@ -164,7 +165,7 @@ public class MusicGenresController : BaseJellyfinApiController return NotFound(); } - if (!userId.Value.Equals(default)) + if (!userId.IsNullOrEmpty()) { var user = _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/PersonsController.cs b/Jellyfin.Api/Controllers/PersonsController.cs index b4c6f490a..6ca308601 100644 --- a/Jellyfin.Api/Controllers/PersonsController.cs +++ b/Jellyfin.Api/Controllers/PersonsController.cs @@ -5,6 +5,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; +using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -83,7 +84,7 @@ public class PersonsController : BaseJellyfinApiController .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - User? user = userId.Value.Equals(default) + User? user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); @@ -129,7 +130,7 @@ public class PersonsController : BaseJellyfinApiController return NotFound(); } - if (!userId.Value.Equals(default)) + if (!userId.IsNullOrEmpty()) { var user = _userManager.GetUserById(userId.Value); return _dtoService.GetBaseItemDto(item, dtoOptions, user); diff --git a/Jellyfin.Api/Controllers/PlaylistsController.cs b/Jellyfin.Api/Controllers/PlaylistsController.cs index c4c89ccde..921cc6031 100644 --- a/Jellyfin.Api/Controllers/PlaylistsController.cs +++ b/Jellyfin.Api/Controllers/PlaylistsController.cs @@ -9,6 +9,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.PlaylistDtos; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; @@ -188,7 +189,7 @@ public class PlaylistsController : BaseJellyfinApiController return NotFound(); } - var user = userId.Equals(default) + var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId); diff --git a/Jellyfin.Api/Controllers/PlaystateController.cs b/Jellyfin.Api/Controllers/PlaystateController.cs index 8ad553bcb..bde2f4d1a 100644 --- a/Jellyfin.Api/Controllers/PlaystateController.cs +++ b/Jellyfin.Api/Controllers/PlaystateController.cs @@ -8,6 +8,7 @@ using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Session; @@ -30,7 +31,7 @@ public class PlaystateController : BaseJellyfinApiController private readonly ILibraryManager _libraryManager; private readonly ISessionManager _sessionManager; private readonly ILogger<PlaystateController> _logger; - private readonly TranscodingJobHelper _transcodingJobHelper; + private readonly ITranscodeManager _transcodeManager; /// <summary> /// Initializes a new instance of the <see cref="PlaystateController"/> class. @@ -40,14 +41,14 @@ public class PlaystateController : BaseJellyfinApiController /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param> /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param> - /// <param name="transcodingJobHelper">Th <see cref="TranscodingJobHelper"/> singleton.</param> + /// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param> public PlaystateController( IUserManager userManager, IUserDataManager userDataRepository, ILibraryManager libraryManager, ISessionManager sessionManager, ILoggerFactory loggerFactory, - TranscodingJobHelper transcodingJobHelper) + ITranscodeManager transcodeManager) { _userManager = userManager; _userDataRepository = userDataRepository; @@ -55,7 +56,7 @@ public class PlaystateController : BaseJellyfinApiController _sessionManager = sessionManager; _logger = loggerFactory.CreateLogger<PlaystateController>(); - _transcodingJobHelper = transcodingJobHelper; + _transcodeManager = transcodeManager; } /// <summary> @@ -188,7 +189,7 @@ public class PlaystateController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status204NoContent)] public ActionResult PingPlaybackSession([FromQuery, Required] string playSessionId) { - _transcodingJobHelper.PingTranscodingJob(playSessionId, null); + _transcodeManager.PingTranscodingJob(playSessionId, null); return NoContent(); } @@ -205,7 +206,7 @@ public class PlaystateController : BaseJellyfinApiController _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty); if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId)) { - await _transcodingJobHelper.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); + await _transcodeManager.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); } playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); @@ -354,7 +355,7 @@ public class PlaystateController : BaseJellyfinApiController _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty); if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId)) { - await _transcodingJobHelper.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); + await _transcodeManager.KillTranscodingJobs(User.GetDeviceId()!, playbackStopInfo.PlaySessionId, s => true).ConfigureAwait(false); } playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false); @@ -388,7 +389,7 @@ public class PlaystateController : BaseJellyfinApiController { if (method == PlayMethod.Transcode) { - var job = string.IsNullOrWhiteSpace(playSessionId) ? null : _transcodingJobHelper.GetTranscodingJob(playSessionId); + var job = string.IsNullOrWhiteSpace(playSessionId) ? null : _transcodeManager.GetTranscodingJob(playSessionId); if (job is null) { return PlayMethod.DirectPlay; diff --git a/Jellyfin.Api/Controllers/SearchController.cs b/Jellyfin.Api/Controllers/SearchController.cs index 5b4594165..413b7b834 100644 --- a/Jellyfin.Api/Controllers/SearchController.cs +++ b/Jellyfin.Api/Controllers/SearchController.cs @@ -209,7 +209,7 @@ public class SearchController : BaseJellyfinApiController break; } - if (!item.ChannelId.Equals(default)) + if (!item.ChannelId.IsEmpty()) { var channel = _libraryManager.GetItemById(item.ChannelId); result.ChannelName = channel?.Name; diff --git a/Jellyfin.Api/Controllers/SessionController.cs b/Jellyfin.Api/Controllers/SessionController.cs index fdebb3d45..52b58b8f1 100644 --- a/Jellyfin.Api/Controllers/SessionController.cs +++ b/Jellyfin.Api/Controllers/SessionController.cs @@ -10,6 +10,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Api.Models.SessionDtos; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Common.Api; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Library; @@ -71,7 +72,7 @@ public class SessionController : BaseJellyfinApiController result = result.Where(i => string.Equals(i.DeviceId, deviceId, StringComparison.OrdinalIgnoreCase)); } - if (controllableByUserId.HasValue && !controllableByUserId.Equals(default)) + if (!controllableByUserId.IsNullOrEmpty()) { result = result.Where(i => i.SupportsRemoteControl); @@ -83,12 +84,12 @@ public class SessionController : BaseJellyfinApiController if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers)) { - result = result.Where(i => i.UserId.Equals(default) || i.ContainsUser(controllableByUserId.Value)); + result = result.Where(i => i.UserId.IsEmpty() || i.ContainsUser(controllableByUserId.Value)); } if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl)) { - result = result.Where(i => !i.UserId.Equals(default)); + result = result.Where(i => !i.UserId.IsEmpty()); } result = result.Where(i => @@ -385,7 +386,6 @@ public class SessionController : BaseJellyfinApiController /// <param name="playableMediaTypes">A list of playable media types, comma delimited. Audio, Video, Book, Photo.</param> /// <param name="supportedCommands">A list of supported remote control commands, comma delimited.</param> /// <param name="supportsMediaControl">Determines whether media can be played remotely..</param> - /// <param name="supportsSync">Determines whether sync is supported.</param> /// <param name="supportsPersistentIdentifier">Determines whether the device supports a unique identifier.</param> /// <response code="204">Capabilities posted.</response> /// <returns>A <see cref="NoContentResult"/>.</returns> @@ -397,7 +397,6 @@ public class SessionController : BaseJellyfinApiController [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] playableMediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands, [FromQuery] bool supportsMediaControl = false, - [FromQuery] bool supportsSync = false, [FromQuery] bool supportsPersistentIdentifier = true) { if (string.IsNullOrWhiteSpace(id)) @@ -410,7 +409,6 @@ public class SessionController : BaseJellyfinApiController PlayableMediaTypes = playableMediaTypes, SupportedCommands = supportedCommands, SupportsMediaControl = supportsMediaControl, - SupportsSync = supportsSync, SupportsPersistentIdentifier = supportsPersistentIdentifier }); return NoContent(); diff --git a/Jellyfin.Api/Controllers/StudiosController.cs b/Jellyfin.Api/Controllers/StudiosController.cs index f434f60f5..708fc7436 100644 --- a/Jellyfin.Api/Controllers/StudiosController.cs +++ b/Jellyfin.Api/Controllers/StudiosController.cs @@ -5,6 +5,7 @@ using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -91,7 +92,7 @@ public class StudiosController : BaseJellyfinApiController .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - User? user = userId.Value.Equals(default) + User? user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); @@ -144,7 +145,7 @@ public class StudiosController : BaseJellyfinApiController var dtoOptions = new DtoOptions().AddClientFields(User); var item = _libraryManager.GetStudio(name); - if (!userId.Equals(default)) + if (!userId.IsNullOrEmpty()) { var user = _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/SuggestionsController.cs b/Jellyfin.Api/Controllers/SuggestionsController.cs index 675757fc5..2aa6d25a7 100644 --- a/Jellyfin.Api/Controllers/SuggestionsController.cs +++ b/Jellyfin.Api/Controllers/SuggestionsController.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; @@ -62,7 +63,7 @@ public class SuggestionsController : BaseJellyfinApiController [FromQuery] int? limit, [FromQuery] bool enableTotalRecordCount = false) { - var user = userId.Equals(default) + var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId); diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index 55a30d469..3d84b61bf 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -111,7 +111,7 @@ public class TvShowsController : BaseJellyfinApiController }, options); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); @@ -150,7 +150,7 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery] bool? enableUserData) { userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); @@ -222,7 +222,7 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery] ItemSortBy? sortBy) { userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); @@ -284,7 +284,7 @@ public class TvShowsController : BaseJellyfinApiController } // This must be the last filter - if (adjacentTo.HasValue && !adjacentTo.Value.Equals(default)) + if (!adjacentTo.IsNullOrEmpty()) { episodes = UserViewBuilder.FilterForAdjacency(episodes, adjacentTo.Value).ToList(); } @@ -339,7 +339,7 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery] bool? enableUserData) { userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); diff --git a/Jellyfin.Api/Controllers/UniversalAudioController.cs b/Jellyfin.Api/Controllers/UniversalAudioController.cs index 7177a0440..0a416aedb 100644 --- a/Jellyfin.Api/Controllers/UniversalAudioController.cs +++ b/Jellyfin.Api/Controllers/UniversalAudioController.cs @@ -11,6 +11,7 @@ using Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Streaming; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.MediaInfo; using Microsoft.AspNetCore.Authorization; diff --git a/Jellyfin.Api/Controllers/UserController.cs b/Jellyfin.Api/Controllers/UserController.cs index f9f27f148..ea10ee24f 100644 --- a/Jellyfin.Api/Controllers/UserController.cs +++ b/Jellyfin.Api/Controllers/UserController.cs @@ -8,6 +8,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.Models.UserDtos; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Common.Api; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; @@ -532,7 +533,7 @@ public class UserController : BaseJellyfinApiController public ActionResult<UserDto> GetCurrentUser() { var userId = User.GetUserId(); - if (userId.Equals(default)) + if (userId.IsEmpty()) { return BadRequest(); } diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs index 2c4fe9186..264e0a3db 100644 --- a/Jellyfin.Api/Controllers/UserLibraryController.cs +++ b/Jellyfin.Api/Controllers/UserLibraryController.cs @@ -8,6 +8,7 @@ using Jellyfin.Api.Extensions; using Jellyfin.Api.ModelBinders; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -84,7 +85,7 @@ public class UserLibraryController : BaseJellyfinApiController return NotFound(); } - var item = itemId.Equals(default) + var item = itemId.IsEmpty() ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); @@ -145,7 +146,7 @@ public class UserLibraryController : BaseJellyfinApiController return NotFound(); } - var item = itemId.Equals(default) + var item = itemId.IsEmpty() ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); @@ -185,7 +186,7 @@ public class UserLibraryController : BaseJellyfinApiController return NotFound(); } - var item = itemId.Equals(default) + var item = itemId.IsEmpty() ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); @@ -221,7 +222,7 @@ public class UserLibraryController : BaseJellyfinApiController return NotFound(); } - var item = itemId.Equals(default) + var item = itemId.IsEmpty() ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); @@ -257,7 +258,7 @@ public class UserLibraryController : BaseJellyfinApiController return NotFound(); } - var item = itemId.Equals(default) + var item = itemId.IsEmpty() ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); @@ -294,7 +295,7 @@ public class UserLibraryController : BaseJellyfinApiController return NotFound(); } - var item = itemId.Equals(default) + var item = itemId.IsEmpty() ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); @@ -330,7 +331,7 @@ public class UserLibraryController : BaseJellyfinApiController return NotFound(); } - var item = itemId.Equals(default) + var item = itemId.IsEmpty() ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); @@ -375,7 +376,7 @@ public class UserLibraryController : BaseJellyfinApiController return NotFound(); } - var item = itemId.Equals(default) + var item = itemId.IsEmpty() ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); @@ -558,7 +559,7 @@ public class UserLibraryController : BaseJellyfinApiController return NotFound(); } - var item = itemId.Equals(default) + var item = itemId.IsEmpty() ? _libraryManager.GetUserRootFolder() : _libraryManager.GetItemById(itemId); diff --git a/Jellyfin.Api/Controllers/UserViewsController.cs b/Jellyfin.Api/Controllers/UserViewsController.cs index 0ffa3ab1a..035d04474 100644 --- a/Jellyfin.Api/Controllers/UserViewsController.cs +++ b/Jellyfin.Api/Controllers/UserViewsController.cs @@ -90,7 +90,6 @@ public class UserViewsController : BaseJellyfinApiController fields.Add(ItemFields.PrimaryImageAspectRatio); fields.Add(ItemFields.DisplayPreferencesId); - fields.Remove(ItemFields.BasicSyncInfo); dtoOptions.Fields = fields.ToArray(); var user = _userManager.GetUserById(userId); diff --git a/Jellyfin.Api/Controllers/VideosController.cs b/Jellyfin.Api/Controllers/VideosController.cs index 5d9868eb9..e6c319869 100644 --- a/Jellyfin.Api/Controllers/VideosController.cs +++ b/Jellyfin.Api/Controllers/VideosController.cs @@ -11,7 +11,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; -using Jellyfin.Api.Models.StreamingDtos; +using Jellyfin.Extensions; using MediaBrowser.Common.Api; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; @@ -20,6 +20,7 @@ using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Streaming; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -43,7 +44,7 @@ public class VideosController : BaseJellyfinApiController private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; - private readonly TranscodingJobHelper _transcodingJobHelper; + private readonly ITranscodeManager _transcodeManager; private readonly IHttpClientFactory _httpClientFactory; private readonly EncodingHelper _encodingHelper; @@ -58,7 +59,7 @@ public class VideosController : BaseJellyfinApiController /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> - /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param> + /// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param> /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param> /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> public VideosController( @@ -68,7 +69,7 @@ public class VideosController : BaseJellyfinApiController IMediaSourceManager mediaSourceManager, IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, - TranscodingJobHelper transcodingJobHelper, + ITranscodeManager transcodeManager, IHttpClientFactory httpClientFactory, EncodingHelper encodingHelper) { @@ -78,7 +79,7 @@ public class VideosController : BaseJellyfinApiController _mediaSourceManager = mediaSourceManager; _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; - _transcodingJobHelper = transcodingJobHelper; + _transcodeManager = transcodeManager; _httpClientFactory = httpClientFactory; _encodingHelper = encodingHelper; } @@ -96,12 +97,12 @@ public class VideosController : BaseJellyfinApiController public ActionResult<QueryResult<BaseItemDto>> GetAdditionalPart([FromRoute, Required] Guid itemId, [FromQuery] Guid? userId) { userId = RequestHelpers.GetUserId(User, userId); - var user = userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); - var item = itemId.Equals(default) - ? (userId.Value.Equals(default) + var item = itemId.IsEmpty() + ? (userId.IsNullOrEmpty() ? _libraryManager.RootFolder : _libraryManager.GetUserRootFolder()) : _libraryManager.GetItemById(itemId); @@ -427,7 +428,7 @@ public class VideosController : BaseJellyfinApiController _serverConfigurationManager, _mediaEncoder, _encodingHelper, - _transcodingJobHelper, + _transcodeManager, _transcodingJobType, cancellationTokenSource.Token) .ConfigureAwait(false); @@ -466,7 +467,7 @@ public class VideosController : BaseJellyfinApiController if (state.MediaSource.IsInfiniteStream) { - var liveStream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper); + var liveStream = new ProgressiveFileStream(state.MediaPath, null, _transcodeManager); return File(liveStream, contentType); } @@ -482,7 +483,7 @@ public class VideosController : BaseJellyfinApiController state, isHeadRequest, HttpContext, - _transcodingJobHelper, + _transcodeManager, ffmpegCommandLineArguments, _transcodingJobType, cancellationTokenSource).ConfigureAwait(false); diff --git a/Jellyfin.Api/Controllers/YearsController.cs b/Jellyfin.Api/Controllers/YearsController.cs index ca46c38c5..e4aa0ea42 100644 --- a/Jellyfin.Api/Controllers/YearsController.cs +++ b/Jellyfin.Api/Controllers/YearsController.cs @@ -90,7 +90,7 @@ public class YearsController : BaseJellyfinApiController .AddClientFields(User) .AddAdditionalDtoOptions(enableImages, enableUserData, imageTypeLimit, enableImageTypes); - User? user = userId.Value.Equals(default) + User? user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); BaseItem parentItem = _libraryManager.GetParentItem(parentId, userId); @@ -110,7 +110,7 @@ public class YearsController : BaseJellyfinApiController { var folder = (Folder)parentItem; - if (userId.Equals(default)) + if (userId.IsNullOrEmpty()) { items = recursive ? folder.GetRecursiveChildren(Filter) : folder.Children.Where(Filter).ToList(); } @@ -182,7 +182,7 @@ public class YearsController : BaseJellyfinApiController var dtoOptions = new DtoOptions() .AddClientFields(User); - if (!userId.Value.Equals(default)) + if (!userId.IsNullOrEmpty()) { var user = _userManager.GetUserById(userId.Value); return _dtoService.GetBaseItemDto(item, dtoOptions, user); diff --git a/Jellyfin.Api/Helpers/AudioHelper.cs b/Jellyfin.Api/Helpers/AudioHelper.cs index 926ce99dd..c80a9d582 100644 --- a/Jellyfin.Api/Helpers/AudioHelper.cs +++ b/Jellyfin.Api/Helpers/AudioHelper.cs @@ -2,13 +2,13 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Streaming; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Net; using Microsoft.AspNetCore.Http; @@ -26,7 +26,7 @@ public class AudioHelper private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; - private readonly TranscodingJobHelper _transcodingJobHelper; + private readonly ITranscodeManager _transcodeManager; private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpContextAccessor _httpContextAccessor; private readonly EncodingHelper _encodingHelper; @@ -39,7 +39,7 @@ public class AudioHelper /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> - /// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param> + /// <param name="transcodeManager">Instance of <see cref="ITranscodeManager"/> interface.</param> /// <param name="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param> /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> @@ -49,7 +49,7 @@ public class AudioHelper IMediaSourceManager mediaSourceManager, IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, - TranscodingJobHelper transcodingJobHelper, + ITranscodeManager transcodeManager, IHttpClientFactory httpClientFactory, IHttpContextAccessor httpContextAccessor, EncodingHelper encodingHelper) @@ -59,7 +59,7 @@ public class AudioHelper _mediaSourceManager = mediaSourceManager; _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; - _transcodingJobHelper = transcodingJobHelper; + _transcodeManager = transcodeManager; _httpClientFactory = httpClientFactory; _httpContextAccessor = httpContextAccessor; _encodingHelper = encodingHelper; @@ -94,7 +94,7 @@ public class AudioHelper _serverConfigurationManager, _mediaEncoder, _encodingHelper, - _transcodingJobHelper, + _transcodeManager, transcodingJobType, cancellationTokenSource.Token) .ConfigureAwait(false); @@ -133,7 +133,7 @@ public class AudioHelper if (state.MediaSource.IsInfiniteStream) { - var stream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper); + var stream = new ProgressiveFileStream(state.MediaPath, null, _transcodeManager); return new FileStreamResult(stream, contentType); } @@ -149,7 +149,7 @@ public class AudioHelper state, isHeadRequest, _httpContextAccessor.HttpContext, - _transcodingJobHelper, + _transcodeManager, ffmpegCommandLineArguments, transcodingJobType, cancellationTokenSource).ConfigureAwait(false); diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs index 05f7d44bf..fa81fc284 100644 --- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs +++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs @@ -8,7 +8,6 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Extensions; -using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Extensions; @@ -18,6 +17,7 @@ using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Streaming; using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; @@ -39,7 +39,7 @@ public class DynamicHlsHelper private readonly IMediaSourceManager _mediaSourceManager; private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IMediaEncoder _mediaEncoder; - private readonly TranscodingJobHelper _transcodingJobHelper; + private readonly ITranscodeManager _transcodeManager; private readonly INetworkManager _networkManager; private readonly ILogger<DynamicHlsHelper> _logger; private readonly IHttpContextAccessor _httpContextAccessor; @@ -54,7 +54,7 @@ public class DynamicHlsHelper /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> - /// <param name="transcodingJobHelper">Instance of <see cref="TranscodingJobHelper"/>.</param> + /// <param name="transcodeManager">Instance of <see cref="ITranscodeManager"/>.</param> /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> interface.</param> /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> @@ -66,7 +66,7 @@ public class DynamicHlsHelper IMediaSourceManager mediaSourceManager, IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, - TranscodingJobHelper transcodingJobHelper, + ITranscodeManager transcodeManager, INetworkManager networkManager, ILogger<DynamicHlsHelper> logger, IHttpContextAccessor httpContextAccessor, @@ -78,7 +78,7 @@ public class DynamicHlsHelper _mediaSourceManager = mediaSourceManager; _serverConfigurationManager = serverConfigurationManager; _mediaEncoder = mediaEncoder; - _transcodingJobHelper = transcodingJobHelper; + _transcodeManager = transcodeManager; _networkManager = networkManager; _logger = logger; _httpContextAccessor = httpContextAccessor; @@ -130,7 +130,7 @@ public class DynamicHlsHelper _serverConfigurationManager, _mediaEncoder, _encodingHelper, - _transcodingJobHelper, + _transcodeManager, transcodingJobType, cancellationTokenSource.Token) .ConfigureAwait(false); diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs index 0f0a70c69..5385979d4 100644 --- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs +++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs @@ -4,9 +4,9 @@ using System.Net.Http; using System.Net.Mime; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Api.Models.PlaybackDtos; -using Jellyfin.Api.Models.StreamingDtos; +using Jellyfin.Api.Extensions; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Streaming; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Net.Http.Headers; @@ -65,7 +65,7 @@ public static class FileStreamResponseHelpers /// <param name="state">The current <see cref="StreamState"/>.</param> /// <param name="isHeadRequest">Whether the current request is a HTTP HEAD request so only the headers get returned.</param> /// <param name="httpContext">The current http context.</param> - /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper"/> singleton.</param> + /// <param name="transcodeManager">The <see cref="ITranscodeManager"/> singleton.</param> /// <param name="ffmpegCommandLineArguments">The command line arguments to start ffmpeg.</param> /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param> /// <param name="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param> @@ -74,7 +74,7 @@ public static class FileStreamResponseHelpers StreamState state, bool isHeadRequest, HttpContext httpContext, - TranscodingJobHelper transcodingJobHelper, + ITranscodeManager transcodeManager, string ffmpegCommandLineArguments, TranscodingJobType transcodingJobType, CancellationTokenSource cancellationTokenSource) @@ -93,22 +93,28 @@ public static class FileStreamResponseHelpers return new OkResult(); } - var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath); + var transcodingLock = transcodeManager.GetTranscodingLock(outputPath); await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); try { - TranscodingJobDto? job; + TranscodingJob? job; if (!File.Exists(outputPath)) { - job = await transcodingJobHelper.StartFfMpeg(state, outputPath, ffmpegCommandLineArguments, httpContext.Request, transcodingJobType, cancellationTokenSource).ConfigureAwait(false); + job = await transcodeManager.StartFfMpeg( + state, + outputPath, + ffmpegCommandLineArguments, + httpContext.User.GetUserId(), + transcodingJobType, + cancellationTokenSource).ConfigureAwait(false); } else { - job = transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive); + job = transcodeManager.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive); state.Dispose(); } - var stream = new ProgressiveFileStream(outputPath, job, transcodingJobHelper); + var stream = new ProgressiveFileStream(outputPath, job, transcodeManager); return new FileStreamResult(stream, contentType); } finally diff --git a/Jellyfin.Api/Helpers/HlsHelpers.cs b/Jellyfin.Api/Helpers/HlsHelpers.cs index e2d3bfb19..c8a36c562 100644 --- a/Jellyfin.Api/Helpers/HlsHelpers.cs +++ b/Jellyfin.Api/Helpers/HlsHelpers.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Streaming; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; diff --git a/Jellyfin.Api/Helpers/MediaInfoHelper.cs b/Jellyfin.Api/Helpers/MediaInfoHelper.cs index 321987ca7..6a24ad32a 100644 --- a/Jellyfin.Api/Helpers/MediaInfoHelper.cs +++ b/Jellyfin.Api/Helpers/MediaInfoHelper.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Jellyfin.Api.Extensions; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Configuration; @@ -86,7 +87,7 @@ public class MediaInfoHelper string? mediaSourceId = null, string? liveStreamId = null) { - var user = userId is null || userId.Value.Equals(default) + var user = userId.IsNullOrEmpty() ? null : _userManager.GetUserById(userId.Value); var item = _libraryManager.GetItemById(id); diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs index be3d4dfb6..429e97213 100644 --- a/Jellyfin.Api/Helpers/RequestHelpers.cs +++ b/Jellyfin.Api/Helpers/RequestHelpers.cs @@ -7,6 +7,7 @@ using Jellyfin.Api.Constants; using Jellyfin.Api.Extensions; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -67,7 +68,7 @@ public static class RequestHelpers var authenticatedUserId = claimsPrincipal.GetUserId(); // UserId not provided, fall back to authenticated user id. - if (userId is null || userId.Value.Equals(default)) + if (userId.IsNullOrEmpty()) { return authenticatedUserId; } diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 71c62b235..7a3842a9f 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Extensions; -using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; @@ -14,6 +13,7 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; +using MediaBrowser.Controller.Streaming; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -38,7 +38,7 @@ public static class StreamingHelpers /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> - /// <param name="transcodingJobHelper">Initialized <see cref="TranscodingJobHelper"/>.</param> + /// <param name="transcodeManager">Instance of the <see cref="ITranscodeManager"/> interface.</param> /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param> /// <returns>A <see cref="Task"/> containing the current <see cref="StreamState"/>.</returns> @@ -51,7 +51,7 @@ public static class StreamingHelpers IServerConfigurationManager serverConfigurationManager, IMediaEncoder mediaEncoder, EncodingHelper encodingHelper, - TranscodingJobHelper transcodingJobHelper, + ITranscodeManager transcodeManager, TranscodingJobType transcodingJobType, CancellationToken cancellationToken) { @@ -74,7 +74,7 @@ public static class StreamingHelpers streamingRequest.AudioCodec = encodingHelper.InferAudioCodec(url); } - var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper) + var state = new StreamState(mediaSourceManager, transcodingJobType, transcodeManager) { Request = streamingRequest, RequestedUrl = url, @@ -82,7 +82,7 @@ public static class StreamingHelpers }; var userId = httpContext.User.GetUserId(); - if (!userId.Equals(default)) + if (!userId.IsEmpty()) { state.User = userManager.GetUserById(userId); } @@ -115,7 +115,7 @@ public static class StreamingHelpers if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId)) { var currentJob = !string.IsNullOrWhiteSpace(streamingRequest.PlaySessionId) - ? transcodingJobHelper.GetTranscodingJob(streamingRequest.PlaySessionId) + ? transcodeManager.GetTranscodingJob(streamingRequest.PlaySessionId) : null; if (currentJob is not null) diff --git a/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs b/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs index 27bcd5570..842a69dd9 100644 --- a/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs +++ b/Jellyfin.Api/Middleware/IpBasedAccessValidationMiddleware.cs @@ -41,6 +41,8 @@ public class IPBasedAccessValidationMiddleware if (!networkManager.HasRemoteAccess(remoteIP)) { + // No access from network, respond with 503 instead of 200. + httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; return; } diff --git a/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs index d8c95ddff..35b0a1dd0 100644 --- a/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs +++ b/Jellyfin.Api/Middleware/LanFilteringMiddleware.cs @@ -1,3 +1,4 @@ +using System.Net; using System.Threading.Tasks; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; @@ -40,6 +41,8 @@ public class LanFilteringMiddleware var host = httpContext.GetNormalizedRemoteIP(); if (!networkManager.IsInLocalNetwork(host)) { + // No access from network, respond with 503 instead of 200. + httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable; return; } diff --git a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs index b021771a0..acd3f29e3 100644 --- a/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs +++ b/Jellyfin.Api/Models/SessionDtos/ClientCapabilitiesDto.cs @@ -31,26 +31,11 @@ public class ClientCapabilitiesDto public bool SupportsMediaControl { get; set; } /// <summary> - /// Gets or sets a value indicating whether session supports content uploading. - /// </summary> - public bool SupportsContentUploading { get; set; } - - /// <summary> - /// Gets or sets the message callback url. - /// </summary> - public string? MessageCallbackUrl { get; set; } - - /// <summary> /// Gets or sets a value indicating whether session supports a persistent identifier. /// </summary> public bool SupportsPersistentIdentifier { get; set; } /// <summary> - /// Gets or sets a value indicating whether session supports sync. - /// </summary> - public bool SupportsSync { get; set; } - - /// <summary> /// Gets or sets the device profile. /// </summary> public DeviceProfile? DeviceProfile { get; set; } @@ -76,10 +61,7 @@ public class ClientCapabilitiesDto PlayableMediaTypes = PlayableMediaTypes, SupportedCommands = SupportedCommands, SupportsMediaControl = SupportsMediaControl, - SupportsContentUploading = SupportsContentUploading, - MessageCallbackUrl = MessageCallbackUrl, SupportsPersistentIdentifier = SupportsPersistentIdentifier, - SupportsSync = SupportsSync, DeviceProfile = DeviceProfile, AppStoreUrl = AppStoreUrl, IconUrl = IconUrl diff --git a/Jellyfin.Api/Models/StreamingDtos/HlsAudioRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/HlsAudioRequestDto.cs index 4f1abb1ff..bd176bb6a 100644 --- a/Jellyfin.Api/Models/StreamingDtos/HlsAudioRequestDto.cs +++ b/Jellyfin.Api/Models/StreamingDtos/HlsAudioRequestDto.cs @@ -1,4 +1,6 @@ -namespace Jellyfin.Api.Models.StreamingDtos; +using MediaBrowser.Controller.Streaming; + +namespace Jellyfin.Api.Models.StreamingDtos; /// <summary> /// The hls video request dto. diff --git a/Jellyfin.Api/Models/StreamingDtos/HlsVideoRequestDto.cs b/Jellyfin.Api/Models/StreamingDtos/HlsVideoRequestDto.cs index 1cd3d0132..53b6d7575 100644 --- a/Jellyfin.Api/Models/StreamingDtos/HlsVideoRequestDto.cs +++ b/Jellyfin.Api/Models/StreamingDtos/HlsVideoRequestDto.cs @@ -1,4 +1,6 @@ -namespace Jellyfin.Api.Models.StreamingDtos; +using MediaBrowser.Controller.Streaming; + +namespace Jellyfin.Api.Models.StreamingDtos; /// <summary> /// The hls video request dto. diff --git a/Jellyfin.Data/Enums/AudioSpatialFormat.cs b/Jellyfin.Data/Enums/AudioSpatialFormat.cs new file mode 100644 index 000000000..5e3a12332 --- /dev/null +++ b/Jellyfin.Data/Enums/AudioSpatialFormat.cs @@ -0,0 +1,22 @@ +namespace Jellyfin.Data.Enums; + +/// <summary> +/// An enum representing formats of spatial audio. +/// </summary> +public enum AudioSpatialFormat +{ + /// <summary> + /// None audio spatial format. + /// </summary> + None, + + /// <summary> + /// Dolby Atmos audio spatial format. + /// </summary> + DolbyAtmos, + + /// <summary> + /// DTS:X audio spatial format. + /// </summary> + DTSX, +} diff --git a/Jellyfin.Data/Enums/CollectionType.cs b/Jellyfin.Data/Enums/CollectionType.cs index e2044a0bc..e3d3b07af 100644 --- a/Jellyfin.Data/Enums/CollectionType.cs +++ b/Jellyfin.Data/Enums/CollectionType.cs @@ -1,3 +1,4 @@ +#pragma warning disable SA1300 // The name of a C# element does not begin with an upper-case letter. - disabled due to legacy requirement. using Jellyfin.Data.Attributes; namespace Jellyfin.Data.Enums; @@ -10,155 +11,155 @@ public enum CollectionType /// <summary> /// Unknown collection. /// </summary> - Unknown = 0, + unknown = 0, /// <summary> /// Movies collection. /// </summary> - Movies = 1, + movies = 1, /// <summary> /// Tv shows collection. /// </summary> - TvShows = 2, + tvshows = 2, /// <summary> /// Music collection. /// </summary> - Music = 3, + music = 3, /// <summary> /// Music videos collection. /// </summary> - MusicVideos = 4, + musicvideos = 4, /// <summary> /// Trailers collection. /// </summary> - Trailers = 5, + trailers = 5, /// <summary> /// Home videos collection. /// </summary> - HomeVideos = 6, + homevideos = 6, /// <summary> /// Box sets collection. /// </summary> - BoxSets = 7, + boxsets = 7, /// <summary> /// Books collection. /// </summary> - Books = 8, + books = 8, /// <summary> /// Photos collection. /// </summary> - Photos = 9, + photos = 9, /// <summary> /// Live tv collection. /// </summary> - LiveTv = 10, + livetv = 10, /// <summary> /// Playlists collection. /// </summary> - Playlists = 11, + playlists = 11, /// <summary> /// Folders collection. /// </summary> - Folders = 12, + folders = 12, /// <summary> /// Tv show series collection. /// </summary> [OpenApiIgnoreEnum] - TvShowSeries = 101, + tvshowseries = 101, /// <summary> /// Tv genres collection. /// </summary> [OpenApiIgnoreEnum] - TvGenres = 102, + tvgenres = 102, /// <summary> /// Tv genre collection. /// </summary> [OpenApiIgnoreEnum] - TvGenre = 103, + tvgenre = 103, /// <summary> /// Tv latest collection. /// </summary> [OpenApiIgnoreEnum] - TvLatest = 104, + tvlatest = 104, /// <summary> /// Tv next up collection. /// </summary> [OpenApiIgnoreEnum] - TvNextUp = 105, + tvnextup = 105, /// <summary> /// Tv resume collection. /// </summary> [OpenApiIgnoreEnum] - TvResume = 106, + tvresume = 106, /// <summary> /// Tv favorite series collection. /// </summary> [OpenApiIgnoreEnum] - TvFavoriteSeries = 107, + tvfavoriteseries = 107, /// <summary> /// Tv favorite episodes collection. /// </summary> [OpenApiIgnoreEnum] - TvFavoriteEpisodes = 108, + tvfavoriteepisodes = 108, /// <summary> /// Latest movies collection. /// </summary> [OpenApiIgnoreEnum] - MovieLatest = 109, + movielatest = 109, /// <summary> /// Movies to resume collection. /// </summary> [OpenApiIgnoreEnum] - MovieResume = 110, + movieresume = 110, /// <summary> /// Movie movie collection. /// </summary> [OpenApiIgnoreEnum] - MovieMovies = 111, + moviemovies = 111, /// <summary> /// Movie collections collection. /// </summary> [OpenApiIgnoreEnum] - MovieCollections = 112, + moviecollection = 112, /// <summary> /// Movie favorites collection. /// </summary> [OpenApiIgnoreEnum] - MovieFavorites = 113, + moviefavorites = 113, /// <summary> /// Movie genres collection. /// </summary> [OpenApiIgnoreEnum] - MovieGenres = 114, + moviegenres = 114, /// <summary> /// Movie genre collection. /// </summary> [OpenApiIgnoreEnum] - MovieGenre = 115 + moviegenre = 115 } diff --git a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs index a4b4c1959..5e5d52b6b 100644 --- a/Jellyfin.Server.Implementations/Devices/DeviceManager.cs +++ b/Jellyfin.Server.Implementations/Devices/DeviceManager.cs @@ -110,21 +110,21 @@ namespace Jellyfin.Server.Implementations.Devices /// <inheritdoc /> public async Task<DeviceInfo?> GetDevice(string id) { - Device? device; var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { - device = await dbContext.Devices + var device = await dbContext.Devices .Where(d => d.DeviceId == id) .OrderByDescending(d => d.DateLastActivity) .Include(d => d.User) + .SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o }) .FirstOrDefaultAsync() .ConfigureAwait(false); - } - var deviceInfo = device is null ? null : ToDeviceInfo(device); + var deviceInfo = device is null ? null : ToDeviceInfo(device.Device, device.Options); - return deviceInfo; + return deviceInfo; + } } /// <inheritdoc /> @@ -167,22 +167,18 @@ namespace Jellyfin.Server.Implementations.Devices } /// <inheritdoc /> - public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId, bool? supportsSync) + public async Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId) { var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { - IAsyncEnumerable<Device> sessions = dbContext.Devices + var sessions = dbContext.Devices .Include(d => d.User) .OrderByDescending(d => d.DateLastActivity) .ThenBy(d => d.DeviceId) + .SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o }) .AsAsyncEnumerable(); - if (supportsSync.HasValue) - { - sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == supportsSync.Value); - } - if (userId.HasValue) { var user = _userManager.GetUserById(userId.Value); @@ -191,10 +187,10 @@ namespace Jellyfin.Server.Implementations.Devices throw new ResourceNotFoundException(); } - sessions = sessions.Where(i => CanAccessDevice(user, i.DeviceId)); + sessions = sessions.Where(i => CanAccessDevice(user, i.Device.DeviceId)); } - var array = await sessions.Select(device => ToDeviceInfo(device)).ToArrayAsync().ConfigureAwait(false); + var array = await sessions.Select(device => ToDeviceInfo(device.Device, device.Options)).ToArrayAsync().ConfigureAwait(false); return new QueryResult<DeviceInfo>(array); } @@ -226,7 +222,7 @@ namespace Jellyfin.Server.Implementations.Devices || !GetCapabilities(deviceId).SupportsPersistentIdentifier; } - private DeviceInfo ToDeviceInfo(Device authInfo) + private DeviceInfo ToDeviceInfo(Device authInfo, DeviceOptions? options = null) { var caps = GetCapabilities(authInfo.DeviceId); @@ -239,7 +235,8 @@ namespace Jellyfin.Server.Implementations.Devices LastUserName = authInfo.User.Username, Name = authInfo.DeviceName, DateLastActivity = authInfo.DateLastActivity, - IconUrl = caps.IconUrl + IconUrl = caps.IconUrl, + CustomName = options?.CustomName, }; } } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index 990b9a5bd..c4a2bfdb8 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -1,7 +1,7 @@ #pragma warning disable CA1307 +#pragma warning disable CA1309 // Use ordinal string comparison - EF can't translate this using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -11,6 +11,7 @@ using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Data.Events.Users; +using Jellyfin.Extensions; using MediaBrowser.Common; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; @@ -46,8 +47,6 @@ namespace Jellyfin.Server.Implementations.Users private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider; private readonly IServerConfigurationManager _serverConfigurationManager; - private readonly IDictionary<Guid, User> _users; - /// <summary> /// Initializes a new instance of the <see cref="UserManager"/> class. /// </summary> @@ -58,6 +57,8 @@ namespace Jellyfin.Server.Implementations.Users /// <param name="imageProcessor">The image processor.</param> /// <param name="logger">The logger.</param> /// <param name="serverConfigurationManager">The system config manager.</param> + /// <param name="passwordResetProviders">The password reset providers.</param> + /// <param name="authenticationProviders">The authentication providers.</param> public UserManager( IDbContextFactory<JellyfinDbContext> dbProvider, IEventManager eventManager, @@ -65,7 +66,9 @@ namespace Jellyfin.Server.Implementations.Users IApplicationHost appHost, IImageProcessor imageProcessor, ILogger<UserManager> logger, - IServerConfigurationManager serverConfigurationManager) + IServerConfigurationManager serverConfigurationManager, + IEnumerable<IPasswordResetProvider> passwordResetProviders, + IEnumerable<IAuthenticationProvider> authenticationProviders) { _dbProvider = dbProvider; _eventManager = eventManager; @@ -75,35 +78,36 @@ namespace Jellyfin.Server.Implementations.Users _logger = logger; _serverConfigurationManager = serverConfigurationManager; - _passwordResetProviders = appHost.GetExports<IPasswordResetProvider>(); - _authenticationProviders = appHost.GetExports<IAuthenticationProvider>(); + _passwordResetProviders = passwordResetProviders.ToList(); + _authenticationProviders = authenticationProviders.ToList(); _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First(); _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First(); _defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First(); - - _users = new ConcurrentDictionary<Guid, User>(); - using var dbContext = _dbProvider.CreateDbContext(); - foreach (var user in dbContext.Users - .AsSplitQuery() - .Include(user => user.Permissions) - .Include(user => user.Preferences) - .Include(user => user.AccessSchedules) - .Include(user => user.ProfileImage) - .AsEnumerable()) - { - _users.Add(user.Id, user); - } } /// <inheritdoc/> public event EventHandler<GenericEventArgs<User>>? OnUserUpdated; /// <inheritdoc/> - public IEnumerable<User> Users => _users.Values; + public IEnumerable<User> Users + { + get + { + using var dbContext = _dbProvider.CreateDbContext(); + return GetUsersInternal(dbContext).ToList(); + } + } /// <inheritdoc/> - public IEnumerable<Guid> UsersIds => _users.Keys; + public IEnumerable<Guid> UsersIds + { + get + { + using var dbContext = _dbProvider.CreateDbContext(); + return dbContext.Users.Select(u => u.Id).ToList(); + } + } // This is some regex that matches only on unicode "word" characters, as well as -, _ and @ // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness @@ -114,13 +118,13 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public User? GetUserById(Guid id) { - if (id.Equals(default)) + if (id.IsEmpty()) { throw new ArgumentException("Guid can't be empty", nameof(id)); } - _users.TryGetValue(id, out var user); - return user; + using var dbContext = _dbProvider.CreateDbContext(); + return GetUsersInternal(dbContext).FirstOrDefault(u => u.Id.Equals(id)); } /// <inheritdoc/> @@ -131,7 +135,9 @@ namespace Jellyfin.Server.Implementations.Users throw new ArgumentException("Invalid username", nameof(name)); } - return _users.Values.FirstOrDefault(u => string.Equals(u.Username, name, StringComparison.OrdinalIgnoreCase)); + using var dbContext = _dbProvider.CreateDbContext(); + return GetUsersInternal(dbContext) + .FirstOrDefault(u => string.Equals(u.Username, name)); } /// <inheritdoc/> @@ -196,8 +202,6 @@ namespace Jellyfin.Server.Implementations.Users user.AddDefaultPermissions(); user.AddDefaultPreferences(); - _users.Add(user.Id, user); - return user; } @@ -232,40 +236,46 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc/> public async Task DeleteUserAsync(Guid userId) { - if (!_users.TryGetValue(userId, out var user)) - { - throw new ResourceNotFoundException(nameof(userId)); - } + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); - if (_users.Count == 1) + await using (dbContext.ConfigureAwait(false)) { - throw new InvalidOperationException(string.Format( - CultureInfo.InvariantCulture, - "The user '{0}' cannot be deleted because there must be at least one user in the system.", - user.Username)); - } + var user = await dbContext.Users + .AsSingleQuery() + .Include(u => u.Permissions) + .FirstOrDefaultAsync(u => u.Id.Equals(userId)) + .ConfigureAwait(false); + if (user is null) + { + throw new ResourceNotFoundException(nameof(userId)); + } - if (user.HasPermission(PermissionKind.IsAdministrator) - && Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1) - { - throw new ArgumentException( - string.Format( + if (await dbContext.Users.CountAsync().ConfigureAwait(false) == 1) + { + throw new InvalidOperationException(string.Format( CultureInfo.InvariantCulture, - "The user '{0}' cannot be deleted because there must be at least one admin user in the system.", - user.Username), - nameof(userId)); - } + "The user '{0}' cannot be deleted because there must be at least one user in the system.", + user.Username)); + } + + if (user.HasPermission(PermissionKind.IsAdministrator) + && await dbContext.Users + .CountAsync(u => u.Permissions.Any(p => p.Kind == PermissionKind.IsAdministrator && p.Value)) + .ConfigureAwait(false) == 1) + { + throw new ArgumentException( + string.Format( + CultureInfo.InvariantCulture, + "The user '{0}' cannot be deleted because there must be at least one admin user in the system.", + user.Username), + nameof(userId)); + } - var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); - await using (dbContext.ConfigureAwait(false)) - { dbContext.Users.Remove(user); await dbContext.SaveChangesAsync().ConfigureAwait(false); - } - _users.Remove(userId); - - await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false); + await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false); + } } /// <inheritdoc/> @@ -532,23 +542,23 @@ namespace Jellyfin.Server.Implementations.Users /// <inheritdoc /> public async Task InitializeAsync() { - // TODO: Refactor the startup wizard so that it doesn't require a user to already exist. - if (_users.Any()) + var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); + await using (dbContext.ConfigureAwait(false)) { - return; - } + // TODO: Refactor the startup wizard so that it doesn't require a user to already exist. + if (await dbContext.Users.AnyAsync().ConfigureAwait(false)) + { + return; + } - var defaultName = Environment.UserName; - if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName)) - { - defaultName = "MyJellyfinUser"; - } + var defaultName = Environment.UserName; + if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName)) + { + defaultName = "MyJellyfinUser"; + } - _logger.LogWarning("No users, creating one with username {UserName}", defaultName); + _logger.LogWarning("No users, creating one with username {UserName}", defaultName); - var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); - await using (dbContext.ConfigureAwait(false)) - { var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false); newUser.SetPermission(PermissionKind.IsAdministrator, true); newUser.SetPermission(PermissionKind.EnableContentDeletion, true); @@ -595,12 +605,9 @@ namespace Jellyfin.Server.Implementations.Users var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { - var user = dbContext.Users - .Include(u => u.Permissions) - .Include(u => u.Preferences) - .Include(u => u.AccessSchedules) - .Include(u => u.ProfileImage) - .FirstOrDefault(u => u.Id.Equals(userId)) + var user = await GetUsersInternal(dbContext) + .FirstOrDefaultAsync(u => u.Id.Equals(userId)) + .ConfigureAwait(false) ?? throw new ArgumentException("No user exists with given Id!"); user.SubtitleMode = config.SubtitleMode; @@ -628,7 +635,6 @@ namespace Jellyfin.Server.Implementations.Users user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes); dbContext.Update(user); - _users[user.Id] = user; await dbContext.SaveChangesAsync().ConfigureAwait(false); } } @@ -639,12 +645,9 @@ namespace Jellyfin.Server.Implementations.Users var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); await using (dbContext.ConfigureAwait(false)) { - var user = dbContext.Users - .Include(u => u.Permissions) - .Include(u => u.Preferences) - .Include(u => u.AccessSchedules) - .Include(u => u.ProfileImage) - .FirstOrDefault(u => u.Id.Equals(userId)) + var user = await GetUsersInternal(dbContext) + .FirstOrDefaultAsync(u => u.Id.Equals(userId)) + .ConfigureAwait(false) ?? throw new ArgumentException("No user exists with given Id!"); // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0" @@ -704,7 +707,6 @@ namespace Jellyfin.Server.Implementations.Users user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders); dbContext.Update(user); - _users[user.Id] = user; await dbContext.SaveChangesAsync().ConfigureAwait(false); } } @@ -725,7 +727,6 @@ namespace Jellyfin.Server.Implementations.Users } user.ProfileImage = null; - _users[user.Id] = user; } internal static void ThrowIfInvalidUsername(string name) @@ -872,8 +873,15 @@ namespace Jellyfin.Server.Implementations.Users private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user) { dbContext.Users.Update(user); - _users[user.Id] = user; await dbContext.SaveChangesAsync().ConfigureAwait(false); } + + private IQueryable<User> GetUsersInternal(JellyfinDbContext dbContext) + => dbContext.Users + .AsSplitQuery() + .Include(user => user.Permissions) + .Include(user => user.Preferences) + .Include(user => user.AccessSchedules) + .Include(user => user.ProfileImage); } } diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index c12c90a68..d5b6e93b8 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -6,6 +6,7 @@ using Emby.Server.Implementations.Session; using Jellyfin.Api.WebSocketListeners; using Jellyfin.Drawing; using Jellyfin.Drawing.Skia; +using Jellyfin.LiveTv; using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Activity; using Jellyfin.Server.Implementations.Devices; @@ -14,6 +15,7 @@ using Jellyfin.Server.Implementations.Security; using Jellyfin.Server.Implementations.Trickplay; using Jellyfin.Server.Implementations.Users; using MediaBrowser.Controller; +using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.BaseItemManager; using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Drawing; @@ -78,6 +80,9 @@ namespace Jellyfin.Server serviceCollection.AddSingleton<IActivityManager, ActivityManager>(); serviceCollection.AddSingleton<IUserManager, UserManager>(); + serviceCollection.AddSingleton<IAuthenticationProvider, DefaultAuthenticationProvider>(); + serviceCollection.AddSingleton<IAuthenticationProvider, InvalidAuthProvider>(); + serviceCollection.AddSingleton<IPasswordResetProvider, DefaultPasswordResetProvider>(); serviceCollection.AddScoped<IDisplayPreferencesManager, DisplayPreferencesManager>(); serviceCollection.AddSingleton<IDeviceManager, DeviceManager>(); serviceCollection.AddSingleton<ITrickplayManager, TrickplayManager>(); @@ -113,6 +118,9 @@ namespace Jellyfin.Server // Jellyfin.Server.Implementations yield return typeof(JellyfinDbContext).Assembly; + + // Jellyfin.LiveTv + yield return typeof(LiveTvManager).Assembly; } } } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 1d4d97551..21c6e6f01 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -58,6 +58,7 @@ <ProjectReference Include="..\src\Jellyfin.Drawing\Jellyfin.Drawing.csproj" /> <ProjectReference Include="..\Emby.Server.Implementations\Emby.Server.Implementations.csproj" /> <ProjectReference Include="..\src\Jellyfin.Drawing.Skia\Jellyfin.Drawing.Skia.csproj" /> + <ProjectReference Include="..\src\Jellyfin.LiveTv\Jellyfin.LiveTv.csproj" /> <ProjectReference Include="..\Jellyfin.Server.Implementations\Jellyfin.Server.Implementations.csproj" /> <ProjectReference Include="..\src\Jellyfin.MediaEncoding.Hls\Jellyfin.MediaEncoding.Hls.csproj" /> </ItemGroup> diff --git a/Jellyfin.Server/Startup.cs b/Jellyfin.Server/Startup.cs index aa7be9109..7d5f22545 100644 --- a/Jellyfin.Server/Startup.cs +++ b/Jellyfin.Server/Startup.cs @@ -5,7 +5,9 @@ using System.Net.Http.Headers; using System.Net.Mime; using System.Text; using Jellyfin.Api.Middleware; +using Jellyfin.LiveTv.Extensions; using Jellyfin.MediaEncoding.Hls.Extensions; +using Jellyfin.Networking; using Jellyfin.Networking.HappyEyeballs; using Jellyfin.Server.Extensions; using Jellyfin.Server.HealthChecks; @@ -13,7 +15,6 @@ using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations.Extensions; using Jellyfin.Server.Infrastructure; using MediaBrowser.Common.Net; -using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Extensions; using Microsoft.AspNetCore.Builder; @@ -121,6 +122,9 @@ namespace Jellyfin.Server .AddCheck<DbContextFactoryHealthCheck<JellyfinDbContext>>(nameof(JellyfinDbContext)); services.AddHlsPlaylistGenerator(); + services.AddLiveTvServices(); + + services.AddHostedService<AutoDiscoveryHost>(); } /// <summary> diff --git a/Jellyfin.sln b/Jellyfin.sln index 4385ac241..30eab6cc2 100644 --- a/Jellyfin.sln +++ b/Jellyfin.sln @@ -87,6 +87,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.MediaEncoding.Hls. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.MediaEncoding.Keyframes.Tests", "tests\Jellyfin.MediaEncoding.Keyframes.Tests\Jellyfin.MediaEncoding.Keyframes.Tests.csproj", "{24960660-DE6C-47BF-AEEF-CEE8F19FE6C2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.LiveTv.Tests", "tests\Jellyfin.LiveTv.Tests\Jellyfin.LiveTv.Tests.csproj", "{C4F71272-C6BE-4C30-BE0D-4E6ED651D6D3}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jellyfin.LiveTv", "src\Jellyfin.LiveTv\Jellyfin.LiveTv.csproj", "{8C6B2B13-58A4-4506-9DAB-1F882A093FE0}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -233,6 +237,14 @@ Global {24960660-DE6C-47BF-AEEF-CEE8F19FE6C2}.Debug|Any CPU.Build.0 = Debug|Any CPU {24960660-DE6C-47BF-AEEF-CEE8F19FE6C2}.Release|Any CPU.ActiveCfg = Release|Any CPU {24960660-DE6C-47BF-AEEF-CEE8F19FE6C2}.Release|Any CPU.Build.0 = Release|Any CPU + {C4F71272-C6BE-4C30-BE0D-4E6ED651D6D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C4F71272-C6BE-4C30-BE0D-4E6ED651D6D3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C4F71272-C6BE-4C30-BE0D-4E6ED651D6D3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C4F71272-C6BE-4C30-BE0D-4E6ED651D6D3}.Release|Any CPU.Build.0 = Release|Any CPU + {8C6B2B13-58A4-4506-9DAB-1F882A093FE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C6B2B13-58A4-4506-9DAB-1F882A093FE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C6B2B13-58A4-4506-9DAB-1F882A093FE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C6B2B13-58A4-4506-9DAB-1F882A093FE0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -259,6 +271,8 @@ Global {08FFF49B-F175-4807-A2B5-73B0EBD9F716} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} {154872D9-6C12-4007-96E3-8F70A58386CE} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} {0A3FCC4D-C714-4072-B90F-E374A15F9FF9} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} + {C4F71272-C6BE-4C30-BE0D-4E6ED651D6D3} = {FBBB5129-006E-4AD7-BAD5-8B7CA1D10ED6} + {8C6B2B13-58A4-4506-9DAB-1F882A093FE0} = {C9F0AB5D-F4D7-40C8-A353-3305C86D6D4C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {3448830C-EBDC-426C-85CD-7BBB9651A7FE} diff --git a/MediaBrowser.Controller/Channels/IChannelManager.cs b/MediaBrowser.Controller/Channels/IChannelManager.cs index 8eb27888a..c8b432ecb 100644 --- a/MediaBrowser.Controller/Channels/IChannelManager.cs +++ b/MediaBrowser.Controller/Channels/IChannelManager.cs @@ -95,12 +95,5 @@ namespace MediaBrowser.Controller.Channels /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The item media sources.</returns> IEnumerable<MediaSourceInfo> GetStaticMediaSources(BaseItem item, CancellationToken cancellationToken); - - /// <summary> - /// Whether the item supports media probe. - /// </summary> - /// <param name="item">The item.</param> - /// <returns>Whether media probe should be enabled.</returns> - bool EnableMediaProbe(BaseItem item); } } diff --git a/MediaBrowser.Controller/Devices/IDeviceManager.cs b/MediaBrowser.Controller/Devices/IDeviceManager.cs index 8362db1a7..eb181dcc4 100644 --- a/MediaBrowser.Controller/Devices/IDeviceManager.cs +++ b/MediaBrowser.Controller/Devices/IDeviceManager.cs @@ -59,9 +59,8 @@ namespace MediaBrowser.Controller.Devices /// Gets the devices. /// </summary> /// <param name="userId">The user's id, or <c>null</c>.</param> - /// <param name="supportsSync">A value indicating whether the device supports sync, or <c>null</c>.</param> /// <returns>IEnumerable<DeviceInfo>.</returns> - Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId, bool? supportsSync); + Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId); Task DeleteDevice(Device device); diff --git a/MediaBrowser.Controller/Entities/AggregateFolder.cs b/MediaBrowser.Controller/Entities/AggregateFolder.cs index d789033f1..b225f22df 100644 --- a/MediaBrowser.Controller/Entities/AggregateFolder.cs +++ b/MediaBrowser.Controller/Entities/AggregateFolder.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -184,7 +185,7 @@ namespace MediaBrowser.Controller.Entities /// <exception cref="ArgumentNullException">The id is empty.</exception> public BaseItem FindVirtualChild(Guid id) { - if (id.Equals(default)) + if (id.IsEmpty()) { throw new ArgumentNullException(nameof(id)); } diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index 18d948a62..11cdf8444 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.Entities.Audio public class MusicArtist : Folder, IItemByName, IHasMusicGenres, IHasDualAccess, IHasLookupInfo<ArtistInfo> { [JsonIgnore] - public bool IsAccessedByName => ParentId.Equals(default); + public bool IsAccessedByName => ParentId.IsEmpty(); [JsonIgnore] public override bool IsFolder => !IsAccessedByName; diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 98485f9a8..ddcc994a0 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -240,7 +240,7 @@ namespace MediaBrowser.Controller.Entities { get { - if (!ChannelId.Equals(default)) + if (!ChannelId.IsEmpty()) { return SourceType.Channel; } @@ -530,7 +530,7 @@ namespace MediaBrowser.Controller.Entities get { var id = DisplayParentId; - if (id.Equals(default)) + if (id.IsEmpty()) { return null; } @@ -724,7 +724,7 @@ namespace MediaBrowser.Controller.Entities if (this is IHasCollectionType view) { - if (view.CollectionType == CollectionType.LiveTv) + if (view.CollectionType == CollectionType.livetv) { return true; } @@ -746,7 +746,7 @@ namespace MediaBrowser.Controller.Entities public virtual bool StopRefreshIfLocalMetadataFound => true; [JsonIgnore] - protected virtual bool SupportsOwnedItems => !ParentId.Equals(default) && IsFileProtocol; + protected virtual bool SupportsOwnedItems => !ParentId.IsEmpty() && IsFileProtocol; [JsonIgnore] public virtual bool SupportsPeople => false; @@ -773,8 +773,6 @@ namespace MediaBrowser.Controller.Entities /// <value>The remote trailers.</value> public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; } - public virtual bool SupportsExternalTransfer => false; - public virtual double GetDefaultPrimaryImageAspectRatio() { return 0; @@ -825,7 +823,7 @@ namespace MediaBrowser.Controller.Entities public BaseItem GetOwner() { var ownerId = OwnerId; - return ownerId.Equals(default) ? null : LibraryManager.GetItemById(ownerId); + return ownerId.IsEmpty() ? null : LibraryManager.GetItemById(ownerId); } public bool CanDelete(User user, List<Folder> allCollectionFolders) @@ -970,7 +968,7 @@ namespace MediaBrowser.Controller.Entities public BaseItem GetParent() { var parentId = ParentId; - if (parentId.Equals(default)) + if (parentId.IsEmpty()) { return null; } @@ -1363,7 +1361,7 @@ namespace MediaBrowser.Controller.Entities var tasks = extras.Select(i => { var subOptions = new MetadataRefreshOptions(options); - if (!i.OwnerId.Equals(ownerId) || !i.ParentId.Equals(default)) + if (!i.OwnerId.Equals(ownerId) || !i.ParentId.IsEmpty()) { i.OwnerId = ownerId; i.ParentId = Guid.Empty; @@ -1675,7 +1673,7 @@ namespace MediaBrowser.Controller.Entities // First get using the cached Id if (info.ItemId.HasValue) { - if (info.ItemId.Value.Equals(default)) + if (info.ItemId.Value.IsEmpty()) { return null; } @@ -2441,7 +2439,7 @@ namespace MediaBrowser.Controller.Entities return Task.FromResult(true); } - if (video.OwnerId.Equals(default)) + if (video.OwnerId.IsEmpty()) { video.OwnerId = this.Id; } diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index e707eedbf..74eb089de 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -12,6 +12,7 @@ using System.Threading.Tasks; using System.Threading.Tasks.Dataflow; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Collections; @@ -198,7 +199,7 @@ namespace MediaBrowser.Controller.Entities { item.SetParent(this); - if (item.Id.Equals(default)) + if (item.Id.IsEmpty()) { item.Id = LibraryManager.GetNewItemId(item.Path, item.GetType()); } @@ -697,7 +698,7 @@ namespace MediaBrowser.Controller.Entities if (this is not UserRootFolder && this is not AggregateFolder - && query.ParentId.Equals(default)) + && query.ParentId.IsEmpty()) { query.Parent = this; } @@ -840,7 +841,7 @@ namespace MediaBrowser.Controller.Entities return true; } - if (query.AdjacentTo.HasValue && !query.AdjacentTo.Value.Equals(default)) + if (!query.AdjacentTo.IsNullOrEmpty()) { Logger.LogDebug("Query requires post-filtering due to AdjacentTo"); return true; @@ -987,7 +988,7 @@ namespace MediaBrowser.Controller.Entities #pragma warning restore CA1309 // This must be the last filter - if (query.AdjacentTo.HasValue && !query.AdjacentTo.Value.Equals(default)) + if (!query.AdjacentTo.IsNullOrEmpty()) { items = UserViewBuilder.FilterForAdjacency(items.ToList(), query.AdjacentTo.Value); } diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs index bf31508c1..37e241414 100644 --- a/MediaBrowser.Controller/Entities/TV/Episode.cs +++ b/MediaBrowser.Controller/Entities/TV/Episode.cs @@ -8,6 +8,7 @@ using System.Globalization; using System.Linq; using System.Text.Json.Serialization; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; @@ -74,12 +75,12 @@ namespace MediaBrowser.Controller.Entities.TV get { var seriesId = SeriesId; - if (seriesId.Equals(default)) + if (seriesId.IsEmpty()) { seriesId = FindSeriesId(); } - return seriesId.Equals(default) ? null : (LibraryManager.GetItemById(seriesId) as Series); + return seriesId.IsEmpty() ? null : (LibraryManager.GetItemById(seriesId) as Series); } } @@ -89,12 +90,12 @@ namespace MediaBrowser.Controller.Entities.TV get { var seasonId = SeasonId; - if (seasonId.Equals(default)) + if (seasonId.IsEmpty()) { seasonId = FindSeasonId(); } - return seasonId.Equals(default) ? null : (LibraryManager.GetItemById(seasonId) as Season); + return seasonId.IsEmpty() ? null : (LibraryManager.GetItemById(seasonId) as Season); } } @@ -271,7 +272,7 @@ namespace MediaBrowser.Controller.Entities.TV var seasonId = SeasonId; - if (!seasonId.Equals(default) && !list.Contains(seasonId)) + if (!seasonId.IsEmpty() && !list.Contains(seasonId)) { list.Add(seasonId); } diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index 0a040a3c2..c29cefc15 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Text.Json.Serialization; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Querying; @@ -48,12 +49,12 @@ namespace MediaBrowser.Controller.Entities.TV get { var seriesId = SeriesId; - if (seriesId.Equals(default)) + if (seriesId.IsEmpty()) { seriesId = FindSeriesId(); } - return seriesId.Equals(default) ? null : (LibraryManager.GetItemById(seriesId) as Series); + return seriesId.IsEmpty() ? null : (LibraryManager.GetItemById(seriesId) as Series); } } diff --git a/MediaBrowser.Controller/Entities/UserView.cs b/MediaBrowser.Controller/Entities/UserView.cs index 1f94cf767..c93488a85 100644 --- a/MediaBrowser.Controller/Entities/UserView.cs +++ b/MediaBrowser.Controller/Entities/UserView.cs @@ -19,19 +19,19 @@ namespace MediaBrowser.Controller.Entities { private static readonly CollectionType?[] _viewTypesEligibleForGrouping = { - Jellyfin.Data.Enums.CollectionType.Movies, - Jellyfin.Data.Enums.CollectionType.TvShows, + Jellyfin.Data.Enums.CollectionType.movies, + Jellyfin.Data.Enums.CollectionType.tvshows, null }; private static readonly CollectionType?[] _originalFolderViewTypes = { - Jellyfin.Data.Enums.CollectionType.Books, - Jellyfin.Data.Enums.CollectionType.MusicVideos, - Jellyfin.Data.Enums.CollectionType.HomeVideos, - Jellyfin.Data.Enums.CollectionType.Photos, - Jellyfin.Data.Enums.CollectionType.Music, - Jellyfin.Data.Enums.CollectionType.BoxSets + Jellyfin.Data.Enums.CollectionType.books, + Jellyfin.Data.Enums.CollectionType.musicvideos, + Jellyfin.Data.Enums.CollectionType.homevideos, + Jellyfin.Data.Enums.CollectionType.photos, + Jellyfin.Data.Enums.CollectionType.music, + Jellyfin.Data.Enums.CollectionType.boxsets }; public static ITVSeriesManager TVSeriesManager { get; set; } @@ -70,11 +70,11 @@ namespace MediaBrowser.Controller.Entities /// <inheritdoc /> public override IEnumerable<Guid> GetIdsForAncestorQuery() { - if (!DisplayParentId.Equals(default)) + if (!DisplayParentId.IsEmpty()) { yield return DisplayParentId; } - else if (!ParentId.Equals(default)) + else if (!ParentId.IsEmpty()) { yield return ParentId; } @@ -95,11 +95,11 @@ namespace MediaBrowser.Controller.Entities { var parent = this as Folder; - if (!DisplayParentId.Equals(default)) + if (!DisplayParentId.IsEmpty()) { parent = LibraryManager.GetItemById(DisplayParentId) as Folder ?? parent; } - else if (!ParentId.Equals(default)) + else if (!ParentId.IsEmpty()) { parent = LibraryManager.GetItemById(ParentId) as Folder ?? parent; } @@ -161,7 +161,7 @@ namespace MediaBrowser.Controller.Entities return true; } - return collectionFolder.CollectionType == Jellyfin.Data.Enums.CollectionType.Playlists; + return collectionFolder.CollectionType == Jellyfin.Data.Enums.CollectionType.playlists; } public static bool IsEligibleForGrouping(Folder folder) diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs index 42431c832..4af000557 100644 --- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs +++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs @@ -58,58 +58,58 @@ namespace MediaBrowser.Controller.Entities switch (viewType) { - case CollectionType.Folders: + case CollectionType.folders: return GetResult(_libraryManager.GetUserRootFolder().GetChildren(user, true), query); - case CollectionType.TvShows: + case CollectionType.tvshows: return GetTvView(queryParent, user, query); - case CollectionType.Movies: + case CollectionType.movies: return GetMovieFolders(queryParent, user, query); - case CollectionType.TvShowSeries: + case CollectionType.tvshowseries: return GetTvSeries(queryParent, user, query); - case CollectionType.TvGenres: + case CollectionType.tvgenres: return GetTvGenres(queryParent, user, query); - case CollectionType.TvGenre: + case CollectionType.tvgenre: return GetTvGenreItems(queryParent, displayParent, user, query); - case CollectionType.TvResume: + case CollectionType.tvresume: return GetTvResume(queryParent, user, query); - case CollectionType.TvNextUp: + case CollectionType.tvnextup: return GetTvNextUp(queryParent, query); - case CollectionType.TvLatest: + case CollectionType.tvlatest: return GetTvLatest(queryParent, user, query); - case CollectionType.MovieFavorites: + case CollectionType.moviefavorites: return GetFavoriteMovies(queryParent, user, query); - case CollectionType.MovieLatest: + case CollectionType.movielatest: return GetMovieLatest(queryParent, user, query); - case CollectionType.MovieGenres: + case CollectionType.moviegenres: return GetMovieGenres(queryParent, user, query); - case CollectionType.MovieGenre: + case CollectionType.moviegenre: return GetMovieGenreItems(queryParent, displayParent, user, query); - case CollectionType.MovieResume: + case CollectionType.movieresume: return GetMovieResume(queryParent, user, query); - case CollectionType.MovieMovies: + case CollectionType.moviemovies: return GetMovieMovies(queryParent, user, query); - case CollectionType.MovieCollections: + case CollectionType.moviecollection: return GetMovieCollections(user, query); - case CollectionType.TvFavoriteEpisodes: + case CollectionType.tvfavoriteepisodes: return GetFavoriteEpisodes(queryParent, user, query); - case CollectionType.TvFavoriteSeries: + case CollectionType.tvfavoriteseries: return GetFavoriteSeries(queryParent, user, query); default: @@ -146,12 +146,12 @@ namespace MediaBrowser.Controller.Entities var list = new List<BaseItem> { - GetUserView(CollectionType.MovieResume, "HeaderContinueWatching", "0", parent), - GetUserView(CollectionType.MovieLatest, "Latest", "1", parent), - GetUserView(CollectionType.MovieMovies, "Movies", "2", parent), - GetUserView(CollectionType.MovieCollections, "Collections", "3", parent), - GetUserView(CollectionType.MovieFavorites, "Favorites", "4", parent), - GetUserView(CollectionType.MovieGenres, "Genres", "5", parent) + GetUserView(CollectionType.movieresume, "HeaderContinueWatching", "0", parent), + GetUserView(CollectionType.movielatest, "Latest", "1", parent), + GetUserView(CollectionType.moviemovies, "Movies", "2", parent), + GetUserView(CollectionType.moviecollection, "Collections", "3", parent), + GetUserView(CollectionType.moviefavorites, "Favorites", "4", parent), + GetUserView(CollectionType.moviegenres, "Genres", "5", parent) }; return GetResult(list, query); @@ -264,7 +264,7 @@ namespace MediaBrowser.Controller.Entities } }) .Where(i => i is not null) - .Select(i => GetUserViewWithName(CollectionType.MovieGenre, i.SortName, parent)); + .Select(i => GetUserViewWithName(CollectionType.moviegenre, i.SortName, parent)); return GetResult(genres, query); } @@ -303,13 +303,13 @@ namespace MediaBrowser.Controller.Entities var list = new List<BaseItem> { - GetUserView(CollectionType.TvResume, "HeaderContinueWatching", "0", parent), - GetUserView(CollectionType.TvNextUp, "HeaderNextUp", "1", parent), - GetUserView(CollectionType.TvLatest, "Latest", "2", parent), - GetUserView(CollectionType.TvShowSeries, "Shows", "3", parent), - GetUserView(CollectionType.TvFavoriteSeries, "HeaderFavoriteShows", "4", parent), - GetUserView(CollectionType.TvFavoriteEpisodes, "HeaderFavoriteEpisodes", "5", parent), - GetUserView(CollectionType.TvGenres, "Genres", "6", parent) + GetUserView(CollectionType.tvresume, "HeaderContinueWatching", "0", parent), + GetUserView(CollectionType.tvnextup, "HeaderNextUp", "1", parent), + GetUserView(CollectionType.tvlatest, "Latest", "2", parent), + GetUserView(CollectionType.tvshowseries, "Shows", "3", parent), + GetUserView(CollectionType.tvfavoriteseries, "HeaderFavoriteShows", "4", parent), + GetUserView(CollectionType.tvfavoriteepisodes, "HeaderFavoriteEpisodes", "5", parent), + GetUserView(CollectionType.tvgenres, "Genres", "6", parent) }; return GetResult(list, query); @@ -330,7 +330,7 @@ namespace MediaBrowser.Controller.Entities private QueryResult<BaseItem> GetTvNextUp(Folder parent, InternalItemsQuery query) { - var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.TvShows }); + var parentFolders = GetMediaFolders(parent, query.User, new[] { CollectionType.tvshows }); var result = _tvSeriesManager.GetNextUp( new NextUpQuery @@ -392,7 +392,7 @@ namespace MediaBrowser.Controller.Entities } }) .Where(i => i is not null) - .Select(i => GetUserViewWithName(CollectionType.TvGenre, i.SortName, parent)); + .Select(i => GetUserViewWithName(CollectionType.tvgenre, i.SortName, parent)); return GetResult(genres, query); } @@ -433,7 +433,7 @@ namespace MediaBrowser.Controller.Entities var user = query.User; // This must be the last filter - if (query.AdjacentTo.HasValue && !query.AdjacentTo.Value.Equals(default)) + if (!query.AdjacentTo.IsNullOrEmpty()) { items = FilterForAdjacency(items.ToList(), query.AdjacentTo.Value); } diff --git a/MediaBrowser.Controller/Entities/Video.cs b/MediaBrowser.Controller/Entities/Video.cs index be2eb4d28..5adadec39 100644 --- a/MediaBrowser.Controller/Entities/Video.cs +++ b/MediaBrowser.Controller/Entities/Video.cs @@ -456,7 +456,7 @@ namespace MediaBrowser.Controller.Entities foreach (var child in LinkedAlternateVersions) { // Reset the cached value - if (child.ItemId.HasValue && child.ItemId.Value.Equals(default)) + if (child.ItemId.IsNullOrEmpty()) { child.ItemId = null; } diff --git a/MediaBrowser.Controller/Library/ILiveStream.cs b/MediaBrowser.Controller/Library/ILiveStream.cs index 4c44a17fd..bf64aca0f 100644 --- a/MediaBrowser.Controller/Library/ILiveStream.cs +++ b/MediaBrowser.Controller/Library/ILiveStream.cs @@ -2,6 +2,7 @@ #pragma warning disable CA1711, CS1591 +using System; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -9,7 +10,7 @@ using MediaBrowser.Model.Dto; namespace MediaBrowser.Controller.Library { - public interface ILiveStream + public interface ILiveStream : IDisposable { int ConsumerCount { get; set; } diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs index f1758a9d8..bace703ad 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs @@ -8,6 +8,7 @@ using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -117,6 +118,14 @@ namespace MediaBrowser.Controller.Library public ILiveStream GetLiveStreamInfoByUniqueId(string uniqueId); /// <summary> + /// Gets the media sources for an active recording. + /// </summary> + /// <param name="info">The <see cref="ActiveRecordingInfo"/>.</param> + /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param> + /// <returns>A task containing the <see cref="MediaSourceInfo"/>'s for the recording.</returns> + Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken); + + /// <summary> /// Closes the media source. /// </summary> /// <param name="id">The live stream identifier.</param> diff --git a/MediaBrowser.Controller/Library/IUserDataManager.cs b/MediaBrowser.Controller/Library/IUserDataManager.cs index 034c40591..43cccfc65 100644 --- a/MediaBrowser.Controller/Library/IUserDataManager.cs +++ b/MediaBrowser.Controller/Library/IUserDataManager.cs @@ -35,6 +35,15 @@ namespace MediaBrowser.Controller.Library void SaveUserData(User user, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken); + /// <summary> + /// Save the provided user data for the given user. + /// </summary> + /// <param name="user">The user.</param> + /// <param name="item">The item.</param> + /// <param name="userDataDto">The reason for updating the user data.</param> + /// <param name="reason">The reason.</param> + void SaveUserData(User user, BaseItem item, UpdateUserItemDataDto userDataDto, UserDataSaveReason reason); + UserItemData GetUserData(User user, BaseItem item); UserItemData GetUserData(Guid userId, BaseItem item); diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs index 3b6a16dee..26f9fe42d 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvManager.cs @@ -36,7 +36,7 @@ namespace MediaBrowser.Controller.LiveTv /// <value>The services.</value> IReadOnlyList<ILiveTvService> Services { get; } - IListingsProvider[] ListingProviders { get; } + IReadOnlyList<IListingsProvider> ListingProviders { get; } /// <summary> /// Gets the new timer defaults asynchronous. @@ -71,9 +71,8 @@ namespace MediaBrowser.Controller.LiveTv /// Adds the parts. /// </summary> /// <param name="services">The services.</param> - /// <param name="tunerHosts">The tuner hosts.</param> /// <param name="listingProviders">The listing providers.</param> - void AddParts(IEnumerable<ILiveTvService> services, IEnumerable<ITunerHost> tunerHosts, IEnumerable<IListingsProvider> listingProviders); + void AddParts(IEnumerable<ILiveTvService> services, IEnumerable<IListingsProvider> listingProviders); /// <summary> /// Gets the timer. @@ -254,14 +253,6 @@ namespace MediaBrowser.Controller.LiveTv Task AddInfoToProgramDto(IReadOnlyCollection<(BaseItem Item, BaseItemDto ItemDto)> programs, IReadOnlyList<ItemFields> fields, User user = null); /// <summary> - /// Saves the tuner host. - /// </summary> - /// <param name="info">Turner host to save.</param> - /// <param name="dataSourceChanged">Option to specify that data source has changed.</param> - /// <returns>Tuner host information wrapped in a task.</returns> - Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true); - - /// <summary> /// Saves the listing provider. /// </summary> /// <param name="info">The information.</param> @@ -298,10 +289,6 @@ namespace MediaBrowser.Controller.LiveTv Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken); - List<NameIdPair> GetTunerHostTypes(); - - Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken); - string GetEmbyTvActiveRecordingPath(string id); ActiveRecordingInfo GetActiveRecordingInfo(string path); diff --git a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs index ce34954e3..52fb15648 100644 --- a/MediaBrowser.Controller/LiveTv/ILiveTvService.cs +++ b/MediaBrowser.Controller/LiveTv/ILiveTvService.cs @@ -141,14 +141,6 @@ namespace MediaBrowser.Controller.LiveTv Task CloseLiveStream(string id, CancellationToken cancellationToken); /// <summary> - /// Records the live stream. - /// </summary> - /// <param name="id">The identifier.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - Task RecordLiveStream(string id, CancellationToken cancellationToken); - - /// <summary> /// Resets the tuner. /// </summary> /// <param name="id">The identifier.</param> @@ -180,9 +172,4 @@ namespace MediaBrowser.Controller.LiveTv { Task<ILiveStream> GetChannelStreamWithDirectStreamProvider(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken); } - - public interface ISupportsUpdatingDefaults - { - Task UpdateTimerDefaults(SeriesTimerInfo info, CancellationToken cancellationToken); - } } diff --git a/MediaBrowser.Controller/LiveTv/ITunerHost.cs b/MediaBrowser.Controller/LiveTv/ITunerHost.cs index 24820abb9..3689a2adf 100644 --- a/MediaBrowser.Controller/LiveTv/ITunerHost.cs +++ b/MediaBrowser.Controller/LiveTv/ITunerHost.cs @@ -36,13 +36,6 @@ namespace MediaBrowser.Controller.LiveTv Task<List<ChannelInfo>> GetChannels(bool enableCache, CancellationToken cancellationToken); /// <summary> - /// Gets the tuner infos. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task<List<LiveTvTunerInfo>>.</returns> - Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken); - - /// <summary> /// Gets the channel stream. /// </summary> /// <param name="channelId">The channel identifier.</param> @@ -50,7 +43,7 @@ namespace MediaBrowser.Controller.LiveTv /// <param name="currentLiveStreams">The current live streams.</param> /// <param name="cancellationToken">The cancellation token to cancel operation.</param> /// <returns>Live stream wrapped in a task.</returns> - Task<ILiveStream> GetChannelStream(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken); + Task<ILiveStream> GetChannelStream(string channelId, string streamId, IList<ILiveStream> currentLiveStreams, CancellationToken cancellationToken); /// <summary> /// Gets the channel stream media sources. diff --git a/MediaBrowser.Controller/LiveTv/ITunerHostManager.cs b/MediaBrowser.Controller/LiveTv/ITunerHostManager.cs new file mode 100644 index 000000000..3df6066f6 --- /dev/null +++ b/MediaBrowser.Controller/LiveTv/ITunerHostManager.cs @@ -0,0 +1,46 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.LiveTv; + +namespace MediaBrowser.Controller.LiveTv; + +/// <summary> +/// Service responsible for managing the <see cref="ITunerHost"/>s. +/// </summary> +public interface ITunerHostManager +{ + /// <summary> + /// Gets the available <see cref="ITunerHost"/>s. + /// </summary> + IReadOnlyList<ITunerHost> TunerHosts { get; } + + /// <summary> + /// Gets the <see cref="NameIdPair"/>s for the available <see cref="ITunerHost"/>s. + /// </summary> + /// <returns>The <see cref="NameIdPair"/>s.</returns> + IEnumerable<NameIdPair> GetTunerHostTypes(); + + /// <summary> + /// Saves the tuner host. + /// </summary> + /// <param name="info">Turner host to save.</param> + /// <param name="dataSourceChanged">Option to specify that data source has changed.</param> + /// <returns>Tuner host information wrapped in a task.</returns> + Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true); + + /// <summary> + /// Discovers the available tuners. + /// </summary> + /// <param name="newDevicesOnly">A value indicating whether to only return new devices.</param> + /// <returns>The <see cref="TunerHostInfo"/>s.</returns> + IAsyncEnumerable<TunerHostInfo> DiscoverTuners(bool newDevicesOnly); + + /// <summary> + /// Scans for tuner devices that have changed URLs. + /// </summary> + /// <param name="cancellationToken">The <see cref="CancellationToken"/> to use.</param> + /// <returns>A task that represents the scanning operation.</returns> + Task ScanForTunerDeviceChanges(CancellationToken cancellationToken); +} diff --git a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs b/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs deleted file mode 100644 index eb3babc18..000000000 --- a/MediaBrowser.Controller/LiveTv/LiveTvServiceStatusInfo.cs +++ /dev/null @@ -1,54 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System.Collections.Generic; -using MediaBrowser.Model.LiveTv; - -namespace MediaBrowser.Controller.LiveTv -{ - public class LiveTvServiceStatusInfo - { - public LiveTvServiceStatusInfo() - { - Tuners = new List<LiveTvTunerInfo>(); - IsVisible = true; - } - - /// <summary> - /// Gets or sets the status. - /// </summary> - /// <value>The status.</value> - public LiveTvServiceStatus Status { get; set; } - - /// <summary> - /// Gets or sets the status message. - /// </summary> - /// <value>The status message.</value> - public string StatusMessage { get; set; } - - /// <summary> - /// Gets or sets the version. - /// </summary> - /// <value>The version.</value> - public string Version { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance has update available. - /// </summary> - /// <value><c>true</c> if this instance has update available; otherwise, <c>false</c>.</value> - public bool HasUpdateAvailable { get; set; } - - /// <summary> - /// Gets or sets the tuners. - /// </summary> - /// <value>The tuners.</value> - public List<LiveTvTunerInfo> Tuners { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is visible. - /// </summary> - /// <value><c>true</c> if this instance is visible; otherwise, <c>false</c>.</value> - public bool IsVisible { get; set; } - } -} diff --git a/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs b/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs deleted file mode 100644 index aa5eb59d1..000000000 --- a/MediaBrowser.Controller/LiveTv/LiveTvTunerInfo.cs +++ /dev/null @@ -1,77 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System.Collections.Generic; -using MediaBrowser.Model.LiveTv; - -namespace MediaBrowser.Controller.LiveTv -{ - public class LiveTvTunerInfo - { - public LiveTvTunerInfo() - { - Clients = new List<string>(); - } - - /// <summary> - /// Gets or sets the type of the source. - /// </summary> - /// <value>The type of the source.</value> - public string SourceType { get; set; } - - /// <summary> - /// Gets or sets the name. - /// </summary> - /// <value>The name.</value> - public string Name { get; set; } - - /// <summary> - /// Gets or sets the identifier. - /// </summary> - /// <value>The identifier.</value> - public string Id { get; set; } - - /// <summary> - /// Gets or sets the URL. - /// </summary> - /// <value>The URL.</value> - public string Url { get; set; } - - /// <summary> - /// Gets or sets the status. - /// </summary> - /// <value>The status.</value> - public LiveTvTunerStatus Status { get; set; } - - /// <summary> - /// Gets or sets the channel identifier. - /// </summary> - /// <value>The channel identifier.</value> - public string ChannelId { get; set; } - - /// <summary> - /// Gets or sets the recording identifier. - /// </summary> - /// <value>The recording identifier.</value> - public string RecordingId { get; set; } - - /// <summary> - /// Gets or sets the name of the program. - /// </summary> - /// <value>The name of the program.</value> - public string ProgramName { get; set; } - - /// <summary> - /// Gets or sets the clients. - /// </summary> - /// <value>The clients.</value> - public List<string> Clients { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance can reset. - /// </summary> - /// <value><c>true</c> if this instance can reset; otherwise, <c>false</c>.</value> - public bool CanReset { get; set; } - } -} diff --git a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs b/MediaBrowser.Controller/LiveTv/RecordingInfo.cs deleted file mode 100644 index 1dcf7a58f..000000000 --- a/MediaBrowser.Controller/LiveTv/RecordingInfo.cs +++ /dev/null @@ -1,210 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using MediaBrowser.Model.LiveTv; - -namespace MediaBrowser.Controller.LiveTv -{ - public class RecordingInfo - { - public RecordingInfo() - { - Genres = new List<string>(); - } - - /// <summary> - /// Gets or sets the id of the recording. - /// </summary> - public string Id { get; set; } - - /// <summary> - /// Gets or sets the series timer identifier. - /// </summary> - /// <value>The series timer identifier.</value> - public string SeriesTimerId { get; set; } - - /// <summary> - /// Gets or sets the timer identifier. - /// </summary> - /// <value>The timer identifier.</value> - public string TimerId { get; set; } - - /// <summary> - /// Gets or sets the channelId of the recording. - /// </summary> - public string ChannelId { get; set; } - - /// <summary> - /// Gets or sets the type of the channel. - /// </summary> - /// <value>The type of the channel.</value> - public ChannelType ChannelType { get; set; } - - /// <summary> - /// Gets or sets the name of the recording. - /// </summary> - public string Name { get; set; } - - /// <summary> - /// Gets or sets the path. - /// </summary> - /// <value>The path.</value> - public string Path { get; set; } - - /// <summary> - /// Gets or sets the URL. - /// </summary> - /// <value>The URL.</value> - public string Url { get; set; } - - /// <summary> - /// Gets or sets the overview. - /// </summary> - /// <value>The overview.</value> - public string Overview { get; set; } - - /// <summary> - /// Gets or sets the start date of the recording, in UTC. - /// </summary> - public DateTime StartDate { get; set; } - - /// <summary> - /// Gets or sets the end date of the recording, in UTC. - /// </summary> - public DateTime EndDate { get; set; } - - /// <summary> - /// Gets or sets the program identifier. - /// </summary> - /// <value>The program identifier.</value> - public string ProgramId { get; set; } - - /// <summary> - /// Gets or sets the status. - /// </summary> - /// <value>The status.</value> - public RecordingStatus Status { get; set; } - - /// <summary> - /// Gets or sets the genre of the program. - /// </summary> - public List<string> Genres { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is repeat. - /// </summary> - /// <value><c>true</c> if this instance is repeat; otherwise, <c>false</c>.</value> - public bool IsRepeat { get; set; } - - /// <summary> - /// Gets or sets the episode title. - /// </summary> - /// <value>The episode title.</value> - public string EpisodeTitle { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is hd. - /// </summary> - /// <value><c>true</c> if this instance is hd; otherwise, <c>false</c>.</value> - public bool? IsHD { get; set; } - - /// <summary> - /// Gets or sets the audio. - /// </summary> - /// <value>The audio.</value> - public ProgramAudio? Audio { get; set; } - - /// <summary> - /// Gets or sets the original air date. - /// </summary> - /// <value>The original air date.</value> - public DateTime? OriginalAirDate { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is movie. - /// </summary> - /// <value><c>true</c> if this instance is movie; otherwise, <c>false</c>.</value> - public bool IsMovie { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is sports. - /// </summary> - /// <value><c>true</c> if this instance is sports; otherwise, <c>false</c>.</value> - public bool IsSports { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is series. - /// </summary> - /// <value><c>true</c> if this instance is series; otherwise, <c>false</c>.</value> - public bool IsSeries { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is live. - /// </summary> - /// <value><c>true</c> if this instance is live; otherwise, <c>false</c>.</value> - public bool IsLive { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is news. - /// </summary> - /// <value><c>true</c> if this instance is news; otherwise, <c>false</c>.</value> - public bool IsNews { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is kids. - /// </summary> - /// <value><c>true</c> if this instance is kids; otherwise, <c>false</c>.</value> - public bool IsKids { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance is premiere. - /// </summary> - /// <value><c>true</c> if this instance is premiere; otherwise, <c>false</c>.</value> - public bool IsPremiere { get; set; } - - /// <summary> - /// Gets or sets the official rating. - /// </summary> - /// <value>The official rating.</value> - public string OfficialRating { get; set; } - - /// <summary> - /// Gets or sets the community rating. - /// </summary> - /// <value>The community rating.</value> - public float? CommunityRating { get; set; } - - /// <summary> - /// Gets or sets the image path if it can be accessed directly from the file system. - /// </summary> - /// <value>The image path.</value> - public string ImagePath { get; set; } - - /// <summary> - /// Gets or sets the image url if it can be downloaded. - /// </summary> - /// <value>The image URL.</value> - public string ImageUrl { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether this instance has image. - /// </summary> - /// <value><c>null</c> if [has image] contains no value, <c>true</c> if [has image]; otherwise, <c>false</c>.</value> - public bool? HasImage { get; set; } - - /// <summary> - /// Gets or sets the show identifier. - /// </summary> - /// <value>The show identifier.</value> - public string ShowId { get; set; } - - /// <summary> - /// Gets or sets the date last updated. - /// </summary> - /// <value>The date last updated.</value> - public DateTime DateLastUpdated { get; set; } - } -} diff --git a/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs b/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs deleted file mode 100644 index 0b943c939..000000000 --- a/MediaBrowser.Controller/LiveTv/RecordingStatusChangedEventArgs.cs +++ /dev/null @@ -1,16 +0,0 @@ -#nullable disable - -#pragma warning disable CS1591 - -using System; -using MediaBrowser.Model.LiveTv; - -namespace MediaBrowser.Controller.LiveTv -{ - public class RecordingStatusChangedEventArgs : EventArgs - { - public string RecordingId { get; set; } - - public RecordingStatus NewStatus { get; set; } - } -} diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 46fd1ae47..400e7f40f 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -1068,7 +1068,7 @@ namespace MediaBrowser.Controller.MediaEncoding } // hw transpose filters should be added manually. - args.Append(" -autorotate 0"); + args.Append(" -noautorotate"); return args.ToString().Trim(); } @@ -1159,7 +1159,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isSwDecoder = string.IsNullOrEmpty(GetHardwareVideoDecoder(state, options)); if (!isSwDecoder && _mediaEncoder.EncoderVersion >= new Version(4, 4)) { - arg.Append(" -autoscale 0"); + arg.Append(" -noautoscale"); } return arg.ToString(); @@ -3343,7 +3343,7 @@ namespace MediaBrowser.Controller.MediaEncoding // [0:s]scale=s=1280x720 var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); - overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } return (mainFilters, subFilters, overlayFilters); @@ -3520,7 +3520,7 @@ namespace MediaBrowser.Controller.MediaEncoding } subFilters.Add("hwupload=derive_device=cuda"); - overlayFilters.Add("overlay_cuda=eof_action=endall:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay_cuda=eof_action=pass:repeatlast=0"); } } else @@ -3529,7 +3529,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); - overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } } @@ -3718,7 +3718,7 @@ namespace MediaBrowser.Controller.MediaEncoding } subFilters.Add("hwupload=derive_device=opencl"); - overlayFilters.Add("overlay_opencl=eof_action=endall:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay_opencl=eof_action=pass:repeatlast=0"); overlayFilters.Add("hwmap=derive_device=d3d11va:reverse=1"); overlayFilters.Add("format=d3d11"); } @@ -3729,7 +3729,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); - overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } } @@ -3964,7 +3964,7 @@ namespace MediaBrowser.Controller.MediaEncoding : string.Empty; var overlayQsvFilter = string.Format( CultureInfo.InvariantCulture, - "overlay_qsv=eof_action=endall:shortest=1:repeatlast=0{0}", + "overlay_qsv=eof_action=pass:repeatlast=0{0}", overlaySize); overlayFilters.Add(overlayQsvFilter); } @@ -3975,7 +3975,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); - overlayFilters.Add("overlay=eof_action=endall:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } } @@ -4180,7 +4180,7 @@ namespace MediaBrowser.Controller.MediaEncoding : string.Empty; var overlayQsvFilter = string.Format( CultureInfo.InvariantCulture, - "overlay_qsv=eof_action=endall:shortest=1:repeatlast=0{0}", + "overlay_qsv=eof_action=pass:repeatlast=0{0}", overlaySize); overlayFilters.Add(overlayQsvFilter); } @@ -4191,7 +4191,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); - overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); } } @@ -4445,7 +4445,7 @@ namespace MediaBrowser.Controller.MediaEncoding : string.Empty; var overlayVaapiFilter = string.Format( CultureInfo.InvariantCulture, - "overlay_vaapi=eof_action=endall:shortest=1:repeatlast=0{0}", + "overlay_vaapi=eof_action=pass:repeatlast=0{0}", overlaySize); overlayFilters.Add(overlayVaapiFilter); } @@ -4456,7 +4456,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); - overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); if (isVaapiEncoder) { @@ -4616,7 +4616,7 @@ namespace MediaBrowser.Controller.MediaEncoding subFilters.Add("hwupload=derive_device=vulkan"); subFilters.Add("format=vulkan"); - overlayFilters.Add("overlay_vulkan=eof_action=endall:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay_vulkan=eof_action=pass:repeatlast=0"); if (isSwEncoder) { @@ -4817,7 +4817,7 @@ namespace MediaBrowser.Controller.MediaEncoding { var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); subFilters.Add(subSwScaleFilter); - overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0"); + overlayFilters.Add("overlay=eof_action=pass:repeatlast=0"); if (isVaapiEncoder) { diff --git a/MediaBrowser.Controller/MediaEncoding/ITranscodeManager.cs b/MediaBrowser.Controller/MediaEncoding/ITranscodeManager.cs new file mode 100644 index 000000000..c19a12ae7 --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/ITranscodeManager.cs @@ -0,0 +1,104 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Streaming; + +namespace MediaBrowser.Controller.MediaEncoding; + +/// <summary> +/// A service for managing media transcoding. +/// </summary> +public interface ITranscodeManager +{ + /// <summary> + /// Get transcoding job. + /// </summary> + /// <param name="playSessionId">Playback session id.</param> + /// <returns>The transcoding job.</returns> + public TranscodingJob? GetTranscodingJob(string playSessionId); + + /// <summary> + /// Get transcoding job. + /// </summary> + /// <param name="path">Path to the transcoding file.</param> + /// <param name="type">The <see cref="TranscodingJobType"/>.</param> + /// <returns>The transcoding job.</returns> + public TranscodingJob? GetTranscodingJob(string path, TranscodingJobType type); + + /// <summary> + /// Ping transcoding job. + /// </summary> + /// <param name="playSessionId">Play session id.</param> + /// <param name="isUserPaused">Is user paused.</param> + /// <exception cref="ArgumentNullException">Play session id is null.</exception> + public void PingTranscodingJob(string playSessionId, bool? isUserPaused); + + /// <summary> + /// Kills the single transcoding job. + /// </summary> + /// <param name="deviceId">The device id.</param> + /// <param name="playSessionId">The play session identifier.</param> + /// <param name="deleteFiles">The delete files.</param> + /// <returns>Task.</returns> + public Task KillTranscodingJobs(string deviceId, string? playSessionId, Func<string, bool> deleteFiles); + + /// <summary> + /// Report the transcoding progress to the session manager. + /// </summary> + /// <param name="job">The <see cref="TranscodingJob"/> of which the progress will be reported.</param> + /// <param name="state">The <see cref="StreamState"/> of the current transcoding job.</param> + /// <param name="transcodingPosition">The current transcoding position.</param> + /// <param name="framerate">The framerate of the transcoding job.</param> + /// <param name="percentComplete">The completion percentage of the transcode.</param> + /// <param name="bytesTranscoded">The number of bytes transcoded.</param> + /// <param name="bitRate">The bitrate of the transcoding job.</param> + public void ReportTranscodingProgress( + TranscodingJob job, + StreamState state, + TimeSpan? transcodingPosition, + float? framerate, + double? percentComplete, + long? bytesTranscoded, + int? bitRate); + + /// <summary> + /// Starts FFMpeg. + /// </summary> + /// <param name="state">The state.</param> + /// <param name="outputPath">The output path.</param> + /// <param name="commandLineArguments">The command line arguments for FFmpeg.</param> + /// <param name="userId">The user id.</param> + /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param> + /// <param name="cancellationTokenSource">The cancellation token source.</param> + /// <param name="workingDirectory">The working directory.</param> + /// <returns>Task.</returns> + public Task<TranscodingJob> StartFfMpeg( + StreamState state, + string outputPath, + string commandLineArguments, + Guid userId, + TranscodingJobType transcodingJobType, + CancellationTokenSource cancellationTokenSource, + string? workingDirectory = null); + + /// <summary> + /// Called when [transcode begin request]. + /// </summary> + /// <param name="path">The path.</param> + /// <param name="type">The type.</param> + /// <returns>The <see cref="TranscodingJob"/>.</returns> + public TranscodingJob? OnTranscodeBeginRequest(string path, TranscodingJobType type); + + /// <summary> + /// Called when [transcode end]. + /// </summary> + /// <param name="job">The transcode job.</param> + public void OnTranscodeEndRequest(TranscodingJob job); + + /// <summary> + /// Gets the transcoding lock. + /// </summary> + /// <param name="outputPath">The output path of the transcoded file.</param> + /// <returns>A <see cref="SemaphoreSlim"/>.</returns> + public SemaphoreSlim GetTranscodingLock(string outputPath); +} diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs index 480ddab09..1e6d5933c 100644 --- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingJobDto.cs +++ b/MediaBrowser.Controller/MediaEncoding/TranscodingJob.cs @@ -1,49 +1,39 @@ using System; using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Threading; -using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dto; using Microsoft.Extensions.Logging; -namespace Jellyfin.Api.Models.PlaybackDtos; +namespace MediaBrowser.Controller.MediaEncoding; /// <summary> /// Class TranscodingJob. /// </summary> -public class TranscodingJobDto : IDisposable +public sealed class TranscodingJob : IDisposable { - /// <summary> - /// The process lock. - /// </summary> - [SuppressMessage("Microsoft.Performance", "CA1051:NoVisibleInstanceFields", MessageId = "ProcessLock", Justification = "Imported from ServiceStack")] - [SuppressMessage("Microsoft.Performance", "SA1401:PrivateField", MessageId = "ProcessLock", Justification = "Imported from ServiceStack")] - public readonly object ProcessLock = new object(); + private readonly ILogger<TranscodingJob> _logger; + private readonly object _processLock = new(); + private readonly object _timerLock = new(); - /// <summary> - /// Timer lock. - /// </summary> - private readonly object _timerLock = new object(); + private Timer? _killTimer; /// <summary> - /// Initializes a new instance of the <see cref="TranscodingJobDto"/> class. + /// Initializes a new instance of the <see cref="TranscodingJob"/> class. /// </summary> /// <param name="logger">Instance of the <see cref="ILogger{TranscodingJobDto}"/> interface.</param> - public TranscodingJobDto(ILogger<TranscodingJobDto> logger) + public TranscodingJob(ILogger<TranscodingJob> logger) { - Logger = logger; + _logger = logger; } /// <summary> /// Gets or sets the play session identifier. /// </summary> - /// <value>The play session identifier.</value> public string? PlaySessionId { get; set; } /// <summary> /// Gets or sets the live stream identifier. /// </summary> - /// <value>The live stream identifier.</value> public string? LiveStreamId { get; set; } /// <summary> @@ -54,7 +44,6 @@ public class TranscodingJobDto : IDisposable /// <summary> /// Gets or sets the path. /// </summary> - /// <value>The path.</value> public MediaSourceInfo? MediaSource { get; set; } /// <summary> @@ -65,33 +54,19 @@ public class TranscodingJobDto : IDisposable /// <summary> /// Gets or sets the type. /// </summary> - /// <value>The type.</value> public TranscodingJobType Type { get; set; } /// <summary> /// Gets or sets the process. /// </summary> - /// <value>The process.</value> public Process? Process { get; set; } /// <summary> - /// Gets logger. - /// </summary> - public ILogger<TranscodingJobDto> Logger { get; private set; } - - /// <summary> /// Gets or sets the active request count. /// </summary> - /// <value>The active request count.</value> public int ActiveRequestCount { get; set; } /// <summary> - /// Gets or sets the kill timer. - /// </summary> - /// <value>The kill timer.</value> - private Timer? KillTimer { get; set; } - - /// <summary> /// Gets or sets device id. /// </summary> public string? DeviceId { get; set; } @@ -178,7 +153,7 @@ public class TranscodingJobDto : IDisposable { lock (_timerLock) { - KillTimer?.Change(Timeout.Infinite, Timeout.Infinite); + _killTimer?.Change(Timeout.Infinite, Timeout.Infinite); } } @@ -189,10 +164,10 @@ public class TranscodingJobDto : IDisposable { lock (_timerLock) { - if (KillTimer is not null) + if (_killTimer is not null) { - KillTimer.Dispose(); - KillTimer = null; + _killTimer.Dispose(); + _killTimer = null; } } } @@ -220,15 +195,15 @@ public class TranscodingJobDto : IDisposable lock (_timerLock) { - if (KillTimer is null) + if (_killTimer is null) { - Logger.LogDebug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); - KillTimer = new Timer(new TimerCallback(callback), this, intervalMs, Timeout.Infinite); + _logger.LogDebug("Starting kill timer at {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); + _killTimer = new Timer(new TimerCallback(callback), this, intervalMs, Timeout.Infinite); } else { - Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); - KillTimer.Change(intervalMs, Timeout.Infinite); + _logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); + _killTimer.Change(intervalMs, Timeout.Infinite); } } } @@ -245,39 +220,61 @@ public class TranscodingJobDto : IDisposable lock (_timerLock) { - if (KillTimer is not null) + if (_killTimer is not null) { var intervalMs = PingTimeout; - Logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); - KillTimer.Change(intervalMs, Timeout.Infinite); + _logger.LogDebug("Changing kill timer to {0}ms. JobId {1} PlaySessionId {2}", intervalMs, Id, PlaySessionId); + _killTimer.Change(intervalMs, Timeout.Infinite); } } } - /// <inheritdoc /> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - /// <summary> - /// Dispose all resources. + /// Stops the transcoding job. /// </summary> - /// <param name="disposing">Whether to dispose all resources.</param> - protected virtual void Dispose(bool disposing) + public void Stop() { - if (disposing) + lock (_processLock) { - Process?.Dispose(); - Process = null; - KillTimer?.Dispose(); - KillTimer = null; - CancellationTokenSource?.Dispose(); - CancellationTokenSource = null; - TranscodingThrottler?.Dispose(); - TranscodingThrottler = null; +#pragma warning disable CA1849 // Can't await in lock block + TranscodingThrottler?.Stop().GetAwaiter().GetResult(); + + var process = Process; + + if (!HasExited) + { + try + { + _logger.LogInformation("Stopping ffmpeg process with q command for {Path}", Path); + + process!.StandardInput.WriteLine("q"); + + // Need to wait because killing is asynchronous. + if (!process.WaitForExit(5000)) + { + _logger.LogInformation("Killing FFmpeg process for {Path}", Path); + process.Kill(); + } + } + catch (InvalidOperationException) + { + } + } +#pragma warning restore CA1849 } } + + /// <inheritdoc /> + public void Dispose() + { + Process?.Dispose(); + Process = null; + _killTimer?.Dispose(); + _killTimer = null; + CancellationTokenSource?.Dispose(); + CancellationTokenSource = null; + TranscodingThrottler?.Dispose(); + TranscodingThrottler = null; + } } diff --git a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs b/MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs index b577c4ea6..813f13eae 100644 --- a/Jellyfin.Api/Models/PlaybackDtos/TranscodingThrottler.cs +++ b/MediaBrowser.Controller/MediaEncoding/TranscodingThrottler.cs @@ -2,19 +2,18 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; -using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; -namespace Jellyfin.Api.Models.PlaybackDtos; +namespace MediaBrowser.Controller.MediaEncoding; /// <summary> /// Transcoding throttler. /// </summary> public class TranscodingThrottler : IDisposable { - private readonly TranscodingJobDto _job; + private readonly TranscodingJob _job; private readonly ILogger<TranscodingThrottler> _logger; private readonly IConfigurationManager _config; private readonly IFileSystem _fileSystem; @@ -30,7 +29,7 @@ public class TranscodingThrottler : IDisposable /// <param name="config">Instance of the <see cref="IConfigurationManager"/> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> - public TranscodingThrottler(TranscodingJobDto job, ILogger<TranscodingThrottler> logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder) + public TranscodingThrottler(TranscodingJob job, ILogger<TranscodingThrottler> logger, IConfigurationManager config, IFileSystem fileSystem, IMediaEncoder mediaEncoder) { _job = job; _logger = logger; @@ -146,7 +145,7 @@ public class TranscodingThrottler : IDisposable } } - private bool IsThrottleAllowed(TranscodingJobDto job, int thresholdSeconds) + private bool IsThrottleAllowed(TranscodingJob job, int thresholdSeconds) { var bytesDownloaded = job.BytesDownloaded; var transcodingPositionTicks = job.TranscodingPositionTicks ?? 0; diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs index 53df7133b..5a47236f9 100644 --- a/MediaBrowser.Controller/Session/ISessionManager.cs +++ b/MediaBrowser.Controller/Session/ISessionManager.cs @@ -111,7 +111,8 @@ namespace MediaBrowser.Controller.Session /// Reports the session ended. /// </summary> /// <param name="sessionId">The session identifier.</param> - void ReportSessionEnded(string sessionId); + /// <returns>Task.</returns> + ValueTask ReportSessionEnded(string sessionId); /// <summary> /// Sends the general command. diff --git a/MediaBrowser.Controller/Session/SessionInfo.cs b/MediaBrowser.Controller/Session/SessionInfo.cs index 3e30c8dc4..3a12a56f1 100644 --- a/MediaBrowser.Controller/Session/SessionInfo.cs +++ b/MediaBrowser.Controller/Session/SessionInfo.cs @@ -19,7 +19,7 @@ namespace MediaBrowser.Controller.Session /// <summary> /// Class SessionInfo. /// </summary> - public sealed class SessionInfo : IAsyncDisposable, IDisposable + public sealed class SessionInfo : IAsyncDisposable { // 1 second private const long ProgressIncrement = 10000000; @@ -374,26 +374,6 @@ namespace MediaBrowser.Controller.Session } } - /// <inheritdoc /> - public void Dispose() - { - _disposed = true; - - StopAutomaticProgress(); - - var controllers = SessionControllers.ToList(); - SessionControllers = Array.Empty<ISessionController>(); - - foreach (var controller in controllers) - { - if (controller is IDisposable disposable) - { - _logger.LogDebug("Disposing session controller synchronously {TypeName}", disposable.GetType().Name); - disposable.Dispose(); - } - } - } - public async ValueTask DisposeAsync() { _disposed = true; @@ -401,6 +381,7 @@ namespace MediaBrowser.Controller.Session StopAutomaticProgress(); var controllers = SessionControllers.ToList(); + SessionControllers = Array.Empty<ISessionController>(); foreach (var controller in controllers) { @@ -409,6 +390,11 @@ namespace MediaBrowser.Controller.Session _logger.LogDebug("Disposing session controller asynchronously {TypeName}", disposableAsync.GetType().Name); await disposableAsync.DisposeAsync().ConfigureAwait(false); } + else if (controller is IDisposable disposable) + { + _logger.LogDebug("Disposing session controller synchronously {TypeName}", disposable.GetType().Name); + disposable.Dispose(); + } } } } diff --git a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs b/MediaBrowser.Controller/Streaming/ProgressiveFileStream.cs index d7b1c9f8b..f44dc92d7 100644 --- a/Jellyfin.Api/Helpers/ProgressiveFileStream.cs +++ b/MediaBrowser.Controller/Streaming/ProgressiveFileStream.cs @@ -3,10 +3,10 @@ using System.Diagnostics; using System.IO; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Api.Models.PlaybackDtos; +using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.IO; -namespace Jellyfin.Api.Helpers; +namespace MediaBrowser.Controller.Streaming; /// <summary> /// A progressive file stream for transferring transcoded files as they are written to. @@ -14,8 +14,8 @@ namespace Jellyfin.Api.Helpers; public class ProgressiveFileStream : Stream { private readonly Stream _stream; - private readonly TranscodingJobDto? _job; - private readonly TranscodingJobHelper? _transcodingJobHelper; + private readonly TranscodingJob? _job; + private readonly ITranscodeManager? _transcodeManager; private readonly int _timeoutMs; private bool _disposed; @@ -24,12 +24,12 @@ public class ProgressiveFileStream : Stream /// </summary> /// <param name="filePath">The path to the transcoded file.</param> /// <param name="job">The transcoding job information.</param> - /// <param name="transcodingJobHelper">The transcoding job helper.</param> + /// <param name="transcodeManager">The transcode manager.</param> /// <param name="timeoutMs">The timeout duration in milliseconds.</param> - public ProgressiveFileStream(string filePath, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, int timeoutMs = 30000) + public ProgressiveFileStream(string filePath, TranscodingJob? job, ITranscodeManager transcodeManager, int timeoutMs = 30000) { _job = job; - _transcodingJobHelper = transcodingJobHelper; + _transcodeManager = transcodeManager; _timeoutMs = timeoutMs; _stream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan); @@ -43,7 +43,7 @@ public class ProgressiveFileStream : Stream public ProgressiveFileStream(Stream stream, int timeoutMs = 30000) { _job = null; - _transcodingJobHelper = null; + _transcodeManager = null; _timeoutMs = timeoutMs; _stream = stream; } @@ -153,7 +153,7 @@ public class ProgressiveFileStream : Stream if (_job is not null) { - _transcodingJobHelper?.OnTranscodeEndRequest(_job); + _transcodeManager?.OnTranscodeEndRequest(_job); } } } diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs b/MediaBrowser.Controller/Streaming/StreamState.cs index cc1f9163e..b5dbe29ec 100644 --- a/Jellyfin.Api/Models/StreamingDtos/StreamState.cs +++ b/MediaBrowser.Controller/Streaming/StreamState.cs @@ -1,11 +1,9 @@ using System; -using Jellyfin.Api.Helpers; -using Jellyfin.Api.Models.PlaybackDtos; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Model.Dlna; -namespace Jellyfin.Api.Models.StreamingDtos; +namespace MediaBrowser.Controller.Streaming; /// <summary> /// The stream state dto. @@ -13,7 +11,7 @@ namespace Jellyfin.Api.Models.StreamingDtos; public class StreamState : EncodingJobInfo, IDisposable { private readonly IMediaSourceManager _mediaSourceManager; - private readonly TranscodingJobHelper _transcodingJobHelper; + private readonly ITranscodeManager _transcodeManager; private bool _disposed; /// <summary> @@ -21,12 +19,12 @@ public class StreamState : EncodingJobInfo, IDisposable /// </summary> /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager" /> interface.</param> /// <param name="transcodingType">The <see cref="TranscodingJobType" />.</param> - /// <param name="transcodingJobHelper">The <see cref="TranscodingJobHelper" /> singleton.</param> - public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType, TranscodingJobHelper transcodingJobHelper) + /// <param name="transcodeManager">The <see cref="ITranscodeManager" /> singleton.</param> + public StreamState(IMediaSourceManager mediaSourceManager, TranscodingJobType transcodingType, ITranscodeManager transcodeManager) : base(transcodingType) { _mediaSourceManager = mediaSourceManager; - _transcodingJobHelper = transcodingJobHelper; + _transcodeManager = transcodeManager; } /// <summary> @@ -141,7 +139,7 @@ public class StreamState : EncodingJobInfo, IDisposable /// <summary> /// Gets or sets the transcoding job. /// </summary> - public TranscodingJobDto? TranscodingJob { get; set; } + public TranscodingJob? TranscodingJob { get; set; } /// <inheritdoc /> public void Dispose() @@ -153,7 +151,7 @@ public class StreamState : EncodingJobInfo, IDisposable /// <inheritdoc /> public override void ReportTranscodingProgress(TimeSpan? transcodingPosition, float? framerate, double? percentComplete, long? bytesTranscoded, int? bitRate) { - _transcodingJobHelper.ReportTranscodingProgress(TranscodingJob!, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate); + _transcodeManager.ReportTranscodingProgress(TranscodingJob!, this, transcodingPosition, framerate, percentComplete, bytesTranscoded, bitRate); } /// <summary> diff --git a/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs b/MediaBrowser.Controller/Streaming/StreamingRequestDto.cs index a357498d4..e47ef65f0 100644 --- a/Jellyfin.Api/Models/StreamingDtos/StreamingRequestDto.cs +++ b/MediaBrowser.Controller/Streaming/StreamingRequestDto.cs @@ -1,6 +1,6 @@ using MediaBrowser.Controller.MediaEncoding; -namespace Jellyfin.Api.Models.StreamingDtos; +namespace MediaBrowser.Controller.Streaming; /// <summary> /// The audio streaming request dto. diff --git a/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs b/MediaBrowser.Controller/Streaming/VideoRequestDto.cs index 8548fec1a..44dc831fd 100644 --- a/Jellyfin.Api/Models/StreamingDtos/VideoRequestDto.cs +++ b/MediaBrowser.Controller/Streaming/VideoRequestDto.cs @@ -1,4 +1,4 @@ -namespace Jellyfin.Api.Models.StreamingDtos; +namespace MediaBrowser.Controller.Streaming; /// <summary> /// The video request dto. diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs index 77d3edbd6..d79e4441a 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/MediaBrowser.MediaEncoding/Transcoding/TranscodeManager.cs @@ -8,10 +8,8 @@ using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Api.Extensions; -using Jellyfin.Api.Models.PlaybackDtos; -using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; @@ -19,94 +17,78 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Session; +using MediaBrowser.Controller.Streaming; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Session; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -namespace Jellyfin.Api.Helpers; +namespace MediaBrowser.MediaEncoding.Transcoding; -/// <summary> -/// Transcoding job helpers. -/// </summary> -public class TranscodingJobHelper : IDisposable +/// <inheritdoc cref="ITranscodeManager"/> +public sealed class TranscodeManager : ITranscodeManager, IDisposable { - /// <summary> - /// The active transcoding jobs. - /// </summary> - private static readonly List<TranscodingJobDto> _activeTranscodingJobs = new List<TranscodingJobDto>(); - - /// <summary> - /// The transcoding locks. - /// </summary> - private static readonly Dictionary<string, SemaphoreSlim> _transcodingLocks = new Dictionary<string, SemaphoreSlim>(); - - private readonly IAttachmentExtractor _attachmentExtractor; + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger<TranscodeManager> _logger; + private readonly IFileSystem _fileSystem; private readonly IApplicationPaths _appPaths; + private readonly IServerConfigurationManager _serverConfigurationManager; + private readonly IUserManager _userManager; + private readonly ISessionManager _sessionManager; private readonly EncodingHelper _encodingHelper; - private readonly IFileSystem _fileSystem; - private readonly ILogger<TranscodingJobHelper> _logger; private readonly IMediaEncoder _mediaEncoder; private readonly IMediaSourceManager _mediaSourceManager; - private readonly IServerConfigurationManager _serverConfigurationManager; - private readonly ISessionManager _sessionManager; - private readonly ILoggerFactory _loggerFactory; - private readonly IUserManager _userManager; + private readonly IAttachmentExtractor _attachmentExtractor; + + private readonly List<TranscodingJob> _activeTranscodingJobs = new(); + private readonly Dictionary<string, SemaphoreSlim> _transcodingLocks = new(); /// <summary> - /// Initializes a new instance of the <see cref="TranscodingJobHelper"/> class. + /// Initializes a new instance of the <see cref="TranscodeManager"/> class. /// </summary> - /// <param name="attachmentExtractor">Instance of the <see cref="IAttachmentExtractor"/> interface.</param> - /// <param name="appPaths">Instance of the <see cref="IApplicationPaths"/> interface.</param> - /// <param name="logger">Instance of the <see cref="ILogger{TranscodingJobHelpers}"/> interface.</param> - /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> - /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> - /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> - /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> - /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param> - /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> - /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param> - /// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param> - public TranscodingJobHelper( - IAttachmentExtractor attachmentExtractor, - IApplicationPaths appPaths, - ILogger<TranscodingJobHelper> logger, - IMediaSourceManager mediaSourceManager, + /// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param> + /// <param name="fileSystem">The <see cref="IFileSystem"/>.</param> + /// <param name="appPaths">The <see cref="IApplicationPaths"/>.</param> + /// <param name="serverConfigurationManager">The <see cref="IServerConfigurationManager"/>.</param> + /// <param name="userManager">The <see cref="IUserManager"/>.</param> + /// <param name="sessionManager">The <see cref="ISessionManager"/>.</param> + /// <param name="encodingHelper">The <see cref="EncodingHelper"/>.</param> + /// <param name="mediaEncoder">The <see cref="IMediaEncoder"/>.</param> + /// <param name="mediaSourceManager">The <see cref="IMediaSourceManager"/>.</param> + /// <param name="attachmentExtractor">The <see cref="IAttachmentExtractor"/>.</param> + public TranscodeManager( + ILoggerFactory loggerFactory, IFileSystem fileSystem, - IMediaEncoder mediaEncoder, + IApplicationPaths appPaths, IServerConfigurationManager serverConfigurationManager, + IUserManager userManager, ISessionManager sessionManager, EncodingHelper encodingHelper, - ILoggerFactory loggerFactory, - IUserManager userManager) + IMediaEncoder mediaEncoder, + IMediaSourceManager mediaSourceManager, + IAttachmentExtractor attachmentExtractor) { - _attachmentExtractor = attachmentExtractor; - _appPaths = appPaths; - _logger = logger; - _mediaSourceManager = mediaSourceManager; + _loggerFactory = loggerFactory; _fileSystem = fileSystem; - _mediaEncoder = mediaEncoder; + _appPaths = appPaths; _serverConfigurationManager = serverConfigurationManager; + _userManager = userManager; _sessionManager = sessionManager; _encodingHelper = encodingHelper; - _loggerFactory = loggerFactory; - _userManager = userManager; + _mediaEncoder = mediaEncoder; + _mediaSourceManager = mediaSourceManager; + _attachmentExtractor = attachmentExtractor; + _logger = loggerFactory.CreateLogger<TranscodeManager>(); DeleteEncodedMediaCache(); - - sessionManager.PlaybackProgress += OnPlaybackProgress; - sessionManager.PlaybackStart += OnPlaybackProgress; + _sessionManager.PlaybackProgress += OnPlaybackProgress; + _sessionManager.PlaybackStart += OnPlaybackProgress; } - /// <summary> - /// Get transcoding job. - /// </summary> - /// <param name="playSessionId">Playback session id.</param> - /// <returns>The transcoding job.</returns> - public TranscodingJobDto? GetTranscodingJob(string playSessionId) + /// <inheritdoc /> + public TranscodingJob? GetTranscodingJob(string playSessionId) { lock (_activeTranscodingJobs) { @@ -114,13 +96,8 @@ public class TranscodingJobHelper : IDisposable } } - /// <summary> - /// Get transcoding job. - /// </summary> - /// <param name="path">Path to the transcoding file.</param> - /// <param name="type">The <see cref="TranscodingJobType"/>.</param> - /// <returns>The transcoding job.</returns> - public TranscodingJobDto? GetTranscodingJob(string path, TranscodingJobType type) + /// <inheritdoc /> + public TranscodingJob? GetTranscodingJob(string path, TranscodingJobType type) { lock (_activeTranscodingJobs) { @@ -128,19 +105,14 @@ public class TranscodingJobHelper : IDisposable } } - /// <summary> - /// Ping transcoding job. - /// </summary> - /// <param name="playSessionId">Play session id.</param> - /// <param name="isUserPaused">Is user paused.</param> - /// <exception cref="ArgumentNullException">Play session id is null.</exception> + /// <inheritdoc /> public void PingTranscodingJob(string playSessionId, bool? isUserPaused) { ArgumentException.ThrowIfNullOrEmpty(playSessionId); _logger.LogDebug("PingTranscodingJob PlaySessionId={0} isUsedPaused: {1}", playSessionId, isUserPaused); - List<TranscodingJobDto> jobs; + List<TranscodingJob> jobs; lock (_activeTranscodingJobs) { @@ -161,7 +133,7 @@ public class TranscodingJobHelper : IDisposable } } - private void PingTimer(TranscodingJobDto job, bool isProgressCheckIn) + private void PingTimer(TranscodingJob job, bool isProgressCheckIn) { if (job.HasExited) { @@ -190,13 +162,9 @@ public class TranscodingJobHelper : IDisposable } } - /// <summary> - /// Called when [transcode kill timer stopped]. - /// </summary> - /// <param name="state">The state.</param> private async void OnTranscodeKillTimerStopped(object? state) { - var job = state as TranscodingJobDto ?? throw new ArgumentException($"{nameof(state)} is not of type {nameof(TranscodingJobDto)}", nameof(state)); + var job = state as TranscodingJob ?? throw new ArgumentException($"{nameof(state)} is not of type {nameof(TranscodingJob)}", nameof(state)); if (!job.HasExited && job.Type != TranscodingJobType.Progressive) { var timeSinceLastPing = (DateTime.UtcNow - job.LastPingDate).TotalMilliseconds; @@ -213,43 +181,21 @@ public class TranscodingJobHelper : IDisposable await KillTranscodingJob(job, true, path => true).ConfigureAwait(false); } - /// <summary> - /// Kills the single transcoding job. - /// </summary> - /// <param name="deviceId">The device id.</param> - /// <param name="playSessionId">The play session identifier.</param> - /// <param name="deleteFiles">The delete files.</param> - /// <returns>Task.</returns> + /// <inheritdoc /> public Task KillTranscodingJobs(string deviceId, string? playSessionId, Func<string, bool> deleteFiles) { - return KillTranscodingJobs( - j => string.IsNullOrWhiteSpace(playSessionId) - ? string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase) - : string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase), - deleteFiles); - } - - /// <summary> - /// Kills the transcoding jobs. - /// </summary> - /// <param name="killJob">The kill job.</param> - /// <param name="deleteFiles">The delete files.</param> - /// <returns>Task.</returns> - private Task KillTranscodingJobs(Func<TranscodingJobDto, bool> killJob, Func<string, bool> deleteFiles) - { - var jobs = new List<TranscodingJobDto>(); + var jobs = new List<TranscodingJob>(); lock (_activeTranscodingJobs) { // This is really only needed for HLS. // Progressive streams can stop on their own reliably. - jobs.AddRange(_activeTranscodingJobs.Where(killJob)); + jobs.AddRange(_activeTranscodingJobs.Where(j => string.IsNullOrWhiteSpace(playSessionId) + ? string.Equals(deviceId, j.DeviceId, StringComparison.OrdinalIgnoreCase) + : string.Equals(playSessionId, j.PlaySessionId, StringComparison.OrdinalIgnoreCase))); } - if (jobs.Count == 0) - { - return Task.CompletedTask; - } + return Task.WhenAll(GetKillJobs()); IEnumerable<Task> GetKillJobs() { @@ -258,17 +204,9 @@ public class TranscodingJobHelper : IDisposable yield return KillTranscodingJob(job, false, deleteFiles); } } - - return Task.WhenAll(GetKillJobs()); } - /// <summary> - /// Kills the transcoding job. - /// </summary> - /// <param name="job">The job.</param> - /// <param name="closeLiveStream">if set to <c>true</c> [close live stream].</param> - /// <param name="delete">The delete.</param> - private async Task KillTranscodingJob(TranscodingJobDto job, bool closeLiveStream, Func<string, bool> delete) + private async Task KillTranscodingJob(TranscodingJob job, bool closeLiveStream, Func<string, bool> delete) { job.DisposeKillTimer(); @@ -282,6 +220,7 @@ public class TranscodingJobHelper : IDisposable { #pragma warning disable CA1849 // Can't await in lock block job.CancellationTokenSource.Cancel(); +#pragma warning restore CA1849 } } @@ -290,35 +229,7 @@ public class TranscodingJobHelper : IDisposable _transcodingLocks.Remove(job.Path!); } - lock (job.ProcessLock!) - { - job.TranscodingThrottler?.Stop().GetAwaiter().GetResult(); - - var process = job.Process; - - var hasExited = job.HasExited; - - if (!hasExited) - { - try - { - _logger.LogInformation("Stopping ffmpeg process with q command for {Path}", job.Path); - - process!.StandardInput.WriteLine("q"); - - // Need to wait because killing is asynchronous. - if (!process.WaitForExit(5000)) - { - _logger.LogInformation("Killing FFmpeg process for {Path}", job.Path); - process.Kill(); - } - } - catch (InvalidOperationException) - { - } - } -#pragma warning restore CA1849 - } + job.Stop(); if (delete(job.Path!)) { @@ -381,10 +292,6 @@ public class TranscodingJobHelper : IDisposable } } - /// <summary> - /// Deletes the progressive partial stream files. - /// </summary> - /// <param name="outputFilePath">The output file path.</param> private void DeleteProgressivePartialStreamFiles(string outputFilePath) { if (File.Exists(outputFilePath)) @@ -393,10 +300,6 @@ public class TranscodingJobHelper : IDisposable } } - /// <summary> - /// Deletes the HLS partial stream files. - /// </summary> - /// <param name="outputFilePath">The output file path.</param> private void DeleteHlsPartialStreamFiles(string outputFilePath) { var directory = Path.GetDirectoryName(outputFilePath) @@ -428,18 +331,9 @@ public class TranscodingJobHelper : IDisposable } } - /// <summary> - /// Report the transcoding progress to the session manager. - /// </summary> - /// <param name="job">The <see cref="TranscodingJobDto"/> of which the progress will be reported.</param> - /// <param name="state">The <see cref="StreamState"/> of the current transcoding job.</param> - /// <param name="transcodingPosition">The current transcoding position.</param> - /// <param name="framerate">The framerate of the transcoding job.</param> - /// <param name="percentComplete">The completion percentage of the transcode.</param> - /// <param name="bytesTranscoded">The number of bytes transcoded.</param> - /// <param name="bitRate">The bitrate of the transcoding job.</param> + /// <inheritdoc /> public void ReportTranscodingProgress( - TranscodingJobDto job, + TranscodingJob job, StreamState state, TimeSpan? transcodingPosition, float? framerate, @@ -490,22 +384,12 @@ public class TranscodingJobHelper : IDisposable } } - /// <summary> - /// Starts FFmpeg. - /// </summary> - /// <param name="state">The state.</param> - /// <param name="outputPath">The output path.</param> - /// <param name="commandLineArguments">The command line arguments for FFmpeg.</param> - /// <param name="request">The <see cref="HttpRequest"/>.</param> - /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param> - /// <param name="cancellationTokenSource">The cancellation token source.</param> - /// <param name="workingDirectory">The working directory.</param> - /// <returns>Task.</returns> - public async Task<TranscodingJobDto> StartFfMpeg( + /// <inheritdoc /> + public async Task<TranscodingJob> StartFfMpeg( StreamState state, string outputPath, string commandLineArguments, - HttpRequest request, + Guid userId, TranscodingJobType transcodingJobType, CancellationTokenSource cancellationTokenSource, string? workingDirectory = null) @@ -517,8 +401,7 @@ public class TranscodingJobHelper : IDisposable if (state.VideoRequest is not null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec)) { - var userId = request.HttpContext.User.GetUserId(); - var user = userId.Equals(default) ? null : _userManager.GetUserById(userId); + var user = userId.IsEmpty() ? null : _userManager.GetUserById(userId); if (user is not null && !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)) { this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state); @@ -595,13 +478,26 @@ public class TranscodingJobHelper : IDisposable $"{logFilePrefix}{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{state.Request.MediaSourceId}_{Guid.NewGuid().ToString()[..8]}.log"); // FFmpeg writes debug/error info to stderr. This is useful when debugging so let's put it in the log directory. - Stream logStream = new FileStream(logFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); + Stream logStream = new FileStream( + logFilePath, + FileMode.Create, + FileAccess.Write, + FileShare.Read, + IODefaults.FileStreamBufferSize, + FileOptions.Asynchronous); var commandLineLogMessage = process.StartInfo.FileName + " " + process.StartInfo.Arguments; - var commandLineLogMessageBytes = Encoding.UTF8.GetBytes(request.Path + Environment.NewLine + Environment.NewLine + JsonSerializer.Serialize(state.MediaSource) + Environment.NewLine + Environment.NewLine + commandLineLogMessage + Environment.NewLine + Environment.NewLine); + var commandLineLogMessageBytes = Encoding.UTF8.GetBytes( + JsonSerializer.Serialize(state.MediaSource) + + Environment.NewLine + + Environment.NewLine + + commandLineLogMessage + + Environment.NewLine + + Environment.NewLine); + await logStream.WriteAsync(commandLineLogMessageBytes, cancellationTokenSource.Token).ConfigureAwait(false); - process.Exited += (sender, args) => OnFfMpegProcessExited(process, transcodingJob, state); + process.Exited += (_, _) => OnFfMpegProcessExited(process, transcodingJob, state); try { @@ -610,7 +506,6 @@ public class TranscodingJobHelper : IDisposable catch (Exception ex) { _logger.LogError(ex, "Error starting FFmpeg"); - this.OnTranscodeFailedToStart(outputPath, transcodingJobType, state); throw; @@ -656,7 +551,7 @@ public class TranscodingJobHelper : IDisposable return transcodingJob; } - private void StartThrottler(StreamState state, TranscodingJobDto transcodingJob) + private void StartThrottler(StreamState state, TranscodingJob transcodingJob) { if (EnableThrottling(state)) { @@ -665,31 +560,14 @@ public class TranscodingJobHelper : IDisposable } } - private bool EnableThrottling(StreamState state) - { - var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); - - return state.InputProtocol == MediaProtocol.File && - state.RunTimeTicks.HasValue && - state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks && - state.IsInputVideo && - state.VideoType == VideoType.VideoFile; - } + private static bool EnableThrottling(StreamState state) + => state.InputProtocol == MediaProtocol.File + && state.RunTimeTicks.HasValue + && state.RunTimeTicks.Value >= TimeSpan.FromMinutes(5).Ticks + && state.IsInputVideo + && state.VideoType == VideoType.VideoFile; - /// <summary> - /// Called when [transcode beginning]. - /// </summary> - /// <param name="path">The path.</param> - /// <param name="playSessionId">The play session identifier.</param> - /// <param name="liveStreamId">The live stream identifier.</param> - /// <param name="transcodingJobId">The transcoding job identifier.</param> - /// <param name="type">The type.</param> - /// <param name="process">The process.</param> - /// <param name="deviceId">The device id.</param> - /// <param name="state">The state.</param> - /// <param name="cancellationTokenSource">The cancellation token source.</param> - /// <returns>TranscodingJob.</returns> - public TranscodingJobDto OnTranscodeBeginning( + private TranscodingJob OnTranscodeBeginning( string path, string? playSessionId, string? liveStreamId, @@ -702,7 +580,7 @@ public class TranscodingJobHelper : IDisposable { lock (_activeTranscodingJobs) { - var job = new TranscodingJobDto(_loggerFactory.CreateLogger<TranscodingJobDto>()) + var job = new TranscodingJob(_loggerFactory.CreateLogger<TranscodingJob>()) { Type = type, Path = path, @@ -724,11 +602,8 @@ public class TranscodingJobHelper : IDisposable } } - /// <summary> - /// Called when [transcode end]. - /// </summary> - /// <param name="job">The transcode job.</param> - public void OnTranscodeEndRequest(TranscodingJobDto job) + /// <inheritdoc /> + public void OnTranscodeEndRequest(TranscodingJob job) { job.ActiveRequestCount--; _logger.LogDebug("OnTranscodeEndRequest job.ActiveRequestCount={ActiveRequestCount}", job.ActiveRequestCount); @@ -738,16 +613,7 @@ public class TranscodingJobHelper : IDisposable } } - /// <summary> - /// <summary> - /// The progressive - /// </summary> - /// Called when [transcode failed to start]. - /// </summary> - /// <param name="path">The path.</param> - /// <param name="type">The type.</param> - /// <param name="state">The state.</param> - public void OnTranscodeFailedToStart(string path, TranscodingJobType type, StreamState state) + private void OnTranscodeFailedToStart(string path, TranscodingJobType type, StreamState state) { lock (_activeTranscodingJobs) { @@ -770,13 +636,7 @@ public class TranscodingJobHelper : IDisposable } } - /// <summary> - /// Processes the exited. - /// </summary> - /// <param name="process">The process.</param> - /// <param name="job">The job.</param> - /// <param name="state">The state.</param> - private void OnFfMpegProcessExited(Process process, TranscodingJobDto job, StreamState state) + private void OnFfMpegProcessExited(Process process, TranscodingJob job, StreamState state) { job.HasExited = true; job.ExitCode = process.ExitCode; @@ -822,44 +682,30 @@ public class TranscodingJobHelper : IDisposable } } - /// <summary> - /// Called when [transcode begin request]. - /// </summary> - /// <param name="path">The path.</param> - /// <param name="type">The type.</param> - /// <returns>The <see cref="TranscodingJobDto"/>.</returns> - public TranscodingJobDto? OnTranscodeBeginRequest(string path, TranscodingJobType type) + /// <inheritdoc /> + public TranscodingJob? OnTranscodeBeginRequest(string path, TranscodingJobType type) { lock (_activeTranscodingJobs) { - var job = _activeTranscodingJobs.FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase)); + var job = _activeTranscodingJobs + .FirstOrDefault(j => j.Type == type && string.Equals(j.Path, path, StringComparison.OrdinalIgnoreCase)); if (job is null) { return null; } - OnTranscodeBeginRequest(job); + job.ActiveRequestCount++; + if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive) + { + job.StopKillTimer(); + } return job; } } - private void OnTranscodeBeginRequest(TranscodingJobDto job) - { - job.ActiveRequestCount++; - - if (string.IsNullOrWhiteSpace(job.PlaySessionId) || job.Type == TranscodingJobType.Progressive) - { - job.StopKillTimer(); - } - } - - /// <summary> - /// Gets the transcoding lock. - /// </summary> - /// <param name="outputPath">The output path of the transcoded file.</param> - /// <returns>A <see cref="SemaphoreSlim"/>.</returns> + /// <inheritdoc /> public SemaphoreSlim GetTranscodingLock(string outputPath) { lock (_transcodingLocks) @@ -882,9 +728,6 @@ public class TranscodingJobHelper : IDisposable } } - /// <summary> - /// Deletes the encoded media cache. - /// </summary> private void DeleteEncodedMediaCache() { var path = _serverConfigurationManager.GetTranscodePath(); @@ -899,26 +742,10 @@ public class TranscodingJobHelper : IDisposable } } - /// <summary> - /// Dispose transcoding job helper. - /// </summary> + /// <inheritdoc /> public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// <summary> - /// Dispose throttler. - /// </summary> - /// <param name="disposing">Disposing.</param> - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - _loggerFactory.Dispose(); - _sessionManager.PlaybackProgress -= OnPlaybackProgress; - _sessionManager.PlaybackStart -= OnPlaybackProgress; - } + _sessionManager.PlaybackProgress -= OnPlaybackProgress; + _sessionManager.PlaybackStart -= OnPlaybackProgress; } } diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index fbad29143..1c071067d 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs @@ -31,6 +31,8 @@ namespace MediaBrowser.Model.Configuration public bool EnableLUFSScan { get; set; } + public bool UseReplayGainTags { get; set; } + public bool EnableChapterImageExtraction { get; set; } public bool ExtractChapterImagesDuringLibraryScan { get; set; } diff --git a/MediaBrowser.Model/Devices/DeviceInfo.cs b/MediaBrowser.Model/Devices/DeviceInfo.cs index 7a1c7a738..4962992a0 100644 --- a/MediaBrowser.Model/Devices/DeviceInfo.cs +++ b/MediaBrowser.Model/Devices/DeviceInfo.cs @@ -15,6 +15,8 @@ namespace MediaBrowser.Model.Devices public string Name { get; set; } + public string CustomName { get; set; } + /// <summary> /// Gets or sets the access token. /// </summary> diff --git a/MediaBrowser.Model/Dlna/DeviceIdentification.cs b/MediaBrowser.Model/Dlna/DeviceIdentification.cs deleted file mode 100644 index 6625b7981..000000000 --- a/MediaBrowser.Model/Dlna/DeviceIdentification.cs +++ /dev/null @@ -1,63 +0,0 @@ -#pragma warning disable CS1591 - -using System; - -namespace MediaBrowser.Model.Dlna -{ - public class DeviceIdentification - { - /// <summary> - /// Gets or sets the name of the friendly. - /// </summary> - /// <value>The name of the friendly.</value> - public string FriendlyName { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the model number. - /// </summary> - /// <value>The model number.</value> - public string ModelNumber { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the serial number. - /// </summary> - /// <value>The serial number.</value> - public string SerialNumber { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the name of the model. - /// </summary> - /// <value>The name of the model.</value> - public string ModelName { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the model description. - /// </summary> - /// <value>The model description.</value> - public string ModelDescription { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the model URL. - /// </summary> - /// <value>The model URL.</value> - public string ModelUrl { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the manufacturer. - /// </summary> - /// <value>The manufacturer.</value> - public string Manufacturer { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the manufacturer URL. - /// </summary> - /// <value>The manufacturer URL.</value> - public string ManufacturerUrl { get; set; } = string.Empty; - - /// <summary> - /// Gets or sets the headers. - /// </summary> - /// <value>The headers.</value> - public HttpHeaderInfo[] Headers { get; set; } = Array.Empty<HttpHeaderInfo>(); - } -} diff --git a/MediaBrowser.Model/Dlna/DeviceProfile.cs b/MediaBrowser.Model/Dlna/DeviceProfile.cs index 71d0896a7..2addebbfc 100644 --- a/MediaBrowser.Model/Dlna/DeviceProfile.cs +++ b/MediaBrowser.Model/Dlna/DeviceProfile.cs @@ -1,11 +1,7 @@ #pragma warning disable CA1819 // Properties should not return arrays + using System; -using System.ComponentModel; -using System.Linq; using System.Xml.Serialization; -using Jellyfin.Data.Enums; -using Jellyfin.Extensions; -using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.Model.Dlna { @@ -17,7 +13,6 @@ namespace MediaBrowser.Model.Dlna /// the device is able to direct play (without transcoding or remuxing), /// as well as which <see cref="TranscodingProfiles">containers/codecs to transcode to</see> in case it isn't. /// </summary> - [XmlRoot("Profile")] public class DeviceProfile { /// <summary> @@ -32,104 +27,6 @@ namespace MediaBrowser.Model.Dlna public string? Id { get; set; } /// <summary> - /// Gets or sets the Identification. - /// </summary> - public DeviceIdentification? Identification { get; set; } - - /// <summary> - /// Gets or sets the friendly name of the device profile, which can be shown to users. - /// </summary> - public string? FriendlyName { get; set; } - - /// <summary> - /// Gets or sets the manufacturer of the device which this profile represents. - /// </summary> - public string? Manufacturer { get; set; } - - /// <summary> - /// Gets or sets an url for the manufacturer of the device which this profile represents. - /// </summary> - public string? ManufacturerUrl { get; set; } - - /// <summary> - /// Gets or sets the model name of the device which this profile represents. - /// </summary> - public string? ModelName { get; set; } - - /// <summary> - /// Gets or sets the model description of the device which this profile represents. - /// </summary> - public string? ModelDescription { get; set; } - - /// <summary> - /// Gets or sets the model number of the device which this profile represents. - /// </summary> - public string? ModelNumber { get; set; } - - /// <summary> - /// Gets or sets the ModelUrl. - /// </summary> - public string? ModelUrl { get; set; } - - /// <summary> - /// Gets or sets the serial number of the device which this profile represents. - /// </summary> - public string? SerialNumber { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether EnableAlbumArtInDidl. - /// </summary> - [DefaultValue(false)] - public bool EnableAlbumArtInDidl { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether EnableSingleAlbumArtLimit. - /// </summary> - [DefaultValue(false)] - public bool EnableSingleAlbumArtLimit { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether EnableSingleSubtitleLimit. - /// </summary> - [DefaultValue(false)] - public bool EnableSingleSubtitleLimit { get; set; } - - /// <summary> - /// Gets or sets the SupportedMediaTypes. - /// </summary> - public string SupportedMediaTypes { get; set; } = "Audio,Photo,Video"; - - /// <summary> - /// Gets or sets the UserId. - /// </summary> - public string? UserId { get; set; } - - /// <summary> - /// Gets or sets the AlbumArtPn. - /// </summary> - public string? AlbumArtPn { get; set; } - - /// <summary> - /// Gets or sets the MaxAlbumArtWidth. - /// </summary> - public int? MaxAlbumArtWidth { get; set; } - - /// <summary> - /// Gets or sets the MaxAlbumArtHeight. - /// </summary> - public int? MaxAlbumArtHeight { get; set; } - - /// <summary> - /// Gets or sets the maximum allowed width of embedded icons. - /// </summary> - public int? MaxIconWidth { get; set; } - - /// <summary> - /// Gets or sets the maximum allowed height of embedded icons. - /// </summary> - public int? MaxIconHeight { get; set; } - - /// <summary> /// Gets or sets the maximum allowed bitrate for all streamed content. /// </summary> public int? MaxStreamingBitrate { get; set; } = 8000000; @@ -150,51 +47,6 @@ namespace MediaBrowser.Model.Dlna public int? MaxStaticMusicBitrate { get; set; } = 8000000; /// <summary> - /// Gets or sets the content of the aggregationFlags element in the urn:schemas-sonycom:av namespace. - /// </summary> - public string? SonyAggregationFlags { get; set; } - - /// <summary> - /// Gets or sets the ProtocolInfo. - /// </summary> - public string? ProtocolInfo { get; set; } - - /// <summary> - /// Gets or sets the TimelineOffsetSeconds. - /// </summary> - [DefaultValue(0)] - public int TimelineOffsetSeconds { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether RequiresPlainVideoItems. - /// </summary> - [DefaultValue(false)] - public bool RequiresPlainVideoItems { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether RequiresPlainFolders. - /// </summary> - [DefaultValue(false)] - public bool RequiresPlainFolders { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether EnableMSMediaReceiverRegistrar. - /// </summary> - [DefaultValue(false)] - public bool EnableMSMediaReceiverRegistrar { get; set; } - - /// <summary> - /// Gets or sets a value indicating whether IgnoreTranscodeByteRangeRequests. - /// </summary> - [DefaultValue(false)] - public bool IgnoreTranscodeByteRangeRequests { get; set; } - - /// <summary> - /// Gets or sets the XmlRootAttributes. - /// </summary> - public XmlAttribute[] XmlRootAttributes { get; set; } = Array.Empty<XmlAttribute>(); - - /// <summary> /// Gets or sets the direct play profiles. /// </summary> public DirectPlayProfile[] DirectPlayProfiles { get; set; } = Array.Empty<DirectPlayProfile>(); @@ -215,297 +67,8 @@ namespace MediaBrowser.Model.Dlna public CodecProfile[] CodecProfiles { get; set; } = Array.Empty<CodecProfile>(); /// <summary> - /// Gets or sets the ResponseProfiles. - /// </summary> - public ResponseProfile[] ResponseProfiles { get; set; } = Array.Empty<ResponseProfile>(); - - /// <summary> /// Gets or sets the subtitle profiles. /// </summary> public SubtitleProfile[] SubtitleProfiles { get; set; } = Array.Empty<SubtitleProfile>(); - - /// <summary> - /// The GetSupportedMediaTypes. - /// </summary> - /// <returns>The .</returns> - public MediaType[] GetSupportedMediaTypes() - { - return ContainerProfile.SplitValue(SupportedMediaTypes) - .Select(m => Enum.TryParse<MediaType>(m, out var parsed) ? parsed : MediaType.Unknown) - .Where(m => m != MediaType.Unknown) - .ToArray(); - } - - /// <summary> - /// Gets the audio transcoding profile. - /// </summary> - /// <param name="container">The container.</param> - /// <param name="audioCodec">The audio Codec.</param> - /// <returns>A <see cref="TranscodingProfile"/>.</returns> - public TranscodingProfile? GetAudioTranscodingProfile(string? container, string? audioCodec) - { - container = (container ?? string.Empty).TrimStart('.'); - - foreach (var i in TranscodingProfiles) - { - if (i.Type != DlnaProfileType.Audio) - { - continue; - } - - if (!string.Equals(container, i.Container, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - if (!i.GetAudioCodecs().Contains(audioCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - return i; - } - - return null; - } - - /// <summary> - /// Gets the video transcoding profile. - /// </summary> - /// <param name="container">The container.</param> - /// <param name="audioCodec">The audio Codec.</param> - /// <param name="videoCodec">The video Codec.</param> - /// <returns>The <see cref="TranscodingProfile"/>.</returns> - public TranscodingProfile? GetVideoTranscodingProfile(string? container, string? audioCodec, string? videoCodec) - { - container = (container ?? string.Empty).TrimStart('.'); - - foreach (var i in TranscodingProfiles) - { - if (i.Type != DlnaProfileType.Video) - { - continue; - } - - if (!string.Equals(container, i.Container, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - if (!i.GetAudioCodecs().Contains(audioCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - if (!string.Equals(videoCodec, i.VideoCodec, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - return i; - } - - return null; - } - - /// <summary> - /// Gets the audio media profile. - /// </summary> - /// <param name="container">The container.</param> - /// <param name="audioCodec">The audio codec.</param> - /// <param name="audioChannels">The audio channels.</param> - /// <param name="audioBitrate">The audio bitrate.</param> - /// <param name="audioSampleRate">The audio sample rate.</param> - /// <param name="audioBitDepth">The audio bit depth.</param> - /// <returns>The <see cref="ResponseProfile"/>.</returns> - public ResponseProfile? GetAudioMediaProfile(string? container, string? audioCodec, int? audioChannels, int? audioBitrate, int? audioSampleRate, int? audioBitDepth) - { - foreach (var i in ResponseProfiles) - { - if (i.Type != DlnaProfileType.Audio) - { - continue; - } - - if (!ContainerProfile.ContainsContainer(i.GetContainers(), container)) - { - continue; - } - - var audioCodecs = i.GetAudioCodecs(); - if (audioCodecs.Length > 0 && !audioCodecs.Contains(audioCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - var anyOff = false; - foreach (ProfileCondition c in i.Conditions) - { - if (!ConditionProcessor.IsAudioConditionSatisfied(GetModelProfileCondition(c), audioChannels, audioBitrate, audioSampleRate, audioBitDepth)) - { - anyOff = true; - break; - } - } - - if (anyOff) - { - continue; - } - - return i; - } - - return null; - } - - /// <summary> - /// Gets the model profile condition. - /// </summary> - /// <param name="c">The c<see cref="ProfileCondition"/>.</param> - /// <returns>The <see cref="ProfileCondition"/>.</returns> - private ProfileCondition GetModelProfileCondition(ProfileCondition c) - { - return new ProfileCondition - { - Condition = c.Condition, - IsRequired = c.IsRequired, - Property = c.Property, - Value = c.Value - }; - } - - /// <summary> - /// Gets the image media profile. - /// </summary> - /// <param name="container">The container.</param> - /// <param name="width">The width.</param> - /// <param name="height">The height.</param> - /// <returns>The <see cref="ResponseProfile"/>.</returns> - public ResponseProfile? GetImageMediaProfile(string container, int? width, int? height) - { - foreach (var i in ResponseProfiles) - { - if (i.Type != DlnaProfileType.Photo) - { - continue; - } - - if (!ContainerProfile.ContainsContainer(i.GetContainers(), container)) - { - continue; - } - - var anyOff = false; - foreach (var c in i.Conditions) - { - if (!ConditionProcessor.IsImageConditionSatisfied(GetModelProfileCondition(c), width, height)) - { - anyOff = true; - break; - } - } - - if (anyOff) - { - continue; - } - - return i; - } - - return null; - } - - /// <summary> - /// Gets the video media profile. - /// </summary> - /// <param name="container">The container.</param> - /// <param name="audioCodec">The audio codec.</param> - /// <param name="videoCodec">The video codec.</param> - /// <param name="width">The width.</param> - /// <param name="height">The height.</param> - /// <param name="bitDepth">The bit depth.</param> - /// <param name="videoBitrate">The video bitrate.</param> - /// <param name="videoProfile">The video profile.</param> - /// <param name="videoRangeType">The video range type.</param> - /// <param name="videoLevel">The video level.</param> - /// <param name="videoFramerate">The video framerate.</param> - /// <param name="packetLength">The packet length.</param> - /// <param name="timestamp">The timestamp<see cref="TransportStreamTimestamp"/>.</param> - /// <param name="isAnamorphic">True if anamorphic.</param> - /// <param name="isInterlaced">True if interlaced.</param> - /// <param name="refFrames">The ref frames.</param> - /// <param name="numVideoStreams">The number of video streams.</param> - /// <param name="numAudioStreams">The number of audio streams.</param> - /// <param name="videoCodecTag">The video Codec tag.</param> - /// <param name="isAvc">True if Avc.</param> - /// <returns>The <see cref="ResponseProfile"/>.</returns> - public ResponseProfile? GetVideoMediaProfile( - string? container, - string? audioCodec, - string? videoCodec, - int? width, - int? height, - int? bitDepth, - int? videoBitrate, - string? videoProfile, - VideoRangeType videoRangeType, - double? videoLevel, - float? videoFramerate, - int? packetLength, - TransportStreamTimestamp timestamp, - bool? isAnamorphic, - bool? isInterlaced, - int? refFrames, - int? numVideoStreams, - int? numAudioStreams, - string? videoCodecTag, - bool? isAvc) - { - foreach (var i in ResponseProfiles) - { - if (i.Type != DlnaProfileType.Video) - { - continue; - } - - if (!ContainerProfile.ContainsContainer(i.GetContainers(), container)) - { - continue; - } - - var audioCodecs = i.GetAudioCodecs(); - if (audioCodecs.Length > 0 && !audioCodecs.Contains(audioCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - var videoCodecs = i.GetVideoCodecs(); - if (videoCodecs.Length > 0 && !videoCodecs.Contains(videoCodec ?? string.Empty, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - var anyOff = false; - foreach (ProfileCondition c in i.Conditions) - { - if (!ConditionProcessor.IsVideoConditionSatisfied(GetModelProfileCondition(c), width, height, bitDepth, videoBitrate, videoProfile, videoRangeType, videoLevel, videoFramerate, packetLength, timestamp, isAnamorphic, isInterlaced, refFrames, numVideoStreams, numAudioStreams, videoCodecTag, isAvc)) - { - anyOff = true; - break; - } - } - - if (anyOff) - { - continue; - } - - return i; - } - - return null; - } } } diff --git a/MediaBrowser.Model/Dlna/DeviceProfileInfo.cs b/MediaBrowser.Model/Dlna/DeviceProfileInfo.cs deleted file mode 100644 index 74c32c523..000000000 --- a/MediaBrowser.Model/Dlna/DeviceProfileInfo.cs +++ /dev/null @@ -1,26 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Dlna -{ - public class DeviceProfileInfo - { - /// <summary> - /// Gets or sets the identifier. - /// </summary> - /// <value>The identifier.</value> - public string Id { get; set; } - - /// <summary> - /// Gets or sets the name. - /// </summary> - /// <value>The name.</value> - public string Name { get; set; } - - /// <summary> - /// Gets or sets the type. - /// </summary> - /// <value>The type.</value> - public DeviceProfileType Type { get; set; } - } -} diff --git a/MediaBrowser.Model/Dlna/DeviceProfileType.cs b/MediaBrowser.Model/Dlna/DeviceProfileType.cs deleted file mode 100644 index 46062abd0..000000000 --- a/MediaBrowser.Model/Dlna/DeviceProfileType.cs +++ /dev/null @@ -1,10 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Dlna -{ - public enum DeviceProfileType - { - System = 0, - User = 1 - } -} diff --git a/MediaBrowser.Model/Dlna/HeaderMatchType.cs b/MediaBrowser.Model/Dlna/HeaderMatchType.cs deleted file mode 100644 index 2a9abb20e..000000000 --- a/MediaBrowser.Model/Dlna/HeaderMatchType.cs +++ /dev/null @@ -1,11 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.Dlna -{ - public enum HeaderMatchType - { - Equals = 0, - Regex = 1, - Substring = 2 - } -} diff --git a/MediaBrowser.Model/Dlna/HttpHeaderInfo.cs b/MediaBrowser.Model/Dlna/HttpHeaderInfo.cs deleted file mode 100644 index 17c4dffcc..000000000 --- a/MediaBrowser.Model/Dlna/HttpHeaderInfo.cs +++ /dev/null @@ -1,19 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System.Xml.Serialization; - -namespace MediaBrowser.Model.Dlna -{ - public class HttpHeaderInfo - { - [XmlAttribute("name")] - public string Name { get; set; } - - [XmlAttribute("value")] - public string Value { get; set; } - - [XmlAttribute("match")] - public HeaderMatchType Match { get; set; } - } -} diff --git a/MediaBrowser.Model/Dlna/ResponseProfile.cs b/MediaBrowser.Model/Dlna/ResponseProfile.cs deleted file mode 100644 index bf9661f7f..000000000 --- a/MediaBrowser.Model/Dlna/ResponseProfile.cs +++ /dev/null @@ -1,51 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; -using System.Xml.Serialization; - -namespace MediaBrowser.Model.Dlna -{ - public class ResponseProfile - { - public ResponseProfile() - { - Conditions = Array.Empty<ProfileCondition>(); - } - - [XmlAttribute("container")] - public string Container { get; set; } - - [XmlAttribute("audioCodec")] - public string AudioCodec { get; set; } - - [XmlAttribute("videoCodec")] - public string VideoCodec { get; set; } - - [XmlAttribute("type")] - public DlnaProfileType Type { get; set; } - - [XmlAttribute("orgPn")] - public string OrgPn { get; set; } - - [XmlAttribute("mimeType")] - public string MimeType { get; set; } - - public ProfileCondition[] Conditions { get; set; } - - public string[] GetContainers() - { - return ContainerProfile.SplitValue(Container); - } - - public string[] GetAudioCodecs() - { - return ContainerProfile.SplitValue(AudioCodec); - } - - public string[] GetVideoCodecs() - { - return ContainerProfile.SplitValue(VideoCodec); - } - } -} diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index bf18d46dc..da683a17e 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.MediaInfo; @@ -1536,7 +1537,7 @@ namespace MediaBrowser.Model.Dlna private static void ValidateMediaOptions(MediaOptions options, bool isMediaSource) { - if (options.ItemId.Equals(default)) + if (options.ItemId.IsEmpty()) { ArgumentException.ThrowIfNullOrEmpty(options.DeviceId); } diff --git a/MediaBrowser.Model/Dlna/XmlAttribute.cs b/MediaBrowser.Model/Dlna/XmlAttribute.cs deleted file mode 100644 index 03bb2e4b1..000000000 --- a/MediaBrowser.Model/Dlna/XmlAttribute.cs +++ /dev/null @@ -1,25 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System.Xml.Serialization; - -namespace MediaBrowser.Model.Dlna -{ - /// <summary> - /// Defines the <see cref="XmlAttribute" />. - /// </summary> - public class XmlAttribute - { - /// <summary> - /// Gets or sets the name of the attribute. - /// </summary> - [XmlAttribute("name")] - public string Name { get; set; } - - /// <summary> - /// Gets or sets the value of the attribute. - /// </summary> - [XmlAttribute("value")] - public string Value { get; set; } - } -} diff --git a/MediaBrowser.Model/Dto/BaseItemDto.cs b/MediaBrowser.Model/Dto/BaseItemDto.cs index d257eab92..cfff717db 100644 --- a/MediaBrowser.Model/Dto/BaseItemDto.cs +++ b/MediaBrowser.Model/Dto/BaseItemDto.cs @@ -85,11 +85,6 @@ namespace MediaBrowser.Model.Dto public string PreferredMetadataCountryCode { get; set; } - /// <summary> - /// Gets or sets a value indicating whether [supports synchronize]. - /// </summary> - public bool? SupportsSync { get; set; } - public string Container { get; set; } /// <summary> diff --git a/MediaBrowser.Model/Dto/UpdateUserItemDataDto.cs b/MediaBrowser.Model/Dto/UpdateUserItemDataDto.cs new file mode 100644 index 000000000..7bfedf973 --- /dev/null +++ b/MediaBrowser.Model/Dto/UpdateUserItemDataDto.cs @@ -0,0 +1,76 @@ +using System; + +namespace MediaBrowser.Model.Dto +{ + /// <summary> + /// This is used by the api to get information about a item user data. + /// </summary> + public class UpdateUserItemDataDto + { + /// <summary> + /// Gets or sets the rating. + /// </summary> + /// <value>The rating.</value> + public double? Rating { get; set; } + + /// <summary> + /// Gets or sets the played percentage. + /// </summary> + /// <value>The played percentage.</value> + public double? PlayedPercentage { get; set; } + + /// <summary> + /// Gets or sets the unplayed item count. + /// </summary> + /// <value>The unplayed item count.</value> + public int? UnplayedItemCount { get; set; } + + /// <summary> + /// Gets or sets the playback position ticks. + /// </summary> + /// <value>The playback position ticks.</value> + public long? PlaybackPositionTicks { get; set; } + + /// <summary> + /// Gets or sets the play count. + /// </summary> + /// <value>The play count.</value> + public int? PlayCount { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this instance is favorite. + /// </summary> + /// <value><c>true</c> if this instance is favorite; otherwise, <c>false</c>.</value> + public bool? IsFavorite { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this <see cref="UpdateUserItemDataDto" /> is likes. + /// </summary> + /// <value><c>null</c> if [likes] contains no value, <c>true</c> if [likes]; otherwise, <c>false</c>.</value> + public bool? Likes { get; set; } + + /// <summary> + /// Gets or sets the last played date. + /// </summary> + /// <value>The last played date.</value> + public DateTime? LastPlayedDate { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether this <see cref="UserItemDataDto" /> is played. + /// </summary> + /// <value><c>true</c> if played; otherwise, <c>false</c>.</value> + public bool? Played { get; set; } + + /// <summary> + /// Gets or sets the key. + /// </summary> + /// <value>The key.</value> + public string? Key { get; set; } + + /// <summary> + /// Gets or sets the item identifier. + /// </summary> + /// <value>The item identifier.</value> + public string? ItemId { get; set; } + } +} diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 34642b83a..ae4a008bb 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Globalization; using System.Linq; using System.Text; @@ -214,6 +215,27 @@ namespace MediaBrowser.Model.Entities } } + /// <summary> + /// Gets the audio spatial format. + /// </summary> + /// <value>The audio spatial format.</value> + [DefaultValue(AudioSpatialFormat.None)] + public AudioSpatialFormat AudioSpatialFormat + { + get + { + if (Type != MediaStreamType.Audio || string.IsNullOrEmpty(Profile)) + { + return AudioSpatialFormat.None; + } + + return + Profile.Contains("Dolby Atmos", StringComparison.OrdinalIgnoreCase) ? AudioSpatialFormat.DolbyAtmos : + Profile.Contains("DTS:X", StringComparison.OrdinalIgnoreCase) ? AudioSpatialFormat.DTSX : + AudioSpatialFormat.None; + } + } + public string LocalizedUndefined { get; set; } public string LocalizedDefault { get; set; } diff --git a/MediaBrowser.Model/Entities/UserDataSaveReason.cs b/MediaBrowser.Model/Entities/UserDataSaveReason.cs index 20404e6f4..b8e73a98c 100644 --- a/MediaBrowser.Model/Entities/UserDataSaveReason.cs +++ b/MediaBrowser.Model/Entities/UserDataSaveReason.cs @@ -33,6 +33,11 @@ namespace MediaBrowser.Model.Entities /// <summary> /// The import. /// </summary> - Import = 6 + Import = 6, + + /// <summary> + /// API call updated item user data. + /// </summary> + UpdateUserData = 7, } } diff --git a/MediaBrowser.Model/IO/IStreamHelper.cs b/MediaBrowser.Model/IO/IStreamHelper.cs index f900da556..034a6bf8b 100644 --- a/MediaBrowser.Model/IO/IStreamHelper.cs +++ b/MediaBrowser.Model/IO/IStreamHelper.cs @@ -13,8 +13,6 @@ namespace MediaBrowser.Model.IO Task CopyToAsync(Stream source, Stream destination, int bufferSize, int emptyReadLimit, CancellationToken cancellationToken); - Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken); - Task CopyUntilCancelled(Stream source, Stream target, int bufferSize, CancellationToken cancellationToken); } } diff --git a/MediaBrowser.Model/LiveTv/LiveTvTunerStatus.cs b/MediaBrowser.Model/LiveTv/LiveTvTunerStatus.cs deleted file mode 100644 index 80a646195..000000000 --- a/MediaBrowser.Model/LiveTv/LiveTvTunerStatus.cs +++ /dev/null @@ -1,12 +0,0 @@ -#pragma warning disable CS1591 - -namespace MediaBrowser.Model.LiveTv -{ - public enum LiveTvTunerStatus - { - Available = 0, - Disabled = 1, - RecordingTv = 2, - LiveTv = 3 - } -} diff --git a/MediaBrowser.Model/Querying/ItemFields.cs b/MediaBrowser.Model/Querying/ItemFields.cs index 242a1c6e9..49d7c0bcb 100644 --- a/MediaBrowser.Model/Querying/ItemFields.cs +++ b/MediaBrowser.Model/Querying/ItemFields.cs @@ -175,13 +175,6 @@ namespace MediaBrowser.Model.Querying /// </summary> Studios, - BasicSyncInfo, - - /// <summary> - /// The synchronize information. - /// </summary> - SyncInfo, - /// <summary> /// The taglines of the item. /// </summary> diff --git a/MediaBrowser.Model/Session/ClientCapabilities.cs b/MediaBrowser.Model/Session/ClientCapabilities.cs index 7fefce9cd..597845fc1 100644 --- a/MediaBrowser.Model/Session/ClientCapabilities.cs +++ b/MediaBrowser.Model/Session/ClientCapabilities.cs @@ -23,14 +23,8 @@ namespace MediaBrowser.Model.Session public bool SupportsMediaControl { get; set; } - public bool SupportsContentUploading { get; set; } - - public string MessageCallbackUrl { get; set; } - public bool SupportsPersistentIdentifier { get; set; } - public bool SupportsSync { get; set; } - public DeviceProfile DeviceProfile { get; set; } public string AppStoreUrl { get; set; } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 4ba884418..b530b9de3 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -706,7 +706,7 @@ namespace MediaBrowser.Providers.Manager { BaseItem? referenceItem = null; - if (!searchInfo.ItemId.Equals(default)) + if (!searchInfo.ItemId.IsEmpty()) { referenceItem = _libraryManager.GetItemById(searchInfo.ItemId); } @@ -944,7 +944,7 @@ namespace MediaBrowser.Providers.Manager public void QueueRefresh(Guid itemId, MetadataRefreshOptions options, RefreshPriority priority) { ArgumentNullException.ThrowIfNull(itemId); - if (itemId.Equals(default)) + if (itemId.IsEmpty()) { throw new ArgumentException("Guid can't be empty", nameof(itemId)); } diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 5d41542e2..f68faab04 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -61,6 +61,9 @@ namespace MediaBrowser.Providers.MediaInfo [GeneratedRegex(@"I:\s+(.*?)\s+LUFS")] private static partial Regex LUFSRegex(); + [GeneratedRegex(@"REPLAYGAIN_TRACK_GAIN:\s+-?([0-9.]+)\s+dB")] + private static partial Regex ReplayGainTagRegex(); + /// <summary> /// Probes the specified item for metadata. /// </summary> @@ -104,8 +107,50 @@ namespace MediaBrowser.Providers.MediaInfo } var libraryOptions = _libraryManager.GetLibraryOptions(item); + bool foundLUFSValue = false; + + if (libraryOptions.UseReplayGainTags) + { + using (var process = new Process() + { + StartInfo = new ProcessStartInfo + { + FileName = _mediaEncoder.ProbePath, + Arguments = $"-hide_banner -i \"{path}\"", + RedirectStandardOutput = false, + RedirectStandardError = true + }, + }) + { + try + { + process.Start(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error starting ffmpeg"); + + throw; + } + + using var reader = process.StandardError; + var output = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false); + cancellationToken.ThrowIfCancellationRequested(); + Match split = ReplayGainTagRegex().Match(output); + + if (split.Success) + { + item.LUFS = DefaultLUFSValue - float.Parse(split.Groups[1].ValueSpan, CultureInfo.InvariantCulture.NumberFormat); + foundLUFSValue = true; + } + else + { + item.LUFS = DefaultLUFSValue; + } + } + } - if (libraryOptions.EnableLUFSScan) + if (libraryOptions.EnableLUFSScan && !foundLUFSValue) { using (var process = new Process() { @@ -144,7 +189,8 @@ namespace MediaBrowser.Providers.MediaInfo } } } - else + + if (!libraryOptions.EnableLUFSScan && !libraryOptions.UseReplayGainTags) { item.LUFS = DefaultLUFSValue; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs index c2018d820..c76c65591 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetProvider.cs @@ -75,12 +75,14 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets var collections = new RemoteSearchResult[collectionSearchResults.Count]; for (var i = 0; i < collectionSearchResults.Count; i++) { + var result = collectionSearchResults[i]; var collection = new RemoteSearchResult { - Name = collectionSearchResults[i].Name, - SearchProviderName = Name + Name = result.Name, + SearchProviderName = Name, + ImageUrl = _tmdbClientManager.GetPosterUrl(result.PosterPath) }; - collection.SetProviderId(MetadataProvider.Tmdb, collectionSearchResults[i].Id.ToString(CultureInfo.InvariantCulture)); + collection.SetProviderId(MetadataProvider.Tmdb, result.Id.ToString(CultureInfo.InvariantCulture)); collections[i] = collection; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs index 03aaf380b..99b759ae2 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs @@ -8,6 +8,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb public class PluginConfiguration : BasePluginConfiguration { /// <summary> + /// Gets or sets a value to use as the API key for accessing TMDb. This is intentionally excluded from the + /// settings page as the API key should not need to be changed by most users. + /// </summary> + public string TmdbApiKey { get; set; } = string.Empty; + + /// <summary> /// Gets or sets a value indicating whether include adult content when searching with TMDb. /// </summary> public bool IncludeAdult { get; set; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html index cd21516f9..f3c24e7b4 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html +++ b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html @@ -64,9 +64,18 @@ var clientConfig, pluginConfig; var configureImageScaling = function() { - if (clientConfig === null || pluginConfig === null) { + if (clientConfig === undefined || pluginConfig === undefined) { return; } + if (Object.keys(clientConfig).length === 0) { + clientConfig = { + PosterSizes: [pluginConfig.PosterSize], + BackdropSizes: [pluginConfig.BackdropSize], + LogoSizes: [pluginConfig.LogoSize], + ProfileSizes: [pluginConfig.ProfileSize], + StillSizes: [pluginConfig.StillSize] + }; + } var sizeOptionsGenerator = function (size) { return '<option value="' + size + '">' + size + '</option>'; @@ -104,6 +113,15 @@ ApiClient.fetch(request).then(function (config) { clientConfig = config; configureImageScaling(); + }, function (error) { + error.text().then(function (contents) { + Dashboard.alert({ + title: error.statusText, + message: contents + }); + clientConfig = {}; + configureImageScaling(); + }); }); ApiClient.getPluginConfiguration(PluginConfig.pluginId).then(function (config) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index 2f62e117e..dac7a74ed 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -299,7 +299,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies if (!string.IsNullOrWhiteSpace(person.ProfilePath)) { - personInfo.ImageUrl = _tmdbClientManager.GetPosterUrl(person.ProfilePath); + personInfo.ImageUrl = _tmdbClientManager.GetProfileUrl(person.ProfilePath); } if (person.Id > 0) diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs index 72e59c9ac..82f2c54f1 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs @@ -36,7 +36,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb public TmdbClientManager(IMemoryCache memoryCache) { _memoryCache = memoryCache; - _tmDbClient = new TMDbClient(TmdbUtils.ApiKey); + + var apiKey = Plugin.Instance.Configuration.TmdbApiKey; + apiKey = string.IsNullOrEmpty(apiKey) ? TmdbUtils.ApiKey : apiKey; + _tmDbClient = new TMDbClient(apiKey); + // Not really interested in NotFoundException _tmDbClient.ThrowApiExceptions = false; } @@ -142,6 +142,17 @@ cd Jellyfin.Server/bin/Debug/net8.0 # Change into the build output directory 2. Execute the build output. On Linux, Mac, etc. use `./jellyfin` and on Windows use `jellyfin.exe`. +### Running from GH-Codespaces + +As Jellyfin will run on a container on a github hosted server, JF needs to handle some things differently. +**NOTE:** If you want to access the JF instance from outside, like with a WebClient on another PC, remember to set the "ports" in the lower VsCode window to public. + +#### FFmpeg installation. +Because sometimes you need FFMPEG to test certain cases, follow the instructions from the wiki on the dev enviorment: +https://jellyfin.org/docs/general/installation/linux/#ffmpeg-installation + +**NOTE:** When first opening the server instance with any WebUI, you will be send to the login instead of the setup page. Refresh the login page once and you should be redirected to the Setup. + ### Running The Tests This repository also includes unit tests that are used to validate functionality as part of a CI pipeline on Azure. There are several ways to run these tests. diff --git a/bump_version b/bump_version index 41d27f5c8..dd55e62c7 100755 --- a/bump_version +++ b/bump_version @@ -21,7 +21,7 @@ fi shared_version_file="./SharedVersion.cs" build_file="./build.yaml" # csproj files for nuget packages -jellyfin_subprojects=( +jellyfin_subprojects=( MediaBrowser.Common/MediaBrowser.Common.csproj Jellyfin.Data/Jellyfin.Data.csproj MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -97,7 +97,7 @@ cat ${debian_changelog_file} >> ${debian_changelog_temp} # Move into place mv ${debian_changelog_temp} ${debian_changelog_file} -# Write out a temporary Yum changelog with our new stuff prepended and some templated formatting +# Write out a temporary Dnf changelog with our new stuff prepended and some templated formatting fedora_spec_file="fedora/jellyfin.spec" fedora_changelog_temp="$( mktemp )" fedora_spec_temp_dir="$( mktemp -d )" diff --git a/debian/rules b/debian/rules index 069d48aad..79cd55a15 100755 --- a/debian/rules +++ b/debian/rules @@ -7,27 +7,27 @@ HOST_ARCH := $(shell arch) BUILD_ARCH := ${DEB_HOST_MULTIARCH} ifeq ($(HOST_ARCH),x86_64) # Building AMD64 - DOTNETRUNTIME := debian-x64 + DOTNETRUNTIME := linux-x64 ifeq ($(BUILD_ARCH),arm-linux-gnueabihf) # Cross-building ARM on AMD64 - DOTNETRUNTIME := debian-arm + DOTNETRUNTIME := linux-arm endif ifeq ($(BUILD_ARCH),aarch64-linux-gnu) # Cross-building ARM on AMD64 - DOTNETRUNTIME := debian-arm64 + DOTNETRUNTIME := linux-arm64 endif endif ifeq ($(HOST_ARCH),armv7l) # Building ARM - DOTNETRUNTIME := debian-arm + DOTNETRUNTIME := linux-arm endif ifeq ($(HOST_ARCH),arm64) # Building ARM - DOTNETRUNTIME := debian-arm64 + DOTNETRUNTIME := linux-arm64 endif ifeq ($(HOST_ARCH),aarch64) # Building ARM - DOTNETRUNTIME := debian-arm64 + DOTNETRUNTIME := linux-arm64 endif export DH_VERBOSE=1 diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64 index 7c9bbf39e..af309b083 100644 --- a/deployment/Dockerfile.centos.amd64 +++ b/deployment/Dockerfile.centos.amd64 @@ -1,29 +1,36 @@ -FROM centos:7 +FROM quay.io/centos/centos:stream9 + # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist + # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist ENV IS_DOCKER=YES # Prepare CentOS environment -RUN yum update -yq \ - && yum install -yq epel-release \ - && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget +RUN dnf update -yq \ + && dnf install -yq \ + @buildsys-build rpmdevtools git \ + dnf-plugins-core libcurl-devel fontconfig-devel \ + freetype-devel openssl-devel glibc-devel \ + libicu-devel systemd wget make \ + && dnf clean all \ + && rm -rf /var/cache/dnf # Install DotNET SDK RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ - && mkdir -p dotnet-sdk \ - && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ - && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet + && mkdir -p dotnet-sdk \ + && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ + && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet # Create symlinks and directories RUN ln -sf ${SOURCE_DIR}/deployment/build.centos.amd64 /build.sh \ - && mkdir -p ${SOURCE_DIR}/SPECS \ - && ln -s ${SOURCE_DIR}/fedora/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \ - && mkdir -p ${SOURCE_DIR}/SOURCES \ - && ln -s ${SOURCE_DIR}/fedora ${SOURCE_DIR}/SOURCES + && mkdir -p ${SOURCE_DIR}/SPECS \ + && ln -s ${SOURCE_DIR}/fedora/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \ + && mkdir -p ${SOURCE_DIR}/SOURCES \ + && ln -s ${SOURCE_DIR}/fedora ${SOURCE_DIR}/SOURCES VOLUME ${SOURCE_DIR}/ diff --git a/deployment/Dockerfile.debian.amd64 b/deployment/Dockerfile.debian.amd64 index d344c5964..da0c9dabd 100644 --- a/deployment/Dockerfile.debian.amd64 +++ b/deployment/Dockerfile.debian.amd64 @@ -1,7 +1,11 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim +ARG DOTNET_VERSION=8.0 + +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim + # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist + # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist @@ -10,11 +14,14 @@ ENV ARCH=amd64 ENV IS_DOCKER=YES # Prepare Debian build environment -RUN apt-get update -yqq \ - && apt-get install -yqq --no-install-recommends \ +RUN apt-get update -yq \ + && apt-get install --no-install-recommends -yq \ debhelper gnupg devscripts build-essential mmv \ - libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev libssl-dev \ - libssl1.1 liblttng-ust0 + libcurl4-openssl-dev libfontconfig1-dev libfreetype6-dev \ + libssl-dev libssl3 liblttng-ust1 \ + && apt-get clean autoclean -yq \ + && apt-get autoremove -yq \ + && rm -rf /var/lib/apt/lists/* # Link to build script RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.amd64 /build.sh diff --git a/deployment/Dockerfile.debian.arm64 b/deployment/Dockerfile.debian.arm64 index 8a5411f05..6c4cb816f 100644 --- a/deployment/Dockerfile.debian.arm64 +++ b/deployment/Dockerfile.debian.arm64 @@ -1,7 +1,11 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim +ARG DOTNET_VERSION=8.0 + +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim + # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist + # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist @@ -11,23 +15,26 @@ ENV IS_DOCKER=YES # Prepare Debian build environment RUN apt-get update -yqq \ - && apt-get install -yqq --no-install-recommends \ + && apt-get install --no-install-recommends -yqq \ debhelper gnupg devscripts build-essential mmv # Prepare the cross-toolchain RUN dpkg --add-architecture arm64 \ - && apt-get update -yqq \ - && apt-get install -yqq --no-install-recommends cross-gcc-dev \ - && TARGET_LIST="arm64" cross-gcc-gensource 9 \ - && cd cross-gcc-packages-amd64/cross-gcc-9-arm64 \ - && apt-get install -yqq --no-install-recommends \ - gcc-9-source libstdc++-9-dev-arm64-cross \ + && apt-get update -yqq \ + && apt-get install --no-install-recommends -yqq cross-gcc-dev \ + && TARGET_LIST="arm64" cross-gcc-gensource 12 \ + && cd cross-gcc-packages-amd64/cross-gcc-12-arm64 \ + && apt-get install --no-install-recommends -yqq \ + gcc-12-source libstdc++-12-dev-arm64-cross \ binutils-aarch64-linux-gnu bison flex libtool \ gdb sharutils netbase libmpc-dev libmpfr-dev libgmp-dev \ systemtap-sdt-dev autogen expect chrpath zlib1g-dev zip \ libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 \ libcurl4-openssl-dev:arm64 libfontconfig1-dev:arm64 \ - libfreetype6-dev:arm64 libssl-dev:arm64 liblttng-ust0:arm64 libstdc++-9-dev:arm64 + libfreetype6-dev:arm64 libssl-dev:arm64 liblttng-ust1:arm64 libstdc++-12-dev:arm64 \ + && apt-get clean autoclean -yqq \ + && apt-get autoremove -yqq \ + && rm -rf /var/lib/apt/lists/* # Link to build script RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.arm64 /build.sh diff --git a/deployment/Dockerfile.debian.armhf b/deployment/Dockerfile.debian.armhf index e95ba1696..b1fa6cee5 100644 --- a/deployment/Dockerfile.debian.armhf +++ b/deployment/Dockerfile.debian.armhf @@ -1,7 +1,11 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim +ARG DOTNET_VERSION=8.0 + +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim + # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist + # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist @@ -11,24 +15,27 @@ ENV IS_DOCKER=YES # Prepare Debian build environment RUN apt-get update -yqq \ - && apt-get install -yqq --no-install-recommends \ + && apt-get install --no-install-recommends -yqq \ debhelper gnupg devscripts build-essential mmv # Prepare the cross-toolchain RUN dpkg --add-architecture armhf \ - && apt-get update -yqq \ - && apt-get install -yqq --no-install-recommends cross-gcc-dev \ - && TARGET_LIST="armhf" cross-gcc-gensource 9 \ - && cd cross-gcc-packages-amd64/cross-gcc-9-armhf \ - && apt-get install -yqq --no-install-recommends\ - gcc-9-source libstdc++-9-dev-armhf-cross \ + && apt-get update -yqq \ + && apt-get install --no-install-recommends -yqq cross-gcc-dev \ + && TARGET_LIST="armhf" cross-gcc-gensource 12 \ + && cd cross-gcc-packages-amd64/cross-gcc-12-armhf \ + && apt-get install --no-install-recommends -yqq \ + gcc-12-source libstdc++-12-dev-armhf-cross \ binutils-aarch64-linux-gnu bison flex libtool gdb \ sharutils netbase libmpc-dev libmpfr-dev libgmp-dev \ systemtap-sdt-dev autogen expect chrpath zlib1g-dev \ zip binutils-arm-linux-gnueabihf libc6-dev:armhf \ linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf \ libfontconfig1-dev:armhf libfreetype6-dev:armhf libssl-dev:armhf \ - liblttng-ust0:armhf libstdc++-9-dev:armhf + liblttng-ust1:armhf libstdc++-12-dev:armhf \ + && apt-get clean autoclean -yqq \ + && apt-get autoremove -yqq \ + && rm -rf /var/lib/apt/lists/* # Link to build script RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.armhf /build.sh diff --git a/deployment/Dockerfile.docker.amd64 b/deployment/Dockerfile.docker.amd64 index 1749ca563..ca16a08fb 100644 --- a/deployment/Dockerfile.docker.amd64 +++ b/deployment/Dockerfile.docker.amd64 @@ -1,13 +1,12 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim +ARG DOTNET_VERSION=8.0 + +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim ARG SOURCE_DIR=/src ARG ARTIFACT_DIR=/jellyfin WORKDIR ${SOURCE_DIR} COPY . . - ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 -# because of changes in docker and systemd we need to not build in parallel at the moment -# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting -RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-x64 -p:DebugSymbols=false -p:DebugType=none +RUN dotnet publish Jellyfin.Server --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-x64 -p:DebugSymbols=false -p:DebugType=none diff --git a/deployment/Dockerfile.docker.arm64 b/deployment/Dockerfile.docker.arm64 index bbddb61e4..6e0f7d18e 100644 --- a/deployment/Dockerfile.docker.arm64 +++ b/deployment/Dockerfile.docker.arm64 @@ -1,13 +1,12 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim +ARG DOTNET_VERSION=8.0 + +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim ARG SOURCE_DIR=/src ARG ARTIFACT_DIR=/jellyfin WORKDIR ${SOURCE_DIR} COPY . . - ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 -# because of changes in docker and systemd we need to not build in parallel at the moment -# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting -RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm64 -p:DebugSymbols=false -p:DebugType=none +RUN dotnet publish Jellyfin.Server --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm64 -p:DebugSymbols=false -p:DebugType=none diff --git a/deployment/Dockerfile.docker.armhf b/deployment/Dockerfile.docker.armhf index 3de1d6887..44fb705e6 100644 --- a/deployment/Dockerfile.docker.armhf +++ b/deployment/Dockerfile.docker.armhf @@ -1,13 +1,12 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim +ARG DOTNET_VERSION=8.0 + +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim ARG SOURCE_DIR=/src ARG ARTIFACT_DIR=/jellyfin WORKDIR ${SOURCE_DIR} COPY . . - ENV DOTNET_CLI_TELEMETRY_OPTOUT=1 -# because of changes in docker and systemd we need to not build in parallel at the moment -# see https://success.docker.com/article/how-to-reserve-resource-temporarily-unavailable-errors-due-to-tasksmax-setting -RUN dotnet publish Jellyfin.Server --disable-parallel --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm -p:DebugSymbols=false -p:DebugType=none +RUN dotnet publish Jellyfin.Server --configuration Release --output="${ARTIFACT_DIR}" --self-contained --runtime linux-arm -p:DebugSymbols=false -p:DebugType=none diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64 index 66ead37d7..75a6d1e64 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -1,7 +1,9 @@ FROM fedora:39 + # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist + # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist @@ -9,21 +11,26 @@ ENV IS_DOCKER=YES # Prepare Fedora environment RUN dnf update -yq \ - && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget make + && dnf install -yq \ + @buildsys-build rpmdevtools git \ + dnf-plugins-core libcurl-devel fontconfig-devel \ + freetype-devel openssl-devel glibc-devel \ + libicu-devel systemd wget make \ + && dnf clean all \ + && rm -rf /var/cache/dnf # Install DotNET SDK RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ - && mkdir -p dotnet-sdk \ - && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ - && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet - + && mkdir -p dotnet-sdk \ + && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ + && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet # Create symlinks and directories RUN ln -sf ${SOURCE_DIR}/deployment/build.fedora.amd64 /build.sh \ - && mkdir -p ${SOURCE_DIR}/SPECS \ - && ln -s ${SOURCE_DIR}/fedora/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \ - && mkdir -p ${SOURCE_DIR}/SOURCES \ - && ln -s ${SOURCE_DIR}/fedora ${SOURCE_DIR}/SOURCES + && mkdir -p ${SOURCE_DIR}/SPECS \ + && ln -s ${SOURCE_DIR}/fedora/jellyfin.spec ${SOURCE_DIR}/SPECS/jellyfin.spec \ + && mkdir -p ${SOURCE_DIR}/SOURCES \ + && ln -s ${SOURCE_DIR}/fedora ${SOURCE_DIR}/SOURCES VOLUME ${SOURCE_DIR}/ diff --git a/deployment/Dockerfile.linux.amd64 b/deployment/Dockerfile.linux.amd64 index 386f7cefe..6b8de3773 100644 --- a/deployment/Dockerfile.linux.amd64 +++ b/deployment/Dockerfile.linux.amd64 @@ -1,7 +1,11 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim +ARG DOTNET_VERSION=8.0 + +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim + # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist + # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist @@ -11,10 +15,13 @@ ENV IS_DOCKER=YES # Prepare Debian build environment RUN apt-get update -yqq \ - && apt-get install -yqq --no-install-recommends \ + && apt-get install --no-install-recommends -yqq \ debhelper gnupg devscripts unzip \ mmv libcurl4-openssl-dev libfontconfig1-dev \ - libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 + libfreetype6-dev libssl-dev libssl3 liblttng-ust1 \ + && apt-get clean autoclean -yqq \ + && apt-get autoremove -yqq \ + && rm -rf /var/lib/apt/lists/* # Link to docker-build script RUN ln -sf ${SOURCE_DIR}/deployment/build.linux.amd64 /build.sh diff --git a/deployment/Dockerfile.linux.amd64-musl b/deployment/Dockerfile.linux.amd64-musl index 56c877333..49d98da2a 100644 --- a/deployment/Dockerfile.linux.amd64-musl +++ b/deployment/Dockerfile.linux.amd64-musl @@ -1,7 +1,11 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim +ARG DOTNET_VERSION=8.0 + +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim + # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist + # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist @@ -11,10 +15,13 @@ ENV IS_DOCKER=YES # Prepare Debian build environment RUN apt-get update -yqq \ - && apt-get install -yqq --no-install-recommends \ + && apt-get install --no-install-recommends -yqq \ debhelper gnupg devscripts unzip \ mmv libcurl4-openssl-dev libfontconfig1-dev \ - libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 + libfreetype6-dev libssl-dev libssl3 liblttng-ust1 \ + && apt-get clean autoclean -yqq \ + && apt-get autoremove -yqq \ + && rm -rf /var/lib/apt/lists/* # Link to docker-build script RUN ln -sf ${SOURCE_DIR}/deployment/build.linux.amd64-musl /build.sh diff --git a/deployment/Dockerfile.linux.arm64 b/deployment/Dockerfile.linux.arm64 index c9692c440..aba33c8b2 100644 --- a/deployment/Dockerfile.linux.arm64 +++ b/deployment/Dockerfile.linux.arm64 @@ -1,7 +1,11 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim +ARG DOTNET_VERSION=8.0 + +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim + # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist + # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist @@ -11,10 +15,13 @@ ENV IS_DOCKER=YES # Prepare Debian build environment RUN apt-get update -yqq \ - && apt-get install -yqq --no-install-recommends \ + && apt-get install --no-install-recommends -yqq \ debhelper gnupg devscripts unzip \ mmv libcurl4-openssl-dev libfontconfig1-dev \ - libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 + libfreetype6-dev libssl-dev libssl3 liblttng-ust1 \ + && apt-get clean autoclean -yqq \ + && apt-get autoremove -yqq \ + && rm -rf /var/lib/apt/lists/* # Link to docker-build script RUN ln -sf ${SOURCE_DIR}/deployment/build.linux.arm64 /build.sh diff --git a/deployment/Dockerfile.linux.armhf b/deployment/Dockerfile.linux.armhf index 230461556..247f75615 100644 --- a/deployment/Dockerfile.linux.armhf +++ b/deployment/Dockerfile.linux.armhf @@ -1,7 +1,11 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim +ARG DOTNET_VERSION=8.0 + +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim + # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist + # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist @@ -11,10 +15,13 @@ ENV IS_DOCKER=YES # Prepare Debian build environment RUN apt-get update -yqq \ - && apt-get install -yqq --no-install-recommends \ + && apt-get install --no-install-recommends -yqq \ debhelper gnupg devscripts unzip \ mmv libcurl4-openssl-dev libfontconfig1-dev \ - libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 + libfreetype6-dev libssl-dev libssl3 liblttng-ust1 \ + && apt-get clean autoclean -yqq \ + && apt-get autoremove -yqq \ + && rm -rf /var/lib/apt/lists/* # Link to docker-build script RUN ln -sf ${SOURCE_DIR}/deployment/build.linux.armhf /build.sh diff --git a/deployment/Dockerfile.linux.musl-linux-arm64 b/deployment/Dockerfile.linux.musl-linux-arm64 index 240d09186..a6e1ba217 100644 --- a/deployment/Dockerfile.linux.musl-linux-arm64 +++ b/deployment/Dockerfile.linux.musl-linux-arm64 @@ -1,7 +1,11 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim +ARG DOTNET_VERSION=8.0 + +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim + # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist + # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist @@ -11,10 +15,13 @@ ENV IS_DOCKER=YES # Prepare Debian build environment RUN apt-get update -yqq \ - && apt-get install -yqq --no-install-recommends \ - apt-transport-https debhelper gnupg devscripts unzip \ + && apt-get install --no-install-recommends -yqq \ + debhelper gnupg devscripts unzip \ mmv libcurl4-openssl-dev libfontconfig1-dev \ - libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 + libfreetype6-dev libssl-dev libssl3 liblttng-ust1 \ + && apt-get clean autoclean -yqq \ + && apt-get autoremove -yqq \ + && rm -rf /var/lib/apt/lists/* # Link to docker-build script RUN ln -sf ${SOURCE_DIR}/deployment/build.linux.musl-linux-arm64 /build.sh diff --git a/deployment/Dockerfile.macos.amd64 b/deployment/Dockerfile.macos.amd64 index 1b054dfc4..45980c363 100644 --- a/deployment/Dockerfile.macos.amd64 +++ b/deployment/Dockerfile.macos.amd64 @@ -1,7 +1,11 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim +ARG DOTNET_VERSION=8.0 + +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim + # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist + # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist @@ -11,10 +15,13 @@ ENV IS_DOCKER=YES # Prepare Debian build environment RUN apt-get update -yqq \ - && apt-get install -yqq --no-install-recommends \ + && apt-get install --no-install-recommends -yqq \ debhelper gnupg devscripts \ mmv libcurl4-openssl-dev libfontconfig1-dev \ - libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 + libfreetype6-dev libssl-dev libssl3 liblttng-ust1 \ + && apt-get clean autoclean -yqq \ + && apt-get autoremove -yqq \ + && rm -rf /var/lib/apt/lists/* # Link to docker-build script RUN ln -sf ${SOURCE_DIR}/deployment/build.macos.amd64 /build.sh diff --git a/deployment/Dockerfile.macos.arm64 b/deployment/Dockerfile.macos.arm64 index 07e18da55..ee3a813dd 100644 --- a/deployment/Dockerfile.macos.arm64 +++ b/deployment/Dockerfile.macos.arm64 @@ -1,7 +1,11 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim +ARG DOTNET_VERSION=8.0 + +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim + # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist + # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist @@ -11,10 +15,13 @@ ENV IS_DOCKER=YES # Prepare Debian build environment RUN apt-get update -yqq \ - && apt-get install -yqq --no-install-recommends \ + && apt-get install --no-install-recommends -yqq \ debhelper gnupg devscripts \ mmv libcurl4-openssl-dev libfontconfig1-dev \ - libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 + libfreetype6-dev libssl-dev libssl3 liblttng-ust1 \ + && apt-get clean autoclean -yqq \ + && apt-get autoremove -yqq \ + && rm -rf /var/lib/apt/lists/* # Link to docker-build script RUN ln -sf ${SOURCE_DIR}/deployment/build.macos.arm64 /build.sh diff --git a/deployment/Dockerfile.portable b/deployment/Dockerfile.portable index 36135f7a6..0ab1b1914 100644 --- a/deployment/Dockerfile.portable +++ b/deployment/Dockerfile.portable @@ -1,7 +1,11 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim +ARG DOTNET_VERSION=8.0 + +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim + # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist + # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist @@ -10,10 +14,13 @@ ENV IS_DOCKER=YES # Prepare Debian build environment RUN apt-get update -yqq \ - && apt-get install -yqq --no-install-recommends \ + && apt-get install --no-install-recommends -yqq \ debhelper gnupg devscripts \ mmv libcurl4-openssl-dev libfontconfig1-dev \ - libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 + libfreetype6-dev libssl-dev libssl3 liblttng-ust1 \ + && apt-get clean autoclean -yqq \ + && apt-get autoremove -yqq \ + && rm -rf /var/lib/apt/lists/* # Link to docker-build script RUN ln -sf ${SOURCE_DIR}/deployment/build.portable /build.sh diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64 index 84fa2028e..2326d3e85 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -1,7 +1,11 @@ -FROM ubuntu:bionic +ARG DOTNET_VERSION=8.0 + +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-jammy + # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist + # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist @@ -11,16 +15,13 @@ ENV IS_DOCKER=YES # Prepare Debian build environment RUN apt-get update -yqq \ - && apt-get install -yqq --no-install-recommends \ + && apt-get install --no-install-recommends -yqq \ debhelper gnupg wget ca-certificates devscripts \ mmv build-essential libcurl4-openssl-dev libfontconfig1-dev \ - libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 - -# Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ - && mkdir -p dotnet-sdk \ - && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ - && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet + libfreetype6-dev libssl-dev libssl3 liblttng-ust1 \ + && apt-get clean autoclean -yqq \ + && apt-get autoremove -yqq \ + && rm -rf /var/lib/apt/lists/* # Link to build script RUN ln -sf ${SOURCE_DIR}/deployment/build.ubuntu.amd64 /build.sh diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64 index ca3aa3508..461a287a1 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -1,7 +1,11 @@ -FROM ubuntu:bionic +ARG DOTNET_VERSION=8.0 + +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-jammy + # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist + # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist @@ -11,39 +15,36 @@ ENV IS_DOCKER=YES # Prepare Debian build environment RUN apt-get update -yqq \ - && apt-get install -yqq --no-install-recommends \ + && apt-get install --no-install-recommends -yqq \ debhelper gnupg wget ca-certificates devscripts \ mmv build-essential lsb-release -# Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ - && mkdir -p dotnet-sdk \ - && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ - && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet - # Prepare the cross-toolchain RUN rm /etc/apt/sources.list \ - && export CODENAME="$( lsb_release -c -s )" \ - && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME} main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ - && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-updates main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ - && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-backports main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ - && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-security main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ - && echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME} main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \ - && echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME}-updates main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \ - && echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME}-backports main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \ - && echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME}-security main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \ - && dpkg --add-architecture arm64 \ - && apt-get update -yqq \ - && apt-get install -yqq --no-install-recommends cross-gcc-dev \ - && TARGET_LIST="arm64" cross-gcc-gensource 6 \ - && cd cross-gcc-packages-amd64/cross-gcc-6-arm64 \ - && ln -fs /usr/share/zoneinfo/America/Toronto /etc/localtime \ - && apt-get install -yqq --no-install-recommends \ - gcc-6-source libstdc++6-arm64-cross binutils-aarch64-linux-gnu \ - bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev \ + && export CODENAME="$( lsb_release -c -s )" \ + && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME} main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ + && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-updates main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ + && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-backports main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ + && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-security main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ + && echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME} main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \ + && echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME}-updates main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \ + && echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME}-backports main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \ + && echo "deb [arch=arm64] http://ports.ubuntu.com/ ${CODENAME}-security main restricted universe multiverse" >>/etc/apt/sources.list.d/arm64.list \ + && dpkg --add-architecture arm64 \ + && apt-get update -yqq \ + && apt-get install --no-install-recommends -yqq cross-gcc-dev \ + && TARGET_LIST="arm64" cross-gcc-gensource 12 \ + && cd cross-gcc-packages-amd64/cross-gcc-12-arm64 \ + && ln -fs /usr/share/zoneinfo/America/Toronto /etc/localtime \ + && apt-get install --no-install-recommends -yqq \ + gcc-12-source libstdc++6-arm64-cross binutils-aarch64-linux-gnu \ + bison flex libtool gdb sharutils netbase libmpc-dev \ libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev \ zip libc6-dev:arm64 linux-libc-dev:arm64 libgcc1:arm64 libcurl4-openssl-dev:arm64 \ - libfontconfig1-dev:arm64 libfreetype6-dev:arm64 liblttng-ust0:arm64 libstdc++6:arm64 libssl-dev:arm64 + libfontconfig1-dev:arm64 libfreetype6-dev:arm64 liblttng-ust1:arm64 libstdc++6:arm64 libssl-dev:arm64 \ + && apt-get clean autoclean -yqq \ + && apt-get autoremove -yqq \ + && rm -rf /var/lib/apt/lists/* # Link to build script RUN ln -sf ${SOURCE_DIR}/deployment/build.ubuntu.arm64 /build.sh diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf index e52b7fba3..83fe32acf 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -1,7 +1,11 @@ -FROM ubuntu:bionic +ARG DOTNET_VERSION=8.0 + +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-jammy + # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist + # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist @@ -11,39 +15,36 @@ ENV IS_DOCKER=YES # Prepare Debian build environment RUN apt-get update -yqq \ - && apt-get install -yqq --no-install-recommends \ + && apt-get install --no-install-recommends -yqq \ debhelper gnupg wget ca-certificates devscripts \ mmv build-essential lsb-release -# Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/5226a5fa-8c0b-474f-b79a-8984ad7c5beb/3113ccbf789c9fd29972835f0f334b7a/dotnet-sdk-8.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ - && mkdir -p dotnet-sdk \ - && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ - && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet - # Prepare the cross-toolchain RUN rm /etc/apt/sources.list \ - && export CODENAME="$( lsb_release -c -s )" \ - && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME} main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ - && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-updates main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ - && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-backports main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ - && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-security main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ - && echo "deb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME} main restricted universe multiverse" >>/etc/apt/sources.list.d/armhf.list \ - && echo "deb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME}-updates main restricted universe multiverse" >>/etc/apt/sources.list.d/armhf.list \ - && echo "deb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME}-backports main restricted universe multiverse" >>/etc/apt/sources.list.d/armhf.list \ - && echo "deb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME}-security main restricted universe multiverse" >>/etc/apt/sources.list.d/armhf.list \ - && dpkg --add-architecture armhf \ - && apt-get update -yqq \ - && apt-get install -yqq cross-gcc-dev \ - && TARGET_LIST="armhf" cross-gcc-gensource 6 \ - && cd cross-gcc-packages-amd64/cross-gcc-6-armhf \ - && ln -fs /usr/share/zoneinfo/America/Toronto /etc/localtime \ - && apt-get install -yqq --no-install-recommends \ - gcc-6-source libstdc++6-armhf-cross binutils-arm-linux-gnueabihf \ - bison flex libtool gdb sharutils netbase libcloog-isl-dev libmpc-dev \ + && export CODENAME="$( lsb_release -c -s )" \ + && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME} main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ + && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-updates main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ + && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-backports main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ + && echo "deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ ${CODENAME}-security main restricted universe multiverse" >>/etc/apt/sources.list.d/amd64.list \ + && echo "deb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME} main restricted universe multiverse" >>/etc/apt/sources.list.d/armhf.list \ + && echo "deb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME}-updates main restricted universe multiverse" >>/etc/apt/sources.list.d/armhf.list \ + && echo "deb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME}-backports main restricted universe multiverse" >>/etc/apt/sources.list.d/armhf.list \ + && echo "deb [arch=armhf] http://ports.ubuntu.com/ ${CODENAME}-security main restricted universe multiverse" >>/etc/apt/sources.list.d/armhf.list \ + && dpkg --add-architecture armhf \ + && apt-get update -yqq \ + && apt-get install --no-install-recommends -yqq cross-gcc-dev \ + && TARGET_LIST="armhf" cross-gcc-gensource 12 \ + && cd cross-gcc-packages-amd64/cross-gcc-12-armhf \ + && ln -fs /usr/share/zoneinfo/America/Toronto /etc/localtime \ + && apt-get install --no-install-recommends -yqq \ + gcc-12-source libstdc++6-armhf-cross binutils-arm-linux-gnueabihf \ + bison flex libtool gdb sharutils netbase libmpc-dev \ libmpfr-dev libgmp-dev systemtap-sdt-dev autogen expect chrpath zlib1g-dev \ zip libc6-dev:armhf linux-libc-dev:armhf libgcc1:armhf libcurl4-openssl-dev:armhf \ - libfontconfig1-dev:armhf libfreetype6-dev:armhf liblttng-ust0:armhf libstdc++6:armhf libssl-dev:armhf + libfontconfig1-dev:armhf libfreetype6-dev:armhf liblttng-ust1:armhf libstdc++6:armhf libssl-dev:armhf \ + && apt-get clean autoclean -yqq \ + && apt-get autoremove -yqq \ + && rm -rf /var/lib/apt/lists/* # Link to build script RUN ln -sf ${SOURCE_DIR}/deployment/build.debian.armhf /build.sh diff --git a/deployment/Dockerfile.windows.amd64 b/deployment/Dockerfile.windows.amd64 index 08587aa7e..358fb620a 100644 --- a/deployment/Dockerfile.windows.amd64 +++ b/deployment/Dockerfile.windows.amd64 @@ -1,7 +1,11 @@ -FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim +ARG DOTNET_VERSION=8.0 + +FROM mcr.microsoft.com/dotnet/sdk:${DOTNET_VERSION}-bookworm-slim + # Docker build arguments ARG SOURCE_DIR=/jellyfin ARG ARTIFACT_DIR=/dist + # Docker run environment ENV SOURCE_DIR=/jellyfin ENV ARTIFACT_DIR=/dist @@ -10,10 +14,13 @@ ENV IS_DOCKER=YES # Prepare Debian build environment RUN apt-get update -yqq \ - && apt-get install -yqq --no-install-recommends \ + && apt-get install --no-install-recommends -yqq \ debhelper gnupg devscripts unzip \ mmv libcurl4-openssl-dev libfontconfig1-dev \ - libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 zip + libfreetype6-dev libssl-dev libssl3 liblttng-ust1 zip \ + && apt-get clean autoclean -yqq \ + && apt-get autoremove -yqq \ + && rm -rf /var/lib/apt/lists/* # Link to docker-build script RUN ln -sf ${SOURCE_DIR}/deployment/build.windows.amd64 /build.sh diff --git a/deployment/build.centos.amd64 b/deployment/build.centos.amd64 index 0374624d8..26be377f1 100755 --- a/deployment/build.centos.amd64 +++ b/deployment/build.centos.amd64 @@ -1,16 +1,16 @@ #!/bin/bash -#= CentOS/RHEL 7+ amd64 .rpm +#= CentOS/RHEL 9+ amd64 .rpm set -o errexit set -o xtrace # Move to source directory -pushd ${SOURCE_DIR} +pushd "${SOURCE_DIR}" if [[ ${IS_DOCKER} == YES ]]; then # Remove BuildRequires for dotnet, since it's installed manually - pushd fedora + pushd centos cp -a jellyfin.spec /tmp/spec.orig sed -i 's/BuildRequires: dotnet/# BuildRequires: dotnet/' jellyfin.spec @@ -20,7 +20,7 @@ fi # Modify changelog to unstable configuration if IS_UNSTABLE if [[ ${IS_UNSTABLE} == 'yes' ]]; then - pushd fedora + pushd centos PR_ID=$( git log --grep 'Merge pull request' --oneline --single-worktree --first-parent | head -1 | grep --color=none -Eo '#[0-9]+' | tr -d '#' ) @@ -35,23 +35,23 @@ EOF fi # Build RPM -make -f fedora/Makefile srpm outdir=/root/rpmbuild/SRPMS +make -f centos/Makefile srpm outdir=/root/rpmbuild/SRPMS rpmbuild --rebuild -bb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm # Move the artifacts out -mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/ +mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm "${ARTIFACT_DIR}/" if [[ ${IS_DOCKER} == YES ]]; then - chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} + chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}" fi -rm -f fedora/jellyfin*.tar.gz +rm -f centos/jellyfin*.tar.gz if [[ ${IS_DOCKER} == YES ]]; then - pushd fedora + pushd centos cp -a /tmp/spec.orig jellyfin.spec - chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} + chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}" popd fi diff --git a/deployment/build.debian.amd64 b/deployment/build.debian.amd64 index 7e968192b..350b22a85 100755 --- a/deployment/build.debian.amd64 +++ b/deployment/build.debian.amd64 @@ -1,18 +1,12 @@ #!/bin/bash -#= Debian 10+ amd64 .deb +#= Debian 12+ amd64 .deb set -o errexit set -o xtrace # Move to source directory -pushd ${SOURCE_DIR} - -if [[ ${IS_DOCKER} == YES ]]; then - # Remove build-dep for dotnet-sdk-8.0, since it's installed manually - cp -a debian/control /tmp/control.orig - sed -i '/dotnet-sdk-8.0,/d' debian/control -fi +pushd "${SOURCE_DIR}" # Modify changelog to unstable configuration if IS_UNSTABLE if [[ ${IS_UNSTABLE} == 'yes' ]]; then @@ -32,12 +26,12 @@ fi # Build DEB dpkg-buildpackage -us -uc --pre-clean --post-clean -mkdir -p ${ARTIFACT_DIR}/ -mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} ${ARTIFACT_DIR}/ +mkdir -p "${ARTIFACT_DIR}/" +mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} "${ARTIFACT_DIR}/" if [[ ${IS_DOCKER} == YES ]]; then cp -a /tmp/control.orig debian/control - chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} + chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}" fi popd diff --git a/deployment/build.debian.arm64 b/deployment/build.debian.arm64 index 7b7b603d6..0dfca0ab4 100755 --- a/deployment/build.debian.arm64 +++ b/deployment/build.debian.arm64 @@ -1,18 +1,12 @@ #!/bin/bash -#= Debian 10+ arm64 .deb +#= Debian 12+ arm64 .deb set -o errexit set -o xtrace # Move to source directory -pushd ${SOURCE_DIR} - -if [[ ${IS_DOCKER} == YES ]]; then - # Remove build-dep for dotnet-sdk-8.0, since it's installed manually - cp -a debian/control /tmp/control.orig - sed -i '/dotnet-sdk-8.0,/d' debian/control -fi +pushd "${SOURCE_DIR}" # Modify changelog to unstable configuration if IS_UNSTABLE if [[ ${IS_UNSTABLE} == 'yes' ]]; then @@ -33,12 +27,12 @@ fi export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH} dpkg-buildpackage -us -uc -a arm64 --pre-clean --post-clean -mkdir -p ${ARTIFACT_DIR}/ -mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} ${ARTIFACT_DIR}/ +mkdir -p "${ARTIFACT_DIR}/" +mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} "${ARTIFACT_DIR}/" if [[ ${IS_DOCKER} == YES ]]; then cp -a /tmp/control.orig debian/control - chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} + chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}" fi popd diff --git a/deployment/build.debian.armhf b/deployment/build.debian.armhf index 3d894ba20..0ab9e2f9a 100755 --- a/deployment/build.debian.armhf +++ b/deployment/build.debian.armhf @@ -1,18 +1,12 @@ #!/bin/bash -#= Debian 10+ arm64 .deb +#= Debian 12+ arm64 .deb set -o errexit set -o xtrace # Move to source directory -pushd ${SOURCE_DIR} - -if [[ ${IS_DOCKER} == YES ]]; then - # Remove build-dep for dotnet-sdk-8.0, since it's installed manually - cp -a debian/control /tmp/control.orig - sed -i '/dotnet-sdk-8.0,/d' debian/control -fi +pushd "${SOURCE_DIR}" # Modify changelog to unstable configuration if IS_UNSTABLE if [[ ${IS_UNSTABLE} == 'yes' ]]; then @@ -33,12 +27,12 @@ fi export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH} dpkg-buildpackage -us -uc -a armhf --pre-clean --post-clean -mkdir -p ${ARTIFACT_DIR}/ -mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} ${ARTIFACT_DIR}/ +mkdir -p "${ARTIFACT_DIR}/" +mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} "${ARTIFACT_DIR}/" if [[ ${IS_DOCKER} == YES ]]; then cp -a /tmp/control.orig debian/control - chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} + chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}" fi popd diff --git a/deployment/build.fedora.amd64 b/deployment/build.fedora.amd64 index 1b629289f..2b4ec2a9c 100755 --- a/deployment/build.fedora.amd64 +++ b/deployment/build.fedora.amd64 @@ -1,12 +1,12 @@ #!/bin/bash -#= Fedora 29+ amd64 .rpm +#= Fedora 39+ amd64 .rpm set -o errexit set -o xtrace # Move to source directory -pushd ${SOURCE_DIR} +pushd "${SOURCE_DIR}" if [[ ${IS_DOCKER} == YES ]]; then # Remove BuildRequires for dotnet, since it's installed manually @@ -39,10 +39,10 @@ make -f fedora/Makefile srpm outdir=/root/rpmbuild/SRPMS rpmbuild -rb /root/rpmbuild/SRPMS/jellyfin-*.src.rpm # Move the artifacts out -mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm ${ARTIFACT_DIR}/ +mv /root/rpmbuild/RPMS/x86_64/jellyfin-*.rpm /root/rpmbuild/SRPMS/jellyfin-*.src.rpm "${ARTIFACT_DIR}/" if [[ ${IS_DOCKER} == YES ]]; then - chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} + chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}" fi rm -f fedora/jellyfin*.tar.gz @@ -51,7 +51,7 @@ if [[ ${IS_DOCKER} == YES ]]; then pushd fedora cp -a /tmp/spec.orig jellyfin.spec - chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} + chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}" popd fi diff --git a/deployment/build.linux.amd64 b/deployment/build.linux.amd64 index 05059e4ed..2998d2f9e 100755 --- a/deployment/build.linux.amd64 +++ b/deployment/build.linux.amd64 @@ -6,7 +6,7 @@ set -o errexit set -o xtrace # Move to source directory -pushd ${SOURCE_DIR} +pushd "${SOURCE_DIR}" # Get version if [[ ${IS_UNSTABLE} == 'yes' ]]; then @@ -16,16 +16,16 @@ else fi # Build archives -dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-x64 --output dist/jellyfin-server_${version}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true -tar -czf jellyfin-server_${version}_linux-amd64.tar.gz -C dist jellyfin-server_${version} -rm -rf dist/jellyfin-server_${version} +dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-x64 --output dist/jellyfin-server_"${version}"/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true +tar -czf jellyfin-server_"${version}"_linux-amd64.tar.gz -C dist jellyfin-server_"${version}" +rm -rf dist/jellyfin-server_"${version}" # Move the artifacts out -mkdir -p ${ARTIFACT_DIR}/ -mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/ +mkdir -p "${ARTIFACT_DIR}/" +mv jellyfin[-_]*.tar.gz "${ARTIFACT_DIR}/" if [[ ${IS_DOCKER} == YES ]]; then - chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} + chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}" fi popd diff --git a/deployment/build.linux.amd64-musl b/deployment/build.linux.amd64-musl index 0ee4b05fb..0fa176465 100755 --- a/deployment/build.linux.amd64-musl +++ b/deployment/build.linux.amd64-musl @@ -6,7 +6,7 @@ set -o errexit set -o xtrace # Move to source directory -pushd ${SOURCE_DIR} +pushd "${SOURCE_DIR}" # Get version if [[ ${IS_UNSTABLE} == 'yes' ]]; then @@ -16,16 +16,16 @@ else fi # Build archives -dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-musl-x64 --output dist/jellyfin-server_${version}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true -tar -czf jellyfin-server_${version}_linux-amd64-musl.tar.gz -C dist jellyfin-server_${version} -rm -rf dist/jellyfin-server_${version} +dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-musl-x64 --output dist/jellyfin-server_"${version}"/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true +tar -czf jellyfin-server_"${version}"_linux-amd64-musl.tar.gz -C dist jellyfin-server_"${version}" +rm -rf dist/jellyfin-server_"${version}" # Move the artifacts out -mkdir -p ${ARTIFACT_DIR}/ -mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/ +mkdir -p "${ARTIFACT_DIR}/" +mv jellyfin[-_]*.tar.gz "${ARTIFACT_DIR}/" if [[ ${IS_DOCKER} == YES ]]; then - chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} + chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}" fi popd diff --git a/deployment/build.linux.arm64 b/deployment/build.linux.arm64 index 6e36db0eb..dc44ca330 100755 --- a/deployment/build.linux.arm64 +++ b/deployment/build.linux.arm64 @@ -6,7 +6,7 @@ set -o errexit set -o xtrace # Move to source directory -pushd ${SOURCE_DIR} +pushd "${SOURCE_DIR}" # Get version if [[ ${IS_UNSTABLE} == 'yes' ]]; then @@ -16,16 +16,16 @@ else fi # Build archives -dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-arm64 --output dist/jellyfin-server_${version}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true -tar -czf jellyfin-server_${version}_linux-arm64.tar.gz -C dist jellyfin-server_${version} -rm -rf dist/jellyfin-server_${version} +dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-arm64 --output dist/jellyfin-server_"${version}"/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true +tar -czf jellyfin-server_"${version}"_linux-arm64.tar.gz -C dist jellyfin-server_"${version}" +rm -rf dist/jellyfin-server_"${version}" # Move the artifacts out -mkdir -p ${ARTIFACT_DIR}/ -mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/ +mkdir -p "${ARTIFACT_DIR}/" +mv jellyfin[-_]*.tar.gz "${ARTIFACT_DIR}/" if [[ ${IS_DOCKER} == YES ]]; then - chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} + chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}" fi popd diff --git a/deployment/build.linux.armhf b/deployment/build.linux.armhf index f83eeebf1..f9de9ff0a 100755 --- a/deployment/build.linux.armhf +++ b/deployment/build.linux.armhf @@ -6,7 +6,7 @@ set -o errexit set -o xtrace # Move to source directory -pushd ${SOURCE_DIR} +pushd "${SOURCE_DIR}" # Get version if [[ ${IS_UNSTABLE} == 'yes' ]]; then @@ -16,16 +16,16 @@ else fi # Build archives -dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-arm --output dist/jellyfin-server_${version}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true -tar -czf jellyfin-server_${version}_linux-armhf.tar.gz -C dist jellyfin-server_${version} -rm -rf dist/jellyfin-server_${version} +dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-arm --output dist/jellyfin-server_"${version}"/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true +tar -czf jellyfin-server_"${version}"_linux-armhf.tar.gz -C dist jellyfin-server_"${version}" +rm -rf dist/jellyfin-server_"${version}" # Move the artifacts out -mkdir -p ${ARTIFACT_DIR}/ -mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/ +mkdir -p "${ARTIFACT_DIR}/" +mv jellyfin[-_]*.tar.gz "${ARTIFACT_DIR}/" if [[ ${IS_DOCKER} == YES ]]; then - chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} + chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}" fi popd diff --git a/deployment/build.linux.musl-linux-arm64 b/deployment/build.linux.musl-linux-arm64 index 38826ae7f..ae9ab010f 100755 --- a/deployment/build.linux.musl-linux-arm64 +++ b/deployment/build.linux.musl-linux-arm64 @@ -6,7 +6,7 @@ set -o errexit set -o xtrace # Move to source directory -pushd ${SOURCE_DIR} +pushd "${SOURCE_DIR}" # Get version if [[ ${IS_UNSTABLE} == 'yes' ]]; then @@ -16,16 +16,16 @@ else fi # Build archives -dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-musl-arm64 --output dist/jellyfin-server_${version}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true -tar -czf jellyfin-server_${version}_linux-arm64-musl.tar.gz -C dist jellyfin-server_${version} -rm -rf dist/jellyfin-server_${version} +dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime linux-musl-arm64 --output dist/jellyfin-server_"${version}"/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true +tar -czf jellyfin-server_"${version}"_linux-arm64-musl.tar.gz -C dist jellyfin-server_"${version}" +rm -rf dist/jellyfin-server_"${version}" # Move the artifacts out -mkdir -p ${ARTIFACT_DIR}/ -mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/ +mkdir -p "${ARTIFACT_DIR}/" +mv jellyfin[-_]*.tar.gz "${ARTIFACT_DIR}/" if [[ ${IS_DOCKER} == YES ]]; then - chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} + chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}" fi popd diff --git a/deployment/build.macos.amd64 b/deployment/build.macos.amd64 index eac353877..81e0f43f6 100755 --- a/deployment/build.macos.amd64 +++ b/deployment/build.macos.amd64 @@ -6,7 +6,7 @@ set -o errexit set -o xtrace # Move to source directory -pushd ${SOURCE_DIR} +pushd "${SOURCE_DIR}" # Get version if [[ ${IS_UNSTABLE} == 'yes' ]]; then @@ -16,16 +16,16 @@ else fi # Build archives -dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime osx-x64 --output dist/jellyfin-server_${version}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true -tar -czf jellyfin-server_${version}_macos-amd64.tar.gz -C dist jellyfin-server_${version} -rm -rf dist/jellyfin-server_${version} +dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime osx-x64 --output dist/jellyfin-server_"${version}"/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true +tar -czf jellyfin-server_"${version}"_macos-amd64.tar.gz -C dist jellyfin-server_"${version}" +rm -rf dist/jellyfin-server_"${version}" # Move the artifacts out -mkdir -p ${ARTIFACT_DIR}/ -mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/ +mkdir -p "${ARTIFACT_DIR}/" +mv jellyfin[-_]*.tar.gz "${ARTIFACT_DIR}/" if [[ ${IS_DOCKER} == YES ]]; then - chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} + chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}" fi popd diff --git a/deployment/build.macos.arm64 b/deployment/build.macos.arm64 index 42da07e2f..0a6f37ede 100755 --- a/deployment/build.macos.arm64 +++ b/deployment/build.macos.arm64 @@ -6,7 +6,7 @@ set -o errexit set -o xtrace # Move to source directory -pushd ${SOURCE_DIR} +pushd "${SOURCE_DIR}" # Get version if [[ ${IS_UNSTABLE} == 'yes' ]]; then @@ -16,16 +16,16 @@ else fi # Build archives -dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime osx-arm64 --output dist/jellyfin-server_${version}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true -tar -czf jellyfin-server_${version}_macos-arm64.tar.gz -C dist jellyfin-server_${version} -rm -rf dist/jellyfin-server_${version} +dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime osx-arm64 --output dist/jellyfin-server_"${version}"/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true +tar -czf jellyfin-server_"${version}"_macos-arm64.tar.gz -C dist jellyfin-server_"${version}" +rm -rf dist/jellyfin-server_"${version}" # Move the artifacts out -mkdir -p ${ARTIFACT_DIR}/ -mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/ +mkdir -p "${ARTIFACT_DIR}/" +mv jellyfin[-_]*.tar.gz "${ARTIFACT_DIR}/" if [[ ${IS_DOCKER} == YES ]]; then - chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} + chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}" fi popd diff --git a/deployment/build.portable b/deployment/build.portable index 27e5e987f..fad14fccf 100755 --- a/deployment/build.portable +++ b/deployment/build.portable @@ -6,7 +6,7 @@ set -o errexit set -o xtrace # Move to source directory -pushd ${SOURCE_DIR} +pushd "${SOURCE_DIR}" # Get version if [[ ${IS_UNSTABLE} == 'yes' ]]; then @@ -16,16 +16,16 @@ else fi # Build archives -dotnet publish Jellyfin.Server --configuration Release --output dist/jellyfin-server_${version}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=false -tar -czf jellyfin-server_${version}_portable.tar.gz -C dist jellyfin-server_${version} -rm -rf dist/jellyfin-server_${version} +dotnet publish Jellyfin.Server --configuration Release --output dist/jellyfin-server_"${version}"/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=false +tar -czf jellyfin-server_"${version}"_portable.tar.gz -C dist jellyfin-server_"${version}" +rm -rf dist/jellyfin-server_"${version}" # Move the artifacts out -mkdir -p ${ARTIFACT_DIR}/ -mv jellyfin[-_]*.tar.gz ${ARTIFACT_DIR}/ +mkdir -p "${ARTIFACT_DIR}/" +mv jellyfin[-_]*.tar.gz "${ARTIFACT_DIR}/" if [[ ${IS_DOCKER} == YES ]]; then - chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} + chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}" fi popd diff --git a/deployment/build.ubuntu.amd64 b/deployment/build.ubuntu.amd64 index 5f25cb610..6fd87a3ae 100755 --- a/deployment/build.ubuntu.amd64 +++ b/deployment/build.ubuntu.amd64 @@ -1,18 +1,12 @@ #!/bin/bash -#= Ubuntu 18.04+ amd64 .deb +#= Ubuntu 22.04+ amd64 .deb set -o errexit set -o xtrace # Move to source directory -pushd ${SOURCE_DIR} - -if [[ ${IS_DOCKER} == YES ]]; then - # Remove build-dep for dotnet-sdk-8.0, since it's installed manually - cp -a debian/control /tmp/control.orig - sed -i '/dotnet-sdk-8.0,/d' debian/control -fi +pushd "${SOURCE_DIR}" # Modify changelog to unstable configuration if IS_UNSTABLE if [[ ${IS_UNSTABLE} == 'yes' ]]; then @@ -32,12 +26,12 @@ fi # Build DEB dpkg-buildpackage -us -uc --pre-clean --post-clean -mkdir -p ${ARTIFACT_DIR}/ -mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} ${ARTIFACT_DIR}/ +mkdir -p "${ARTIFACT_DIR}/" +mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} "${ARTIFACT_DIR}/" if [[ ${IS_DOCKER} == YES ]]; then cp -a /tmp/control.orig debian/control - chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} + chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}" fi popd diff --git a/deployment/build.ubuntu.arm64 b/deployment/build.ubuntu.arm64 index 334ced997..f783941c7 100755 --- a/deployment/build.ubuntu.arm64 +++ b/deployment/build.ubuntu.arm64 @@ -1,18 +1,12 @@ #!/bin/bash -#= Ubuntu 18.04+ arm64 .deb +#= Ubuntu 22.04+ arm64 .deb set -o errexit set -o xtrace # Move to source directory -pushd ${SOURCE_DIR} - -if [[ ${IS_DOCKER} == YES ]]; then - # Remove build-dep for dotnet-sdk-8.0, since it's installed manually - cp -a debian/control /tmp/control.orig - sed -i '/dotnet-sdk-8.0,/d' debian/control -fi +pushd "${SOURCE_DIR}" # Modify changelog to unstable configuration if IS_UNSTABLE if [[ ${IS_UNSTABLE} == 'yes' ]]; then @@ -33,12 +27,12 @@ fi export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH} dpkg-buildpackage -us -uc -a arm64 --pre-clean --post-clean -mkdir -p ${ARTIFACT_DIR}/ -mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} ${ARTIFACT_DIR}/ +mkdir -p "${ARTIFACT_DIR}/" +mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} "${ARTIFACT_DIR}/" if [[ ${IS_DOCKER} == YES ]]; then cp -a /tmp/control.orig debian/control - chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} + chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}" fi popd diff --git a/deployment/build.ubuntu.armhf b/deployment/build.ubuntu.armhf index 77e33c307..cde6708c5 100755 --- a/deployment/build.ubuntu.armhf +++ b/deployment/build.ubuntu.armhf @@ -1,18 +1,12 @@ #!/bin/bash -#= Ubuntu 18.04+ arm64 .deb +#= Ubuntu 22.04+ arm64 .deb set -o errexit set -o xtrace # Move to source directory -pushd ${SOURCE_DIR} - -if [[ ${IS_DOCKER} == YES ]]; then - # Remove build-dep for dotnet-sdk-8.0, since it's installed manually - cp -a debian/control /tmp/control.orig - sed -i '/dotnet-sdk-8.0,/d' debian/control -fi +pushd "${SOURCE_DIR}" # Modify changelog to unstable configuration if IS_UNSTABLE if [[ ${IS_UNSTABLE} == 'yes' ]]; then @@ -33,12 +27,12 @@ fi export CONFIG_SITE=/etc/dpkg-cross/cross-config.${ARCH} dpkg-buildpackage -us -uc -a armhf --pre-clean --post-clean -mkdir -p ${ARTIFACT_DIR}/ -mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} ${ARTIFACT_DIR}/ +mkdir -p "${ARTIFACT_DIR}/" +mv ../jellyfin*.{deb,dsc,tar.gz,buildinfo,changes} "${ARTIFACT_DIR}/" if [[ ${IS_DOCKER} == YES ]]; then cp -a /tmp/control.orig debian/control - chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} + chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}" fi popd diff --git a/deployment/build.windows.amd64 b/deployment/build.windows.amd64 index 0786358bd..cd07f4e0b 100755 --- a/deployment/build.windows.amd64 +++ b/deployment/build.windows.amd64 @@ -11,7 +11,7 @@ NSSM_URL="http://files.evilt.win/nssm/${NSSM_VERSION}.zip" FFMPEG_URL="https://repo.jellyfin.org/releases/server/windows/ffmpeg/jellyfin-ffmpeg-portable_win64.zip"; # Move to source directory -pushd ${SOURCE_DIR} +pushd "${SOURCE_DIR}" # Get version if [[ ${IS_UNSTABLE} == 'yes' ]]; then @@ -23,30 +23,30 @@ fi output_dir="dist/jellyfin-server_${version}" # Build binary -dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime win-x64 --output ${output_dir}/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true +dotnet publish Jellyfin.Server --configuration Release --self-contained --runtime win-x64 --output "${output_dir}"/ -p:DebugSymbols=false -p:DebugType=none -p:UseAppHost=true # Prepare addins addin_build_dir="$( mktemp -d )" -wget ${NSSM_URL} -O ${addin_build_dir}/nssm.zip -wget ${FFMPEG_URL} -O ${addin_build_dir}/jellyfin-ffmpeg.zip -unzip ${addin_build_dir}/nssm.zip -d ${addin_build_dir} -cp ${addin_build_dir}/${NSSM_VERSION}/win64/nssm.exe ${output_dir}/nssm.exe -unzip ${addin_build_dir}/jellyfin-ffmpeg.zip -d ${addin_build_dir}/jellyfin-ffmpeg -cp ${addin_build_dir}/jellyfin-ffmpeg/* ${output_dir} -rm -rf ${addin_build_dir} +wget ${NSSM_URL} -O "${addin_build_dir}"/nssm.zip +wget ${FFMPEG_URL} -O "${addin_build_dir}"/jellyfin-ffmpeg.zip +unzip "${addin_build_dir}"/nssm.zip -d "${addin_build_dir}" +cp "${addin_build_dir}"/${NSSM_VERSION}/win64/nssm.exe "${output_dir}"/nssm.exe +unzip "${addin_build_dir}"/jellyfin-ffmpeg.zip -d "${addin_build_dir}"/jellyfin-ffmpeg +cp "${addin_build_dir}"/jellyfin-ffmpeg/* "${output_dir}" +rm -rf "${addin_build_dir}" # Create zip package pushd dist -zip -qr jellyfin-server_${version}.portable.zip jellyfin-server_${version} +zip -qr jellyfin-server_"${version}".portable.zip jellyfin-server_"${version}" popd -rm -rf ${output_dir} +rm -rf "${output_dir}" # Move the artifacts out -mkdir -p ${ARTIFACT_DIR}/ -mv dist/jellyfin[-_]*.zip ${ARTIFACT_DIR}/ +mkdir -p "${ARTIFACT_DIR}/" +mv dist/jellyfin[-_]*.zip "${ARTIFACT_DIR}/" if [[ ${IS_DOCKER} == YES ]]; then - chown -Rc $(stat -c %u:%g ${ARTIFACT_DIR}) ${ARTIFACT_DIR} + chown -Rc "$(stat -c %u:%g "${ARTIFACT_DIR}")" "${ARTIFACT_DIR}" fi popd diff --git a/fedora/README.md b/fedora/README.md index d449b51c1..6ea87740f 100644 --- a/fedora/README.md +++ b/fedora/README.md @@ -14,8 +14,10 @@ The RPM package for Fedora/CentOS requires some additional repositories as ffmpe # ffmpeg from RPMfusion free # Fedora $ sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm -# CentOS 7 -$ sudo yum localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-7.noarch.rpm +# CentOS 8 +$ sudo dnf localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-8.noarch.rpm +# CentOS 9 +$ sudo dnf localinstall --nogpgcheck https://download1.rpmfusion.org/free/el/rpmfusion-free-release-9.noarch.rpm ``` ## Building with dotnet @@ -26,8 +28,10 @@ Jellyfin is build with `--self-contained` so no dotnet required for runtime. # dotnet required for building the RPM # Fedora $ sudo dnf copr enable @dotnet-sig/dotnet -# CentOS -$ sudo rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm +# CentOS 8 +$ sudo rpm -Uvh https://packages.microsoft.com/config/rhel/8/packages-microsoft-prod.rpm +# CentOS 9 +$ sudo rpm -Uvh https://packages.microsoft.com/config/rhel/9/packages-microsoft-prod.rpm ``` ## TODO diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec index fb9fb2f7d..5327495ad 100644 --- a/fedora/jellyfin.spec +++ b/fedora/jellyfin.spec @@ -1,10 +1,4 @@ %global debug_package %{nil} -# Set the dotnet runtime -%if 0%{?fedora} -%global dotnet_runtime fedora.%{fedora}-x64 -%else -%global dotnet_runtime centos-x64 -%endif Name: jellyfin Version: 10.9.0 @@ -29,12 +23,6 @@ BuildRequires: libcurl-devel, fontconfig-devel, freetype-devel, openssl-devel, BuildRequires: dotnet-runtime-8.0, dotnet-sdk-8.0 Requires: %{name}-server = %{version}-%{release}, %{name}-web = %{version}-%{release} -# Temporary (hopefully?) fix for https://github.com/jellyfin/jellyfin/issues/7471 -%if 0%{?fedora} >= 36 -%global __requires_exclude ^liblttng-ust\\.so\\.0.*$ -%endif - - %description Jellyfin is a free software media system that puts you in control of managing and streaming your media. @@ -66,14 +54,14 @@ the Jellyfin server to bind to ports 80 and/or 443 for example. export DOTNET_CLI_TELEMETRY_OPTOUT=1 export PATH=$PATH:/usr/local/bin # cannot use --output due to https://github.com/dotnet/sdk/issues/22220 -dotnet publish --configuration Release --self-contained --runtime %{dotnet_runtime} \ +dotnet publish --configuration Release --self-contained --runtime linux-x64 \ -p:DebugSymbols=false -p:DebugType=none Jellyfin.Server %install # Jellyfin files %{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir} -%{__cp} -r Jellyfin.Server/bin/Release/net8.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin +%{__cp} -r Jellyfin.Server/bin/Release/net8.0/linux-x64/publish/* %{buildroot}%{_libdir}/jellyfin %{__install} -D %{SOURCE10} %{buildroot}%{_bindir}/jellyfin sed -i -e 's|/usr/lib64|%{_libdir}|g' %{buildroot}%{_bindir}/jellyfin diff --git a/nuget.config b/nuget.config new file mode 100644 index 000000000..54e660f9c --- /dev/null +++ b/nuget.config @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<configuration> + <packageSources> + <!--To inherit the global NuGet package sources remove the <clear/> line below --> + <clear /> + <add key="nuget" value="https://api.nuget.org/v3/index.json" /> + </packageSources> +</configuration>
\ No newline at end of file diff --git a/src/Jellyfin.Extensions/GuidExtensions.cs b/src/Jellyfin.Extensions/GuidExtensions.cs new file mode 100644 index 000000000..95c591a82 --- /dev/null +++ b/src/Jellyfin.Extensions/GuidExtensions.cs @@ -0,0 +1,26 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Jellyfin.Extensions; + +/// <summary> +/// Guid specific extensions. +/// </summary> +public static class GuidExtensions +{ + /// <summary> + /// Determine whether the guid is default. + /// </summary> + /// <param name="guid">The guid.</param> + /// <returns>Whether the guid is the default value.</returns> + public static bool IsEmpty(this Guid guid) + => guid.Equals(default); + + /// <summary> + /// Determine whether the guid is null or default. + /// </summary> + /// <param name="guid">The guid.</param> + /// <returns>Whether the guid is null or the default valueF.</returns> + public static bool IsNullOrEmpty([NotNullWhen(false)] this Guid? guid) + => guid is null || guid.Value.IsEmpty(); +} diff --git a/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs b/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs index 656e3c3da..0a50b5c3b 100644 --- a/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs +++ b/src/Jellyfin.Extensions/Json/Converters/JsonNullableGuidConverter.cs @@ -18,7 +18,7 @@ namespace Jellyfin.Extensions.Json.Converters { // null got handled higher up the call stack var val = value!.Value; - if (val.Equals(default)) + if (val.IsEmpty()) { writer.WriteNullValue(); } diff --git a/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs b/src/Jellyfin.LiveTv/Channels/ChannelDynamicMediaSourceProvider.cs index 3e149cc82..839549ed6 100644 --- a/Emby.Server.Implementations/Channels/ChannelDynamicMediaSourceProvider.cs +++ b/src/Jellyfin.LiveTv/Channels/ChannelDynamicMediaSourceProvider.cs @@ -8,7 +8,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; -namespace Emby.Server.Implementations.Channels +namespace Jellyfin.LiveTv.Channels { /// <summary> /// A media source provider for channels. diff --git a/Emby.Server.Implementations/Channels/ChannelImageProvider.cs b/src/Jellyfin.LiveTv/Channels/ChannelImageProvider.cs index 25cbfcf14..32e224550 100644 --- a/Emby.Server.Implementations/Channels/ChannelImageProvider.cs +++ b/src/Jellyfin.LiveTv/Channels/ChannelImageProvider.cs @@ -7,7 +7,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; -namespace Emby.Server.Implementations.Channels +namespace Jellyfin.LiveTv.Channels { /// <summary> /// An image provider for channels. diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs index 8279acb05..bc968f8ee 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/src/Jellyfin.LiveTv/Channels/ChannelManager.cs @@ -34,7 +34,7 @@ using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum; using Season = MediaBrowser.Controller.Entities.TV.Season; using Series = MediaBrowser.Controller.Entities.TV.Series; -namespace Emby.Server.Implementations.Channels +namespace Jellyfin.LiveTv.Channels { /// <summary> /// The LiveTV channel manager. @@ -114,15 +114,6 @@ namespace Emby.Server.Implementations.Channels } /// <inheritdoc /> - public bool EnableMediaProbe(BaseItem item) - { - var internalChannel = _libraryManager.GetItemById(item.ChannelId); - var channel = Channels.FirstOrDefault(i => GetInternalChannelId(i.Name).Equals(internalChannel.Id)); - - return channel is ISupportsMediaProbe; - } - - /// <inheritdoc /> public Task DeleteItem(BaseItem item) { var internalChannel = _libraryManager.GetItemById(item.ChannelId); @@ -159,7 +150,7 @@ namespace Emby.Server.Implementations.Channels /// <inheritdoc /> public async Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query) { - var user = query.UserId.Equals(default) + var user = query.UserId.IsEmpty() ? null : _userManager.GetUserById(query.UserId); @@ -272,7 +263,7 @@ namespace Emby.Server.Implementations.Channels /// <inheritdoc /> public async Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query) { - var user = query.UserId.Equals(default) + var user = query.UserId.IsEmpty() ? null : _userManager.GetUserById(query.UserId); @@ -563,18 +554,6 @@ namespace Emby.Server.Implementations.Channels } /// <summary> - /// Checks whether the provided Guid supports external transfer. - /// </summary> - /// <param name="channelId">The Guid.</param> - /// <returns>Whether or not the provided Guid supports external transfer.</returns> - public bool SupportsExternalTransfer(Guid channelId) - { - var channelProvider = GetChannelProvider(channelId); - - return channelProvider.GetChannelFeatures().SupportsContentDownloading; - } - - /// <summary> /// Gets the provided channel's supported features. /// </summary> /// <param name="channel">The channel.</param> @@ -716,7 +695,7 @@ namespace Emby.Server.Implementations.Channels // Find the corresponding channel provider plugin var channelProvider = GetChannelProvider(channel); - var parentItem = query.ParentId.Equals(default) + var parentItem = query.ParentId.IsEmpty() ? channel : _libraryManager.GetItemById(query.ParentId); @@ -729,7 +708,7 @@ namespace Emby.Server.Implementations.Channels cancellationToken) .ConfigureAwait(false); - if (query.ParentId.Equals(default)) + if (query.ParentId.IsEmpty()) { query.Parent = channel; } @@ -812,11 +791,16 @@ namespace Emby.Server.Implementations.Channels { if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) { - await using FileStream jsonStream = AsyncFile.OpenRead(cachePath); - var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); - if (cachedResult is not null) + var jsonStream = AsyncFile.OpenRead(cachePath); + await using (jsonStream.ConfigureAwait(false)) { - return null; + var cachedResult = await JsonSerializer + .DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken) + .ConfigureAwait(false); + if (cachedResult is not null) + { + return null; + } } } } @@ -835,11 +819,16 @@ namespace Emby.Server.Implementations.Channels { if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) { - await using FileStream jsonStream = AsyncFile.OpenRead(cachePath); - var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); - if (cachedResult is not null) + var jsonStream = AsyncFile.OpenRead(cachePath); + await using (jsonStream.ConfigureAwait(false)) { - return null; + var cachedResult = await JsonSerializer + .DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken) + .ConfigureAwait(false); + if (cachedResult is not null) + { + return null; + } } } } @@ -867,7 +856,7 @@ namespace Emby.Server.Implementations.Channels throw new InvalidOperationException("Channel returned a null result from GetChannelItems"); } - await CacheResponse(result, cachePath); + await CacheResponse(result, cachePath).ConfigureAwait(false); return result; } @@ -883,8 +872,11 @@ namespace Emby.Server.Implementations.Channels { Directory.CreateDirectory(Path.GetDirectoryName(path)); - await using FileStream createStream = File.Create(path); - await JsonSerializer.SerializeAsync(createStream, result, _jsonOptions).ConfigureAwait(false); + var createStream = File.Create(path); + await using (createStream.ConfigureAwait(false)) + { + await JsonSerializer.SerializeAsync(createStream, result, _jsonOptions).ConfigureAwait(false); + } } catch (Exception ex) { @@ -1202,19 +1194,6 @@ namespace Emby.Server.Implementations.Channels return result; } - internal IChannel GetChannelProvider(Guid internalChannelId) - { - var result = GetAllChannels() - .FirstOrDefault(i => internalChannelId.Equals(GetInternalChannelId(i.Name))); - - if (result is null) - { - throw new ResourceNotFoundException("No channel provider found for channel id " + internalChannelId); - } - - return result; - } - /// <inheritdoc /> public void Dispose() { diff --git a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs b/src/Jellyfin.LiveTv/Channels/ChannelPostScanTask.cs index b358ba4d5..b4f6cf731 100644 --- a/Emby.Server.Implementations/Channels/ChannelPostScanTask.cs +++ b/src/Jellyfin.LiveTv/Channels/ChannelPostScanTask.cs @@ -8,7 +8,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Channels +namespace Jellyfin.LiveTv.Channels { /// <summary> /// A task to remove all non-installed channels from the database. diff --git a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs b/src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs index cfd08e653..556e052d4 100644 --- a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs +++ b/src/Jellyfin.LiveTv/Channels/RefreshChannelsScheduledTask.cs @@ -9,7 +9,7 @@ using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Channels +namespace Jellyfin.LiveTv.Channels { /// <summary> /// The "Refresh Channels" scheduled task. diff --git a/src/Jellyfin.LiveTv/Configuration/LiveTvConfigurationExtensions.cs b/src/Jellyfin.LiveTv/Configuration/LiveTvConfigurationExtensions.cs new file mode 100644 index 000000000..67d0e5295 --- /dev/null +++ b/src/Jellyfin.LiveTv/Configuration/LiveTvConfigurationExtensions.cs @@ -0,0 +1,18 @@ +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.LiveTv; + +namespace Jellyfin.LiveTv.Configuration; + +/// <summary> +/// <see cref="IConfigurationManager"/> extensions for Live TV. +/// </summary> +public static class LiveTvConfigurationExtensions +{ + /// <summary> + /// Gets the <see cref="LiveTvOptions"/>. + /// </summary> + /// <param name="configurationManager">The <see cref="IConfigurationManager"/>.</param> + /// <returns>The <see cref="LiveTvOptions"/>.</returns> + public static LiveTvOptions GetLiveTvConfiguration(this IConfigurationManager configurationManager) + => configurationManager.GetConfiguration<LiveTvOptions>("livetv"); +} diff --git a/src/Jellyfin.LiveTv/Configuration/LiveTvConfigurationFactory.cs b/src/Jellyfin.LiveTv/Configuration/LiveTvConfigurationFactory.cs new file mode 100644 index 000000000..258afbb05 --- /dev/null +++ b/src/Jellyfin.LiveTv/Configuration/LiveTvConfigurationFactory.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Model.LiveTv; + +namespace Jellyfin.LiveTv.Configuration; + +/// <summary> +/// <see cref="IConfigurationFactory" /> implementation for <see cref="LiveTvOptions" />. +/// </summary> +public class LiveTvConfigurationFactory : IConfigurationFactory +{ + /// <inheritdoc /> + public IEnumerable<ConfigurationStore> GetConfigurations() + { + return new[] + { + new ConfigurationStore + { + ConfigurationType = typeof(LiveTvOptions), + Key = "livetv" + } + }; + } +} diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/src/Jellyfin.LiveTv/EmbyTV/DirectRecorder.cs index 49833de73..2a25218b6 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ b/src/Jellyfin.LiveTv/EmbyTV/DirectRecorder.cs @@ -5,16 +5,16 @@ using System.IO; using System.Net.Http; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Api.Helpers; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Streaming; using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.LiveTv.EmbyTV +namespace Jellyfin.LiveTv.EmbyTV { - public class DirectRecorder : IRecorder + public sealed class DirectRecorder : IRecorder { private readonly ILogger _logger; private readonly IHttpClientFactory _httpClientFactory; @@ -46,7 +46,15 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile))); - await using (var output = new FileStream(targetFile, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous)) + var output = new FileStream( + targetFile, + FileMode.CreateNew, + FileAccess.Write, + FileShare.Read, + IODefaults.FileStreamBufferSize, + FileOptions.Asynchronous); + + await using (output.ConfigureAwait(false)) { onStarted(); @@ -80,24 +88,31 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile))); - await using var output = new FileStream(targetFile, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.CopyToBufferSize, FileOptions.Asynchronous); + var output = new FileStream(targetFile, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.CopyToBufferSize, FileOptions.Asynchronous); + await using (output.ConfigureAwait(false)) + { + onStarted(); - onStarted(); + _logger.LogInformation("Copying recording stream to file {0}", targetFile); - _logger.LogInformation("Copying recording stream to file {0}", targetFile); + // The media source if infinite so we need to handle stopping ourselves + using var durationToken = new CancellationTokenSource(duration); + using var linkedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token); + cancellationToken = linkedCancellationToken.Token; - // The media source if infinite so we need to handle stopping ourselves - using var durationToken = new CancellationTokenSource(duration); - using var linkedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token); - cancellationToken = linkedCancellationToken.Token; + await _streamHelper.CopyUntilCancelled( + await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), + output, + IODefaults.CopyToBufferSize, + cancellationToken).ConfigureAwait(false); - await _streamHelper.CopyUntilCancelled( - await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), - output, - IODefaults.CopyToBufferSize, - cancellationToken).ConfigureAwait(false); + _logger.LogInformation("Recording completed to file {0}", targetFile); + } + } - _logger.LogInformation("Recording completed to file {0}", targetFile); + /// <inheritdoc /> + public void Dispose() + { } } } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/src/Jellyfin.LiveTv/EmbyTV/EmbyTV.cs index 74b62ca3f..e7e927b2d 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/src/Jellyfin.LiveTv/EmbyTV/EmbyTV.cs @@ -14,14 +14,13 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; -using Emby.Server.Implementations.Library; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; using Jellyfin.Extensions; +using Jellyfin.LiveTv.Configuration; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; -using MediaBrowser.Controller; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -37,18 +36,14 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Providers; -using MediaBrowser.Model.Querying; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.LiveTv.EmbyTV +namespace Jellyfin.LiveTv.EmbyTV { - public class EmbyTV : ILiveTvService, ISupportsDirectStreamProvider, ISupportsNewTimerIds, IDisposable + public sealed class EmbyTV : ILiveTvService, ISupportsDirectStreamProvider, ISupportsNewTimerIds, IDisposable { public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss"; - private const int TunerDiscoveryDurationMs = 3000; - - private readonly IServerApplicationHost _appHost; private readonly ILogger<EmbyTV> _logger; private readonly IHttpClientFactory _httpClientFactory; private readonly IServerConfigurationManager _config; @@ -57,6 +52,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private readonly TimerManager _timerProvider; private readonly LiveTvManager _liveTvManager; + private readonly ITunerHostManager _tunerHostManager; private readonly IFileSystem _fileSystem; private readonly ILibraryMonitor _libraryMonitor; @@ -74,16 +70,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim(1, 1); - private bool _disposed = false; + private bool _disposed; public EmbyTV( - IServerApplicationHost appHost, IStreamHelper streamHelper, IMediaSourceManager mediaSourceManager, ILogger<EmbyTV> logger, IHttpClientFactory httpClientFactory, IServerConfigurationManager config, ILiveTvManager liveTvManager, + ITunerHostManager tunerHostManager, IFileSystem fileSystem, ILibraryManager libraryManager, ILibraryMonitor libraryMonitor, @@ -92,7 +88,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { Current = this; - _appHost = appHost; _logger = logger; _httpClientFactory = httpClientFactory; _config = config; @@ -102,6 +97,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _providerManager = providerManager; _mediaEncoder = mediaEncoder; _liveTvManager = (LiveTvManager)liveTvManager; + _tunerHostManager = tunerHostManager; _mediaSourceManager = mediaSourceManager; _streamHelper = streamHelper; @@ -132,7 +128,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { get { - var path = GetConfiguration().RecordingPath; + var path = _config.GetLiveTvConfiguration().RecordingPath; return string.IsNullOrWhiteSpace(path) ? DefaultRecordingPath @@ -195,7 +191,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV pathsAdded.AddRange(pathsToCreate); } - var config = GetConfiguration(); + var config = _config.GetLiveTvConfiguration(); var pathsToRemove = config.MediaLocationsCreated .Except(recordingFolders.SelectMany(i => i.Locations)) @@ -315,7 +311,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var list = new List<ChannelInfo>(); - foreach (var hostInstance in _liveTvManager.TunerHosts) + foreach (var hostInstance in _tunerHostManager.TunerHosts) { try { @@ -515,7 +511,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var list = new List<ChannelInfo>(); - foreach (var hostInstance in _liveTvManager.TunerHosts) + foreach (var hostInstance in _tunerHostManager.TunerHosts) { try { @@ -837,7 +833,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV public Task<SeriesTimerInfo> GetNewTimerDefaultsAsync(CancellationToken cancellationToken, ProgramInfo program = null) { - var config = GetConfiguration(); + var config = _config.GetLiveTvConfiguration(); var defaults = new SeriesTimerInfo() { @@ -938,7 +934,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private List<Tuple<IListingsProvider, ListingsProviderInfo>> GetListingProviders() { - return GetConfiguration().ListingProviders + return _config.GetLiveTvConfiguration().ListingProviders .Select(i => { var provider = _liveTvManager.ListingProviders.FirstOrDefault(l => string.Equals(l.Type, i.Type, StringComparison.OrdinalIgnoreCase)); @@ -971,7 +967,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return result; } - foreach (var hostInstance in _liveTvManager.TunerHosts) + foreach (var hostInstance in _tunerHostManager.TunerHosts) { try { @@ -1003,7 +999,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV throw new ArgumentNullException(nameof(channelId)); } - foreach (var hostInstance in _liveTvManager.TunerHosts) + foreach (var hostInstance in _tunerHostManager.TunerHosts) { try { @@ -1022,45 +1018,11 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV throw new NotImplementedException(); } - public async Task<List<MediaSourceInfo>> GetRecordingStreamMediaSources(ActiveRecordingInfo info, CancellationToken cancellationToken) - { - var stream = new MediaSourceInfo - { - EncoderPath = _appHost.GetApiUrlForLocalAccess() + "/LiveTv/LiveRecordings/" + info.Id + "/stream", - EncoderProtocol = MediaProtocol.Http, - Path = info.Path, - Protocol = MediaProtocol.File, - Id = info.Id, - SupportsDirectPlay = false, - SupportsDirectStream = true, - SupportsTranscoding = true, - IsInfiniteStream = true, - RequiresOpening = false, - RequiresClosing = false, - BufferMs = 0, - IgnoreDts = true, - IgnoreIndex = true - }; - - await new LiveStreamHelper(_mediaEncoder, _logger, _config.CommonApplicationPaths) - .AddMediaInfoWithProbe(stream, false, false, cancellationToken).ConfigureAwait(false); - - return new List<MediaSourceInfo> - { - stream - }; - } - public Task CloseLiveStream(string id, CancellationToken cancellationToken) { return Task.CompletedTask; } - public Task RecordLiveStream(string id, CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - public Task ResetTuner(string id, CancellationToken cancellationToken) { return Task.CompletedTask; @@ -1111,7 +1073,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private string GetRecordingPath(TimerInfo timer, RemoteSearchResult metadata, out string seriesPath) { var recordPath = RecordingPath; - var config = GetConfiguration(); + var config = _config.GetLiveTvConfiguration(); seriesPath = null; if (timer.IsProgramSeries) @@ -1270,7 +1232,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV directStreamProvider = liveStreamResponse.Item2; } - var recorder = GetRecorder(mediaStreamInfo); + using var recorder = GetRecorder(mediaStreamInfo); recordPath = recorder.GetOutputPath(mediaStreamInfo, recordPath); recordPath = EnsureFileUnique(recordPath, timer.Id); @@ -1631,7 +1593,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private void PostProcessRecording(TimerInfo timer, string path) { - var options = GetConfiguration(); + var options = _config.GetLiveTvConfiguration(); if (string.IsNullOrWhiteSpace(options.RecordingPostProcessor)) { return; @@ -1812,7 +1774,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV program.AddGenre("News"); } - var config = GetConfiguration(); + var config = _config.GetLiveTvConfiguration(); if (config.SaveRecordingNFO) { @@ -2030,7 +1992,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV await writer.WriteElementStringAsync(null, "genre", null, genre).ConfigureAwait(false); } - var people = item.Id.Equals(default) ? new List<PersonInfo>() : _libraryManager.GetPeople(item); + var people = item.Id.IsEmpty() ? new List<PersonInfo>() : _libraryManager.GetPeople(item); var directors = people .Where(i => i.IsType(PersonKind.Director)) @@ -2163,11 +2125,6 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return _libraryManager.GetItemList(query).Cast<LiveTvProgram>().FirstOrDefault(); } - private LiveTvOptions GetConfiguration() - { - return _config.GetConfiguration<LiveTvOptions>("livetv"); - } - private bool ShouldCancelTimerForSeriesTimer(SeriesTimerInfo seriesTimer, TimerInfo timer) { if (timer.IsManual) @@ -2360,7 +2317,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { string channelId = seriesTimer.RecordAnyChannel ? null : seriesTimer.ChannelId; - if (string.IsNullOrWhiteSpace(channelId) && !parent.ChannelId.Equals(default)) + if (string.IsNullOrWhiteSpace(channelId) && !parent.ChannelId.IsEmpty()) { if (!tempChannelCache.TryGetValue(parent.ChannelId, out LiveTvChannel channel)) { @@ -2419,7 +2376,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { string channelId = null; - if (!programInfo.ChannelId.Equals(default)) + if (!programInfo.ChannelId.IsEmpty()) { if (!tempChannelCache.TryGetValue(programInfo.ChannelId, out LiveTvChannel channel)) { @@ -2525,21 +2482,12 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV /// <inheritdoc /> public void Dispose() { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { if (_disposed) { return; } - if (disposing) - { - _recordingDeleteSemaphore.Dispose(); - } + _recordingDeleteSemaphore.Dispose(); foreach (var pair in _activeRecordings.ToList()) { @@ -2563,7 +2511,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV }; } - var customPath = GetConfiguration().MovieRecordingPath; + var customPath = _config.GetLiveTvConfiguration().MovieRecordingPath; if (!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase) && Directory.Exists(customPath)) { yield return new VirtualFolderInfo @@ -2574,7 +2522,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV }; } - customPath = GetConfiguration().SeriesRecordingPath; + customPath = _config.GetLiveTvConfiguration().SeriesRecordingPath; if (!string.IsNullOrWhiteSpace(customPath) && !string.Equals(customPath, defaultFolder, StringComparison.OrdinalIgnoreCase) && Directory.Exists(customPath)) { yield return new VirtualFolderInfo @@ -2585,81 +2533,5 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV }; } } - - public async Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken) - { - var list = new List<TunerHostInfo>(); - - var configuredDeviceIds = GetConfiguration().TunerHosts - .Where(i => !string.IsNullOrWhiteSpace(i.DeviceId)) - .Select(i => i.DeviceId) - .ToList(); - - foreach (var host in _liveTvManager.TunerHosts) - { - var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, cancellationToken).ConfigureAwait(false); - - if (newDevicesOnly) - { - discoveredDevices = discoveredDevices.Where(d => !configuredDeviceIds.Contains(d.DeviceId, StringComparison.OrdinalIgnoreCase)) - .ToList(); - } - - list.AddRange(discoveredDevices); - } - - return list; - } - - public async Task ScanForTunerDeviceChanges(CancellationToken cancellationToken) - { - foreach (var host in _liveTvManager.TunerHosts) - { - await ScanForTunerDeviceChanges(host, cancellationToken).ConfigureAwait(false); - } - } - - private async Task ScanForTunerDeviceChanges(ITunerHost host, CancellationToken cancellationToken) - { - var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, cancellationToken).ConfigureAwait(false); - - var configuredDevices = GetConfiguration().TunerHosts - .Where(i => string.Equals(i.Type, host.Type, StringComparison.OrdinalIgnoreCase)) - .ToList(); - - foreach (var device in discoveredDevices) - { - var configuredDevice = configuredDevices.FirstOrDefault(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase)); - - if (configuredDevice is not null && !string.Equals(device.Url, configuredDevice.Url, StringComparison.OrdinalIgnoreCase)) - { - _logger.LogInformation("Tuner url has changed from {PreviousUrl} to {NewUrl}", configuredDevice.Url, device.Url); - - configuredDevice.Url = device.Url; - await _liveTvManager.SaveTunerHost(configuredDevice).ConfigureAwait(false); - } - } - } - - private async Task<List<TunerHostInfo>> DiscoverDevices(ITunerHost host, int discoveryDurationMs, CancellationToken cancellationToken) - { - try - { - var discoveredDevices = await host.DiscoverDevices(discoveryDurationMs, cancellationToken).ConfigureAwait(false); - - foreach (var device in discoveredDevices) - { - _logger.LogInformation("Discovered tuner device {0} at {1}", host.Name, device.Url); - } - - return discoveredDevices; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error discovering tuner devices"); - - return new List<TunerHostInfo>(); - } - } } } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/src/Jellyfin.LiveTv/EmbyTV/EncodedRecorder.cs index 5369c9b3d..132a5fc51 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/src/Jellyfin.LiveTv/EmbyTV/EncodedRecorder.cs @@ -23,9 +23,9 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.LiveTv.EmbyTV +namespace Jellyfin.LiveTv.EmbyTV { - public class EncodedRecorder : IRecorder, IDisposable + public class EncodedRecorder : IRecorder { private readonly ILogger _logger; private readonly IMediaEncoder _mediaEncoder; @@ -34,10 +34,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV private readonly IServerConfigurationManager _serverConfigurationManager; private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; private bool _hasExited; - private Stream _logFileStream; + private FileStream _logFileStream; private string _targetPath; private Process _process; - private bool _disposed = false; + private bool _disposed; public EncodedRecorder( ILogger logger, @@ -308,7 +308,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - private async Task StartStreamingLog(Stream source, Stream target) + private async Task StartStreamingLog(Stream source, FileStream target) { try { diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs b/src/Jellyfin.LiveTv/EmbyTV/EntryPoint.cs index a2ec2df37..e750c05ac 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EntryPoint.cs +++ b/src/Jellyfin.LiveTv/EmbyTV/EntryPoint.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Plugins; -namespace Emby.Server.Implementations.LiveTv.EmbyTV +namespace Jellyfin.LiveTv.EmbyTV { public sealed class EntryPoint : IServerEntryPoint { diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs b/src/Jellyfin.LiveTv/EmbyTV/EpgChannelData.cs index 20a8213a7..43d308c43 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EpgChannelData.cs +++ b/src/Jellyfin.LiveTv/EmbyTV/EpgChannelData.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; using MediaBrowser.Controller.LiveTv; -namespace Emby.Server.Implementations.LiveTv.EmbyTV +namespace Jellyfin.LiveTv.EmbyTV { internal class EpgChannelData { diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs b/src/Jellyfin.LiveTv/EmbyTV/IRecorder.cs index 7705132da..7ed42e263 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/IRecorder.cs +++ b/src/Jellyfin.LiveTv/EmbyTV/IRecorder.cs @@ -6,9 +6,9 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; -namespace Emby.Server.Implementations.LiveTv.EmbyTV +namespace Jellyfin.LiveTv.EmbyTV { - public interface IRecorder + public interface IRecorder : IDisposable { /// <summary> /// Records the specified media source. diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs b/src/Jellyfin.LiveTv/EmbyTV/ItemDataProvider.cs index d5a6feb47..547ffeb66 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/ItemDataProvider.cs +++ b/src/Jellyfin.LiveTv/EmbyTV/ItemDataProvider.cs @@ -9,7 +9,7 @@ using System.Text.Json; using Jellyfin.Extensions.Json; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.LiveTv.EmbyTV +namespace Jellyfin.LiveTv.EmbyTV { public class ItemDataProvider<T> where T : class diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/NfoConfigurationExtensions.cs b/src/Jellyfin.LiveTv/EmbyTV/NfoConfigurationExtensions.cs index 83f5e8413..e8570f0e0 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/NfoConfigurationExtensions.cs +++ b/src/Jellyfin.LiveTv/EmbyTV/NfoConfigurationExtensions.cs @@ -1,7 +1,7 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Configuration; -namespace Emby.Server.Implementations.LiveTv.EmbyTV +namespace Jellyfin.LiveTv.EmbyTV { /// <summary> /// Class containing extension methods for working with the nfo configuration. diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs b/src/Jellyfin.LiveTv/EmbyTV/RecordingHelper.cs index 7bbeae866..6bda231b2 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/RecordingHelper.cs +++ b/src/Jellyfin.LiveTv/EmbyTV/RecordingHelper.cs @@ -5,7 +5,7 @@ using System.Globalization; using System.Text; using MediaBrowser.Controller.LiveTv; -namespace Emby.Server.Implementations.LiveTv.EmbyTV +namespace Jellyfin.LiveTv.EmbyTV { internal static class RecordingHelper { diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs b/src/Jellyfin.LiveTv/EmbyTV/SeriesTimerManager.cs index bf28f3b67..2ebe60b29 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/SeriesTimerManager.cs +++ b/src/Jellyfin.LiveTv/EmbyTV/SeriesTimerManager.cs @@ -4,7 +4,7 @@ using System; using MediaBrowser.Controller.LiveTv; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.LiveTv.EmbyTV +namespace Jellyfin.LiveTv.EmbyTV { public class SeriesTimerManager : ItemDataProvider<SeriesTimerInfo> { diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs b/src/Jellyfin.LiveTv/EmbyTV/TimerManager.cs index 9f8441fa4..37b1fa14c 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/TimerManager.cs +++ b/src/Jellyfin.LiveTv/EmbyTV/TimerManager.cs @@ -10,7 +10,7 @@ using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.LiveTv; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.LiveTv.EmbyTV +namespace Jellyfin.LiveTv.EmbyTV { public class TimerManager : ItemDataProvider<TimerInfo> { diff --git a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs b/src/Jellyfin.LiveTv/ExclusiveLiveStream.cs index 868071a99..9d442e20c 100644 --- a/Emby.Server.Implementations/Library/ExclusiveLiveStream.cs +++ b/src/Jellyfin.LiveTv/ExclusiveLiveStream.cs @@ -1,5 +1,6 @@ #nullable disable +#pragma warning disable CA1711 #pragma warning disable CS1591 using System; @@ -10,9 +11,9 @@ using System.Threading.Tasks; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Dto; -namespace Emby.Server.Implementations.Library +namespace Jellyfin.LiveTv { - public class ExclusiveLiveStream : ILiveStream + public sealed class ExclusiveLiveStream : ILiveStream { private readonly Func<Task> _closeFn; @@ -51,5 +52,10 @@ namespace Emby.Server.Implementations.Library { return Task.CompletedTask; } + + /// <inheritdoc /> + public void Dispose() + { + } } } diff --git a/src/Jellyfin.LiveTv/Extensions/LiveTvServiceCollectionExtensions.cs b/src/Jellyfin.LiveTv/Extensions/LiveTvServiceCollectionExtensions.cs new file mode 100644 index 000000000..5490547ec --- /dev/null +++ b/src/Jellyfin.LiveTv/Extensions/LiveTvServiceCollectionExtensions.cs @@ -0,0 +1,31 @@ +using Jellyfin.LiveTv.Channels; +using Jellyfin.LiveTv.TunerHosts; +using Jellyfin.LiveTv.TunerHosts.HdHomerun; +using MediaBrowser.Controller.Channels; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.IO; +using Microsoft.Extensions.DependencyInjection; + +namespace Jellyfin.LiveTv.Extensions; + +/// <summary> +/// Live TV extensions for <see cref="IServiceCollection"/>. +/// </summary> +public static class LiveTvServiceCollectionExtensions +{ + /// <summary> + /// Adds Live TV services to the <see cref="IServiceCollection"/>. + /// </summary> + /// <param name="services">The <see cref="IServiceCollection"/> to add services to.</param> + public static void AddLiveTvServices(this IServiceCollection services) + { + services.AddSingleton<LiveTvDtoService>(); + services.AddSingleton<ILiveTvManager, LiveTvManager>(); + services.AddSingleton<IChannelManager, ChannelManager>(); + services.AddSingleton<IStreamHelper, StreamHelper>(); + services.AddSingleton<ITunerHostManager, TunerHostManager>(); + + services.AddSingleton<ITunerHost, HdHomerunHost>(); + services.AddSingleton<ITunerHost, M3UTunerHost>(); + } +} diff --git a/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj b/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj new file mode 100644 index 000000000..5a826a1da --- /dev/null +++ b/src/Jellyfin.LiveTv/Jellyfin.LiveTv.csproj @@ -0,0 +1,23 @@ +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <TargetFramework>net8.0</TargetFramework> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + </PropertyGroup> + + <ItemGroup> + <AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute"> + <_Parameter1>Jellyfin.LiveTv.Tests</_Parameter1> + </AssemblyAttribute> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="Jellyfin.XmlTv" /> + <PackageReference Include="System.Linq.Async" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\MediaBrowser.Model\MediaBrowser.Model.csproj" /> + <ProjectReference Include="..\..\MediaBrowser.Controller\MediaBrowser.Controller.csproj" /> + <ProjectReference Include="..\..\MediaBrowser.Common\MediaBrowser.Common.csproj" /> + </ItemGroup> +</Project> diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs index 6b0520ad0..3b20cd160 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirect.cs @@ -16,9 +16,9 @@ using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos; using Jellyfin.Extensions; using Jellyfin.Extensions.Json; +using Jellyfin.LiveTv.Listings.SchedulesDirectDtos; using MediaBrowser.Common.Net; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.LiveTv; @@ -27,7 +27,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.LiveTv.Listings +namespace Jellyfin.LiveTv.Listings { public class SchedulesDirect : IListingsProvider, IDisposable { @@ -287,7 +287,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings IsMovie = IsMovie(details), Etag = programInfo.Md5, IsLive = string.Equals(programInfo.LiveTapeDelay, "live", StringComparison.OrdinalIgnoreCase), - IsPremiere = programInfo.Premiere || (programInfo.IsPremiereOrFinale ?? string.Empty).IndexOf("premiere", StringComparison.OrdinalIgnoreCase) != -1 + IsPremiere = programInfo.Premiere || (programInfo.IsPremiereOrFinale ?? string.Empty).Contains("premiere", StringComparison.OrdinalIgnoreCase) }; var showId = programId; @@ -414,7 +414,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings return null; } - if (uri.IndexOf("http", StringComparison.OrdinalIgnoreCase) != -1) + if (uri.Contains("http", StringComparison.OrdinalIgnoreCase)) { return uri; } @@ -613,6 +613,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings // Response is automatically disposed in the calling function, // so dispose manually if not returning. +#pragma warning disable IDISP016, IDISP017 response.Dispose(); if (!enableRetry || (int)response.StatusCode >= 500) { @@ -621,6 +622,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings null, response.StatusCode); } +#pragma warning restore IDISP016, IDISP017 _tokens.Clear(); options.Headers.TryAddWithoutValidation("token", await GetToken(providerInfo, cancellationToken).ConfigureAwait(false)); diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs index 95ac996e0..c1a502fd5 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/BroadcasterDto.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Broadcaster dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs index f6251b9ad..0cc39f3bb 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/CaptionDto.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Caption dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/CastDto.cs index 0b7a2c63a..bdcf87fda 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CastDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/CastDto.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Cast dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs index 87c327ed8..4e0d74078 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ChannelDto.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Channel dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs index c19cd2e48..5c624c288 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ContentRatingDto.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Content rating dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs index f00c9accd..6d3c79c18 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/CrewDto.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Crew dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/DayDto.cs index 1a371965c..094f9a319 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DayDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/DayDto.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Day dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs index ca6ae7fb1..0063f4cc3 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/Description1000Dto.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Description 1_000 dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs index 1577219ed..1d9a18cc7 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/Description100Dto.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Description 100 dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs index eaf4a340b..75e91547b 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/DescriptionsProgramDto.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Descriptions program dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs index fbdfb1f71..28abe094e 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/EventDetailsDto.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Event details dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs index 6852d89d7..6eefc1744 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/GracenoteDto.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Gracenote dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs index b9844562f..a62ae61f9 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/HeadendsDto.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Headends dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs index a1ae3ca6d..21b595f24 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ImageDataDto.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Image data dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs index 3dc64e5d8..856b7a89b 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LineupDto.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// The lineup dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs index f19081781..99f80ce8a 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LineupsDto.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Lineups dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs index fecc55e03..d7836384e 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/LogoDto.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Logo dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MapDto.cs index ffd02d474..ea583a1ce 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MapDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MapDto.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Map dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs index 40faa493c..cafc8e273 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataDto.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Metadata dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs index 43f290156..243ccff5c 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataProgramsDto.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Metadata programs dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs index 04560ab55..1c5c5333c 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MetadataScheduleDto.cs @@ -1,7 +1,7 @@ using System; using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Metadata schedule dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs index 31bef423b..aea740833 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MovieDto.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Movie dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs index e8b15dc07..328cefadc 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/MultipartDto.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Multipart dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs index 84c48f67f..8c3906f86 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDetailsDto.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Program details dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs index 60389b45b..527a6f8a1 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ProgramDto.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Program dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs index c5ddcf7c5..61496155a 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/QualityRatingDto.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Quality rating dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs index e04b619a4..287cd4ed5 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/RatingDto.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Rating dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs index c8f79fd1c..d380ec7ae 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/RecommendationDto.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Recommendation dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs index 0cd05709b..6fc695a39 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/RequestScheduleForChannelDto.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Request schedule for channel dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs index 84e224b71..523900a96 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/ShowImagesDto.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Show image dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/StationDto.cs index d797fd49b..dbde1e117 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/StationDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/StationDto.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Station dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs index 61cd4a9b0..146124f98 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/TitleDto.cs @@ -1,6 +1,6 @@ using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// Title dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs index afb999486..b3bc61837 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs +++ b/src/Jellyfin.LiveTv/Listings/SchedulesDirectDtos/TokenDto.cs @@ -1,7 +1,7 @@ using System; using System.Text.Json.Serialization; -namespace Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos +namespace Jellyfin.LiveTv.Listings.SchedulesDirectDtos { /// <summary> /// The token dto. diff --git a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs index 066afb956..cecc363f0 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/XmlTvListingsProvider.cs +++ b/src/Jellyfin.LiveTv/Listings/XmlTvListingsProvider.cs @@ -23,7 +23,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.LiveTv.Listings +namespace Jellyfin.LiveTv.Listings { public class XmlTvListingsProvider : IListingsProvider { @@ -84,38 +84,53 @@ namespace Emby.Server.Implementations.LiveTv.Listings _logger.LogInformation("Downloading xmltv listings from {Path}", info.Path); using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(info.Path, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(false); + var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using (stream.ConfigureAwait(false)) + { + return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(false); + } } else { - await using var stream = AsyncFile.OpenRead(info.Path); - return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(false); + var stream = AsyncFile.OpenRead(info.Path); + await using (stream.ConfigureAwait(false)) + { + return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(false); + } } } private async Task<string> UnzipIfNeededAndCopy(string originalUrl, Stream stream, string file, CancellationToken cancellationToken) { - await using var fileStream = new FileStream(file, FileMode.CreateNew, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); - - if (Path.GetExtension(originalUrl.AsSpan().LeftPart('?')).Equals(".gz", StringComparison.OrdinalIgnoreCase)) + var fileStream = new FileStream( + file, + FileMode.CreateNew, + FileAccess.Write, + FileShare.None, + IODefaults.FileStreamBufferSize, + FileOptions.Asynchronous); + + await using (fileStream.ConfigureAwait(false)) { - try + if (Path.GetExtension(originalUrl.AsSpan().LeftPart('?')).Equals(".gz", StringComparison.OrdinalIgnoreCase)) { - using var reader = new GZipStream(stream, CompressionMode.Decompress); - await reader.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); + try + { + using var reader = new GZipStream(stream, CompressionMode.Decompress); + await reader.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error extracting from gz file {File}", originalUrl); + } } - catch (Exception ex) + else { - _logger.LogError(ex, "Error extracting from gz file {File}", originalUrl); + await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); } - } - else - { - await stream.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); - } - return file; + return file; + } } public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken) diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/src/Jellyfin.LiveTv/LiveTvDtoService.cs index 9326fbd5c..55b056d3d 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/src/Jellyfin.LiveTv/LiveTvDtoService.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Common; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Drawing; @@ -20,7 +21,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.LiveTv; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.LiveTv +namespace Jellyfin.LiveTv { public class LiveTvDtoService { @@ -456,7 +457,7 @@ namespace Emby.Server.Implementations.LiveTv info.Id = timer.ExternalId; } - if (!dto.ChannelId.Equals(default) && string.IsNullOrEmpty(info.ChannelId)) + if (!dto.ChannelId.IsEmpty() && string.IsNullOrEmpty(info.ChannelId)) { var channel = _libraryManager.GetItemById(dto.ChannelId); @@ -522,7 +523,7 @@ namespace Emby.Server.Implementations.LiveTv info.Id = timer.ExternalId; } - if (!dto.ChannelId.Equals(default) && string.IsNullOrEmpty(info.ChannelId)) + if (!dto.ChannelId.IsEmpty() && string.IsNullOrEmpty(info.ChannelId)) { var channel = _libraryManager.GetItemById(dto.ChannelId); diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/src/Jellyfin.LiveTv/LiveTvManager.cs index dd427c736..bada4249a 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/src/Jellyfin.LiveTv/LiveTvManager.cs @@ -9,11 +9,11 @@ using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Emby.Server.Implementations.Library; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; using Jellyfin.Data.Events; -using MediaBrowser.Common.Configuration; +using Jellyfin.Extensions; +using Jellyfin.LiveTv.Configuration; using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Channels; @@ -34,7 +34,7 @@ using MediaBrowser.Model.Querying; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.LiveTv +namespace Jellyfin.LiveTv { /// <summary> /// Class LiveTvManager. @@ -58,9 +58,9 @@ namespace Emby.Server.Implementations.LiveTv private readonly IFileSystem _fileSystem; private readonly IChannelManager _channelManager; private readonly LiveTvDtoService _tvDtoService; + private readonly ITunerHostManager _tunerHostManager; private ILiveTvService[] _services = Array.Empty<ILiveTvService>(); - private ITunerHost[] _tunerHosts = Array.Empty<ITunerHost>(); private IListingsProvider[] _listingProviders = Array.Empty<IListingsProvider>(); public LiveTvManager( @@ -75,7 +75,8 @@ namespace Emby.Server.Implementations.LiveTv ILocalizationManager localization, IFileSystem fileSystem, IChannelManager channelManager, - LiveTvDtoService liveTvDtoService) + LiveTvDtoService liveTvDtoService, + ITunerHostManager tunerHostManager) { _config = config; _logger = logger; @@ -89,6 +90,7 @@ namespace Emby.Server.Implementations.LiveTv _userDataManager = userDataManager; _channelManager = channelManager; _tvDtoService = liveTvDtoService; + _tunerHostManager = tunerHostManager; } public event EventHandler<GenericEventArgs<TimerEventInfo>> SeriesTimerCancelled; @@ -105,30 +107,17 @@ namespace Emby.Server.Implementations.LiveTv /// <value>The services.</value> public IReadOnlyList<ILiveTvService> Services => _services; - public ITunerHost[] TunerHosts => _tunerHosts; - - public IListingsProvider[] ListingProviders => _listingProviders; - - private LiveTvOptions GetConfiguration() - { - return _config.GetConfiguration<LiveTvOptions>("livetv"); - } + public IReadOnlyList<IListingsProvider> ListingProviders => _listingProviders; public string GetEmbyTvActiveRecordingPath(string id) { return EmbyTV.EmbyTV.Current.GetActiveRecordingPath(id); } - /// <summary> - /// Adds the parts. - /// </summary> - /// <param name="services">The services.</param> - /// <param name="tunerHosts">The tuner hosts.</param> - /// <param name="listingProviders">The listing providers.</param> - public void AddParts(IEnumerable<ILiveTvService> services, IEnumerable<ITunerHost> tunerHosts, IEnumerable<IListingsProvider> listingProviders) + /// <inheritdoc /> + public void AddParts(IEnumerable<ILiveTvService> services, IEnumerable<IListingsProvider> listingProviders) { _services = services.ToArray(); - _tunerHosts = tunerHosts.Where(i => i.IsSupported).ToArray(); _listingProviders = listingProviders.ToArray(); @@ -160,23 +149,9 @@ namespace Emby.Server.Implementations.LiveTv })); } - public List<NameIdPair> GetTunerHostTypes() - { - return _tunerHosts.OrderBy(i => i.Name).Select(i => new NameIdPair - { - Name = i.Name, - Id = i.Type - }).ToList(); - } - - public Task<List<TunerHostInfo>> DiscoverTuners(bool newDevicesOnly, CancellationToken cancellationToken) - { - return EmbyTV.EmbyTV.Current.DiscoverTuners(newDevicesOnly, cancellationToken); - } - public QueryResult<BaseItem> GetInternalChannels(LiveTvChannelQuery query, DtoOptions dtoOptions, CancellationToken cancellationToken) { - var user = query.UserId.Equals(default) + var user = query.UserId.IsEmpty() ? null : _userManager.GetUserById(query.UserId); @@ -231,7 +206,9 @@ namespace Emby.Server.Implementations.LiveTv _logger.LogInformation("Opening channel stream from {0}, external channel Id: {1}", service.Name, channel.ExternalId); MediaSourceInfo info; +#pragma warning disable CA1859 // TODO: Analyzer bug? ILiveStream liveStream; +#pragma warning restore CA1859 if (service is ISupportsDirectStreamProvider supportsManagedStream) { liveStream = await supportsManagedStream.GetChannelStreamWithDirectStreamProvider(channel.ExternalId, mediaSourceId, currentLiveStreams, cancellationToken).ConfigureAwait(false); @@ -1033,7 +1010,7 @@ namespace Emby.Server.Implementations.LiveTv { await EmbyTV.EmbyTV.Current.CreateRecordingFolders().ConfigureAwait(false); - await EmbyTV.EmbyTV.Current.ScanForTunerDeviceChanges(cancellationToken).ConfigureAwait(false); + await _tunerHostManager.ScanForTunerDeviceChanges(cancellationToken).ConfigureAwait(false); var numComplete = 0; double progressPerService = _services.Length == 0 @@ -1095,13 +1072,12 @@ namespace Emby.Server.Implementations.LiveTv // Load these now which will prefetch metadata var dtoOptions = new DtoOptions(); var fields = dtoOptions.Fields.ToList(); - fields.Remove(ItemFields.BasicSyncInfo); dtoOptions.Fields = fields.ToArray(); progress.Report(100); } - private async Task<Tuple<List<Guid>, List<Guid>>> RefreshChannelsInternal(ILiveTvService service, IProgress<double> progress, CancellationToken cancellationToken) + private async Task<Tuple<List<Guid>, List<Guid>>> RefreshChannelsInternal(ILiveTvService service, ActionableProgress<double> progress, CancellationToken cancellationToken) { progress.Report(10); @@ -1270,7 +1246,7 @@ namespace Emby.Server.Implementations.LiveTv { cancellationToken.ThrowIfCancellationRequested(); - if (itemId.Equals(default)) + if (itemId.IsEmpty()) { // Somehow some invalid data got into the db. It probably predates the boundary checking continue; @@ -1302,7 +1278,7 @@ namespace Emby.Server.Implementations.LiveTv private double GetGuideDays() { - var config = GetConfiguration(); + var config = _config.GetLiveTvConfiguration(); if (config.GuideDays.HasValue) { @@ -1529,7 +1505,7 @@ namespace Emby.Server.Implementations.LiveTv public async Task<QueryResult<BaseItemDto>> GetRecordingsAsync(RecordingQuery query, DtoOptions options) { - var user = query.UserId.Equals(default) + var user = query.UserId.IsEmpty() ? null : _userManager.GetUserById(query.UserId); @@ -2125,7 +2101,7 @@ namespace Emby.Server.Implementations.LiveTv private bool IsLiveTvEnabled(User user) { - return user.HasPermission(PermissionKind.EnableLiveTvAccess) && (Services.Count > 1 || GetConfiguration().TunerHosts.Length > 0); + return user.HasPermission(PermissionKind.EnableLiveTvAccess) && (Services.Count > 1 || _config.GetLiveTvConfiguration().TunerHosts.Length > 0); } public IEnumerable<User> GetEnabledUsers() @@ -2168,49 +2144,7 @@ namespace Emby.Server.Implementations.LiveTv public Folder GetInternalLiveTvFolder(CancellationToken cancellationToken) { var name = _localization.GetLocalizedString("HeaderLiveTV"); - return _libraryManager.GetNamedView(name, CollectionType.LiveTv, name); - } - - public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true) - { - info = JsonSerializer.Deserialize<TunerHostInfo>(JsonSerializer.SerializeToUtf8Bytes(info)); - - var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); - - if (provider is null) - { - throw new ResourceNotFoundException(); - } - - if (provider is IConfigurableTunerHost configurable) - { - await configurable.Validate(info).ConfigureAwait(false); - } - - var config = GetConfiguration(); - - var list = config.TunerHosts.ToList(); - var index = list.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase)); - - if (index == -1 || string.IsNullOrWhiteSpace(info.Id)) - { - info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); - list.Add(info); - config.TunerHosts = list.ToArray(); - } - else - { - config.TunerHosts[index] = info; - } - - _config.SaveConfiguration("livetv", config); - - if (dataSourceChanged) - { - _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>(); - } - - return info; + return _libraryManager.GetNamedView(name, CollectionType.livetv, name); } public async Task<ListingsProviderInfo> SaveListingProvider(ListingsProviderInfo info, bool validateLogin, bool validateListings) @@ -2232,7 +2166,7 @@ namespace Emby.Server.Implementations.LiveTv await provider.Validate(info, validateLogin, validateListings).ConfigureAwait(false); - LiveTvOptions config = GetConfiguration(); + var config = _config.GetLiveTvConfiguration(); var list = config.ListingProviders.ToList(); int index = list.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase)); @@ -2257,7 +2191,7 @@ namespace Emby.Server.Implementations.LiveTv public void DeleteListingsProvider(string id) { - var config = GetConfiguration(); + var config = _config.GetLiveTvConfiguration(); config.ListingProviders = config.ListingProviders.Where(i => !string.Equals(id, i.Id, StringComparison.OrdinalIgnoreCase)).ToArray(); @@ -2267,7 +2201,7 @@ namespace Emby.Server.Implementations.LiveTv public async Task<TunerChannelMapping> SetChannelMapping(string providerId, string tunerChannelNumber, string providerChannelNumber) { - var config = GetConfiguration(); + var config = _config.GetLiveTvConfiguration(); var listingsProviderInfo = config.ListingProviders.First(i => string.Equals(providerId, i.Id, StringComparison.OrdinalIgnoreCase)); listingsProviderInfo.ChannelMappings = listingsProviderInfo.ChannelMappings.Where(i => !string.Equals(i.Name, tunerChannelNumber, StringComparison.OrdinalIgnoreCase)).ToArray(); @@ -2327,7 +2261,7 @@ namespace Emby.Server.Implementations.LiveTv public Task<List<NameIdPair>> GetLineups(string providerType, string providerId, string country, string location) { - var config = GetConfiguration(); + var config = _config.GetLiveTvConfiguration(); if (string.IsNullOrWhiteSpace(providerId)) { @@ -2357,13 +2291,13 @@ namespace Emby.Server.Implementations.LiveTv public Task<List<ChannelInfo>> GetChannelsForListingsProvider(string id, CancellationToken cancellationToken) { - var info = GetConfiguration().ListingProviders.First(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)); + var info = _config.GetLiveTvConfiguration().ListingProviders.First(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)); return EmbyTV.EmbyTV.Current.GetChannelsForListingsProvider(info, cancellationToken); } public Task<List<ChannelInfo>> GetChannelsFromListingsProviderData(string id, CancellationToken cancellationToken) { - var info = GetConfiguration().ListingProviders.First(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)); + var info = _config.GetLiveTvConfiguration().ListingProviders.First(i => string.Equals(i.Id, id, StringComparison.OrdinalIgnoreCase)); var provider = _listingProviders.First(i => string.Equals(i.Type, info.Type, StringComparison.OrdinalIgnoreCase)); return provider.GetChannels(info, cancellationToken); } diff --git a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs b/src/Jellyfin.LiveTv/LiveTvMediaSourceProvider.cs index 6a92fc599..ce9361089 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvMediaSourceProvider.cs +++ b/src/Jellyfin.LiveTv/LiveTvMediaSourceProvider.cs @@ -16,7 +16,7 @@ using MediaBrowser.Model.Dto; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.LiveTv +namespace Jellyfin.LiveTv { public class LiveTvMediaSourceProvider : IMediaSourceProvider { @@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.LiveTv { if (activeRecordingInfo is not null) { - sources = await EmbyTV.EmbyTV.Current.GetRecordingStreamMediaSources(activeRecordingInfo, cancellationToken) + sources = await _mediaSourceManager.GetRecordingStreamMediaSources(activeRecordingInfo, cancellationToken) .ConfigureAwait(false); } else diff --git a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs b/src/Jellyfin.LiveTv/RecordingNotifier.cs index e0ca02d98..2923948eb 100644 --- a/Emby.Server.Implementations/EntryPoints/RecordingNotifier.cs +++ b/src/Jellyfin.LiveTv/RecordingNotifier.cs @@ -15,7 +15,7 @@ using MediaBrowser.Controller.Session; using MediaBrowser.Model.Session; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.EntryPoints +namespace Jellyfin.LiveTv { public sealed class RecordingNotifier : IServerEntryPoint { diff --git a/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs b/src/Jellyfin.LiveTv/RefreshGuideScheduledTask.cs index 72bbdd14a..18bd61d99 100644 --- a/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs +++ b/src/Jellyfin.LiveTv/RefreshGuideScheduledTask.cs @@ -2,12 +2,12 @@ using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Jellyfin.LiveTv.Configuration; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.Tasks; -namespace Emby.Server.Implementations.LiveTv +namespace Jellyfin.LiveTv { /// <summary> /// The "Refresh Guide" scheduled task. @@ -38,7 +38,7 @@ namespace Emby.Server.Implementations.LiveTv public string Category => "Live TV"; /// <inheritdoc /> - public bool IsHidden => _liveTvManager.Services.Count == 1 && GetConfiguration().TunerHosts.Length == 0; + public bool IsHidden => _liveTvManager.Services.Count == 1 && _config.GetLiveTvConfiguration().TunerHosts.Length == 0; /// <inheritdoc /> public bool IsEnabled => true; @@ -66,10 +66,5 @@ namespace Emby.Server.Implementations.LiveTv new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks } }; } - - private LiveTvOptions GetConfiguration() - { - return _config.GetConfiguration<LiveTvOptions>("livetv"); - } } } diff --git a/Emby.Server.Implementations/IO/StreamHelper.cs b/src/Jellyfin.LiveTv/StreamHelper.cs index 6eaf22ce4..e9644e95e 100644 --- a/Emby.Server.Implementations/IO/StreamHelper.cs +++ b/src/Jellyfin.LiveTv/StreamHelper.cs @@ -7,7 +7,7 @@ using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.IO; -namespace Emby.Server.Implementations.IO +namespace Jellyfin.LiveTv { public class StreamHelper : IStreamHelper { @@ -81,36 +81,6 @@ namespace Emby.Server.Implementations.IO } } - public async Task CopyToAsync(Stream source, Stream destination, long copyLength, CancellationToken cancellationToken) - { - byte[] buffer = ArrayPool<byte>.Shared.Rent(IODefaults.CopyToBufferSize); - try - { - int bytesRead; - - while ((bytesRead = await source.ReadAsync(buffer, cancellationToken).ConfigureAwait(false)) != 0) - { - var bytesToWrite = Math.Min(bytesRead, copyLength); - - if (bytesToWrite > 0) - { - await destination.WriteAsync(buffer.AsMemory(0, Convert.ToInt32(bytesToWrite)), cancellationToken).ConfigureAwait(false); - } - - copyLength -= bytesToWrite; - - if (copyLength <= 0) - { - break; - } - } - } - finally - { - ArrayPool<byte>.Shared.Return(buffer); - } - } - public async Task CopyUntilCancelled(Stream source, Stream target, int bufferSize, CancellationToken cancellationToken) { byte[] buffer = ArrayPool<byte>.Shared.Rent(bufferSize); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/src/Jellyfin.LiveTv/TunerHosts/BaseTunerHost.cs index ff25ee585..afc2e4f9c 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/BaseTunerHost.cs @@ -10,7 +10,7 @@ using System.Linq; using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using MediaBrowser.Common.Configuration; +using Jellyfin.LiveTv.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; @@ -19,7 +19,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.LiveTv.TunerHosts +namespace Jellyfin.LiveTv.TunerHosts { public abstract class BaseTunerHost { @@ -67,9 +67,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return list; } - protected virtual List<TunerHostInfo> GetTunerHosts() + protected virtual IList<TunerHostInfo> GetTunerHosts() { - return GetConfiguration().TunerHosts + return Config.GetLiveTvConfiguration().TunerHosts .Where(i => string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase)) .ToList(); } @@ -96,8 +96,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts try { Directory.CreateDirectory(Path.GetDirectoryName(channelCacheFile)); - await using var writeStream = AsyncFile.OpenWrite(channelCacheFile); - await JsonSerializer.SerializeAsync(writeStream, channels, cancellationToken: cancellationToken).ConfigureAwait(false); + var writeStream = AsyncFile.OpenWrite(channelCacheFile); + await using (writeStream.ConfigureAwait(false)) + { + await JsonSerializer.SerializeAsync(writeStream, channels, cancellationToken: cancellationToken).ConfigureAwait(false); + } } catch (IOException) { @@ -112,10 +115,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { try { - await using var readStream = AsyncFile.OpenRead(channelCacheFile); - var channels = await JsonSerializer.DeserializeAsync<List<ChannelInfo>>(readStream, cancellationToken: cancellationToken) - .ConfigureAwait(false); - list.AddRange(channels); + var readStream = AsyncFile.OpenRead(channelCacheFile); + await using (readStream.ConfigureAwait(false)) + { + var channels = await JsonSerializer + .DeserializeAsync<List<ChannelInfo>>(readStream, cancellationToken: cancellationToken) + .ConfigureAwait(false); + list.AddRange(channels); + } } catch (IOException) { @@ -159,9 +166,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return new List<MediaSourceInfo>(); } - protected abstract Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken); + protected abstract Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, IList<ILiveStream> currentLiveStreams, CancellationToken cancellationToken); - public async Task<ILiveStream> GetChannelStream(string channelId, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) + public async Task<ILiveStream> GetChannelStream(string channelId, string streamId, IList<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) { ArgumentException.ThrowIfNullOrEmpty(channelId); @@ -221,10 +228,5 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return channelId.StartsWith(ChannelIdPrefix, StringComparison.OrdinalIgnoreCase); } - - protected LiveTvOptions GetConfiguration() - { - return Config.GetConfiguration<LiveTvOptions>("livetv"); - } } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/Channels.cs index 0f0453189..311a71d13 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/Channels.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/Channels.cs @@ -1,6 +1,6 @@ #nullable disable -namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun +namespace Jellyfin.LiveTv.TunerHosts.HdHomerun { internal class Channels { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs index 42068cd34..3ece181f2 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/DiscoverResponse.cs @@ -2,7 +2,7 @@ using System; -namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun +namespace Jellyfin.LiveTv.TunerHosts.HdHomerun { internal class DiscoverResponse { @@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { var model = ModelNumber ?? string.Empty; - if (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1) + if (model.Contains("hdtc", StringComparison.OrdinalIgnoreCase)) { return true; } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs index aae33503f..50a887826 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunChannelCommands.cs @@ -3,7 +3,7 @@ using System; using System.Collections.Generic; -namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun +namespace Jellyfin.LiveTv.TunerHosts.HdHomerun { public class HdHomerunChannelCommands : IHdHomerunChannelCommands { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 8cd0c4ffb..fef84dd00 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; using System.Net; using System.Net.Http; @@ -30,7 +29,7 @@ using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun +namespace Jellyfin.LiveTv.TunerHosts.HdHomerun { public class HdHomerunHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost { @@ -143,7 +142,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun if (!throwAllExceptions && ex.StatusCode.HasValue && ex.StatusCode.Value == HttpStatusCode.NotFound) { const string DefaultValue = "HDHR"; - var response = new DiscoverResponse + var discoverResponse = new DiscoverResponse { ModelNumber = DefaultValue }; @@ -152,163 +151,17 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun // HDHR4 doesn't have this api lock (_modelCache) { - _modelCache[cacheKey] = response; + _modelCache[cacheKey] = discoverResponse; } } - return response; + return discoverResponse; } throw; } } - private async Task<List<LiveTvTunerInfo>> GetTunerInfosHttp(TunerHostInfo info, CancellationToken cancellationToken) - { - var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); - - using var response = await _httpClientFactory.CreateClient(NamedClient.Default) - .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken) - .ConfigureAwait(false); - var tuners = new List<LiveTvTunerInfo>(); - var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await using (stream.ConfigureAwait(false)) - { - using var sr = new StreamReader(stream, System.Text.Encoding.UTF8); - await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false)) - { - string stripedLine = StripXML(line); - if (stripedLine.Contains("Channel", StringComparison.Ordinal)) - { - LiveTvTunerStatus status; - var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); - var name = stripedLine.Substring(0, index - 1); - var currentChannel = stripedLine.Substring(index + 7); - if (string.Equals(currentChannel, "none", StringComparison.Ordinal)) - { - status = LiveTvTunerStatus.LiveTv; - } - else - { - status = LiveTvTunerStatus.Available; - } - - tuners.Add(new LiveTvTunerInfo - { - Name = name, - SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, - ProgramName = currentChannel, - Status = status - }); - } - } - } - - return tuners; - } - - private static string StripXML(string source) - { - if (string.IsNullOrEmpty(source)) - { - return string.Empty; - } - - char[] buffer = new char[source.Length]; - int bufferIndex = 0; - bool inside = false; - - for (int i = 0; i < source.Length; i++) - { - char let = source[i]; - if (let == '<') - { - inside = true; - continue; - } - - if (let == '>') - { - inside = false; - continue; - } - - if (!inside) - { - buffer[bufferIndex++] = let; - } - } - - return new string(buffer, 0, bufferIndex); - } - - private async Task<List<LiveTvTunerInfo>> GetTunerInfosUdp(TunerHostInfo info, CancellationToken cancellationToken) - { - var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); - - var tuners = new List<LiveTvTunerInfo>(model.TunerCount); - - var uri = new Uri(GetApiUrl(info)); - - using (var manager = new HdHomerunManager()) - { - // Legacy HdHomeruns are IPv4 only - var ipInfo = IPAddress.Parse(uri.Host); - - for (int i = 0; i < model.TunerCount; i++) - { - var name = string.Format(CultureInfo.InvariantCulture, "Tuner {0}", i + 1); - var currentChannel = "none"; // TODO: Get current channel and map back to Station Id - var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false); - var status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv; - tuners.Add(new LiveTvTunerInfo - { - Name = name, - SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, - ProgramName = currentChannel, - Status = status - }); - } - } - - return tuners; - } - - public async Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken) - { - var list = new List<LiveTvTunerInfo>(); - - foreach (var host in GetConfiguration().TunerHosts - .Where(i => string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase))) - { - try - { - list.AddRange(await GetTunerInfos(host, cancellationToken).ConfigureAwait(false)); - } - catch (Exception ex) - { - Logger.LogError(ex, "Error getting tuner info"); - } - } - - return list; - } - - public async Task<List<LiveTvTunerInfo>> GetTunerInfos(TunerHostInfo info, CancellationToken cancellationToken) - { - // TODO Need faster way to determine UDP vs HTTP - var channels = await GetChannels(info, true, cancellationToken).ConfigureAwait(false); - - var hdHomerunChannelInfo = channels.FirstOrDefault() as HdHomerunChannelInfo; - - if (hdHomerunChannelInfo is null || hdHomerunChannelInfo.IsLegacyTuner) - { - return await GetTunerInfosUdp(info, cancellationToken).ConfigureAwait(false); - } - - return await GetTunerInfosHttp(info, cancellationToken).ConfigureAwait(false); - } - private static string GetApiUrl(TunerHostInfo info) { var url = info.Url; @@ -527,7 +380,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun return list; } - protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) + protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, IList<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) { var tunerCount = tunerHost.TunerCount; @@ -574,40 +427,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun _streamHelper); } - var enableHttpStream = true; - if (enableHttpStream) - { - mediaSource.Protocol = MediaProtocol.Http; - - var httpUrl = channel.Path; - - // If raw was used, the tuner doesn't support params - if (!string.IsNullOrWhiteSpace(profile) && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase)) - { - httpUrl += "?transcode=" + profile; - } + mediaSource.Protocol = MediaProtocol.Http; - mediaSource.Path = httpUrl; + var httpUrl = channel.Path; - return new SharedHttpStream( - mediaSource, - tunerHost, - streamId, - FileSystem, - _httpClientFactory, - Logger, - Config, - _appHost, - _streamHelper); + // If raw was used, the tuner doesn't support params + if (!string.IsNullOrWhiteSpace(profile) && !string.Equals(profile, "native", StringComparison.OrdinalIgnoreCase)) + { + httpUrl += "?transcode=" + profile; } - return new HdHomerunUdpStream( + mediaSource.Path = httpUrl; + + return new SharedHttpStream( mediaSource, tunerHost, streamId, - new HdHomerunChannelCommands(hdhomerunChannel.Number, profile), - modelInfo.TunerCount, FileSystem, + _httpClientFactory, Logger, Config, _appHost, diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index 68383a554..861338727 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -14,7 +14,7 @@ using System.Threading.Tasks; using MediaBrowser.Common; using MediaBrowser.Controller.LiveTv; -namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun +namespace Jellyfin.LiveTv.TunerHosts.HdHomerun { public sealed class HdHomerunManager : IDisposable { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 6195c7648..6c8cde62c 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -1,5 +1,6 @@ #nullable disable +#pragma warning disable CA1711 #pragma warning disable CS1591 using System; @@ -19,7 +20,7 @@ using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun +namespace Jellyfin.LiveTv.TunerHosts.HdHomerun { public class HdHomerunUdpStream : LiveStream, IDirectStreamProvider { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs index 11bd40ab1..9fcf386f9 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/IHdHomerunChannelCommands.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; -namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun +namespace Jellyfin.LiveTv.TunerHosts.HdHomerun { public interface IHdHomerunChannelCommands { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs index 654474e97..6dc9c885f 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/HdHomerun/LegacyHdHomerunChannelCommands.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Text.RegularExpressions; -namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun +namespace Jellyfin.LiveTv.TunerHosts.HdHomerun { public partial class LegacyHdHomerunChannelCommands : IHdHomerunChannelCommands { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/src/Jellyfin.LiveTv/TunerHosts/LiveStream.cs index 767b94136..70d8afc5d 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/LiveStream.cs @@ -1,5 +1,6 @@ #nullable disable +#pragma warning disable CA1711 #pragma warning disable CS1591 using System; @@ -14,7 +15,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.LiveTv.TunerHosts +namespace Jellyfin.LiveTv.TunerHosts { public class LiveStream : ILiveStream { @@ -112,6 +113,21 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return stream; } + /// <inheritdoc /> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + LiveStreamCancellationTokenSource?.Dispose(); + } + } + protected async Task DeleteTempFiles(string path, int retryCount = 0) { if (retryCount == 0) @@ -134,7 +150,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts } } - private void TrySeek(Stream stream, long offset) + private void TrySeek(FileStream stream, long offset) { if (!stream.CanSeek) { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs index db5e81df5..3666d342e 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/M3UTunerHost.cs @@ -24,7 +24,7 @@ using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; -namespace Emby.Server.Implementations.LiveTv.TunerHosts +namespace Jellyfin.LiveTv.TunerHosts { public class M3UTunerHost : BaseTunerHost, ITunerHost, IConfigurableTunerHost { @@ -80,23 +80,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts .ConfigureAwait(false); } - public Task<List<LiveTvTunerInfo>> GetTunerInfos(CancellationToken cancellationToken) - { - var list = GetTunerHosts() - .Select(i => new LiveTvTunerInfo() - { - Name = Name, - SourceType = Type, - Status = LiveTvTunerStatus.Available, - Id = i.Url.GetMD5().ToString("N", CultureInfo.InvariantCulture), - Url = i.Url - }) - .ToList(); - - return Task.FromResult(list); - } - - protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, List<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) + protected override async Task<ILiveStream> GetChannelStream(TunerHostInfo tunerHost, ChannelInfo channel, string streamId, IList<ILiveStream> currentLiveStreams, CancellationToken cancellationToken) { var tunerCount = tunerHost.TunerCount; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs index 341782d9d..5900d1c5b 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/M3uParser.cs @@ -18,7 +18,7 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.LiveTv.TunerHosts +namespace Jellyfin.LiveTv.TunerHosts { public partial class M3uParser { @@ -66,7 +66,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts .ConfigureAwait(false); response.EnsureSuccessStatusCode(); - return await response.Content.ReadAsStreamAsync(cancellationToken); + return await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); } private async Task<List<ChannelInfo>> GetChannelsAsync(TextReader reader, string channelIdPrefix, string tunerHostId) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/src/Jellyfin.LiveTv/TunerHosts/SharedHttpStream.cs index 51f46f4da..5ef04ad9e 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/src/Jellyfin.LiveTv/TunerHosts/SharedHttpStream.cs @@ -1,3 +1,4 @@ +#pragma warning disable CA1711 #pragma warning disable CS1591 using System; @@ -16,7 +17,7 @@ using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.LiveTv.TunerHosts +namespace Jellyfin.LiveTv.TunerHosts { public class SharedHttpStream : LiveStream, IDirectStreamProvider { @@ -83,14 +84,27 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath); using (response) { - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - await using var fileStream = new FileStream(TempFilePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); - await StreamHelper.CopyToAsync( - stream, - fileStream, - IODefaults.CopyToBufferSize, - () => Resolve(openTaskCompletionSource), - cancellationToken).ConfigureAwait(false); + var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using (stream.ConfigureAwait(false)) + { + var fileStream = new FileStream( + TempFilePath, + FileMode.Create, + FileAccess.Write, + FileShare.Read, + IODefaults.FileStreamBufferSize, + FileOptions.Asynchronous); + + await using (fileStream.ConfigureAwait(false)) + { + await StreamHelper.CopyToAsync( + stream, + fileStream, + IODefaults.CopyToBufferSize, + () => Resolve(openTaskCompletionSource), + cancellationToken).ConfigureAwait(false); + } + } } } catch (OperationCanceledException ex) diff --git a/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs b/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs new file mode 100644 index 000000000..3e4b0e13f --- /dev/null +++ b/src/Jellyfin.LiveTv/TunerHosts/TunerHostManager.cs @@ -0,0 +1,174 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.LiveTv.Configuration; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; +using MediaBrowser.Controller.LiveTv; +using MediaBrowser.Model.Dto; +using MediaBrowser.Model.LiveTv; +using MediaBrowser.Model.Tasks; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.LiveTv.TunerHosts; + +/// <inheritdoc /> +public class TunerHostManager : ITunerHostManager +{ + private const int TunerDiscoveryDurationMs = 3000; + + private readonly ILogger<TunerHostManager> _logger; + private readonly IConfigurationManager _config; + private readonly ITaskManager _taskManager; + private readonly ITunerHost[] _tunerHosts; + + /// <summary> + /// Initializes a new instance of the <see cref="TunerHostManager"/> class. + /// </summary> + /// <param name="logger">The <see cref="ILogger{T}"/>.</param> + /// <param name="config">The <see cref="IConfigurationManager"/>.</param> + /// <param name="taskManager">The <see cref="ITaskManager"/>.</param> + /// <param name="tunerHosts">The <see cref="IEnumerable{T}"/>.</param> + public TunerHostManager( + ILogger<TunerHostManager> logger, + IConfigurationManager config, + ITaskManager taskManager, + IEnumerable<ITunerHost> tunerHosts) + { + _logger = logger; + _config = config; + _taskManager = taskManager; + _tunerHosts = tunerHosts.Where(t => t.IsSupported).ToArray(); + } + + /// <inheritdoc /> + public IReadOnlyList<ITunerHost> TunerHosts => _tunerHosts; + + /// <inheritdoc /> + public IEnumerable<NameIdPair> GetTunerHostTypes() + => _tunerHosts.OrderBy(i => i.Name).Select(i => new NameIdPair + { + Name = i.Name, + Id = i.Type + }); + + /// <inheritdoc /> + public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true) + { + info = JsonSerializer.Deserialize<TunerHostInfo>(JsonSerializer.SerializeToUtf8Bytes(info))!; + + var provider = _tunerHosts.FirstOrDefault(i => string.Equals(info.Type, i.Type, StringComparison.OrdinalIgnoreCase)); + + if (provider is null) + { + throw new ResourceNotFoundException(); + } + + if (provider is IConfigurableTunerHost configurable) + { + await configurable.Validate(info).ConfigureAwait(false); + } + + var config = _config.GetLiveTvConfiguration(); + + var list = config.TunerHosts.ToList(); + var index = list.FindIndex(i => string.Equals(i.Id, info.Id, StringComparison.OrdinalIgnoreCase)); + + if (index == -1 || string.IsNullOrWhiteSpace(info.Id)) + { + info.Id = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture); + list.Add(info); + config.TunerHosts = list.ToArray(); + } + else + { + config.TunerHosts[index] = info; + } + + _config.SaveConfiguration("livetv", config); + + if (dataSourceChanged) + { + _taskManager.CancelIfRunningAndQueue<RefreshGuideScheduledTask>(); + } + + return info; + } + + /// <inheritdoc /> + public async IAsyncEnumerable<TunerHostInfo> DiscoverTuners(bool newDevicesOnly) + { + var configuredDeviceIds = _config.GetLiveTvConfiguration().TunerHosts + .Where(i => !string.IsNullOrWhiteSpace(i.DeviceId)) + .Select(i => i.DeviceId) + .ToList(); + + foreach (var host in _tunerHosts) + { + var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, CancellationToken.None).ConfigureAwait(false); + foreach (var tuner in discoveredDevices) + { + if (!newDevicesOnly || !configuredDeviceIds.Contains(tuner.DeviceId, StringComparer.OrdinalIgnoreCase)) + { + yield return tuner; + } + } + } + } + + /// <inheritdoc /> + public async Task ScanForTunerDeviceChanges(CancellationToken cancellationToken) + { + foreach (var host in _tunerHosts) + { + await ScanForTunerDeviceChanges(host, cancellationToken).ConfigureAwait(false); + } + } + + private async Task ScanForTunerDeviceChanges(ITunerHost host, CancellationToken cancellationToken) + { + var discoveredDevices = await DiscoverDevices(host, TunerDiscoveryDurationMs, cancellationToken).ConfigureAwait(false); + + var configuredDevices = _config.GetLiveTvConfiguration().TunerHosts + .Where(i => string.Equals(i.Type, host.Type, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + foreach (var device in discoveredDevices) + { + var configuredDevice = configuredDevices.FirstOrDefault(i => string.Equals(i.DeviceId, device.DeviceId, StringComparison.OrdinalIgnoreCase)); + + if (configuredDevice is not null && !string.Equals(device.Url, configuredDevice.Url, StringComparison.OrdinalIgnoreCase)) + { + _logger.LogInformation("Tuner url has changed from {PreviousUrl} to {NewUrl}", configuredDevice.Url, device.Url); + + configuredDevice.Url = device.Url; + await SaveTunerHost(configuredDevice).ConfigureAwait(false); + } + } + } + + private async Task<IList<TunerHostInfo>> DiscoverDevices(ITunerHost host, int discoveryDurationMs, CancellationToken cancellationToken) + { + try + { + var discoveredDevices = await host.DiscoverDevices(discoveryDurationMs, cancellationToken).ConfigureAwait(false); + + foreach (var device in discoveredDevices) + { + _logger.LogInformation("Discovered tuner device {0} at {1}", host.Name, device.Url); + } + + return discoveredDevices; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error discovering tuner devices"); + + return Array.Empty<TunerHostInfo>(); + } + } +} diff --git a/src/Jellyfin.Networking/AutoDiscoveryHost.cs b/src/Jellyfin.Networking/AutoDiscoveryHost.cs new file mode 100644 index 000000000..5624c4ed1 --- /dev/null +++ b/src/Jellyfin.Networking/AutoDiscoveryHost.cs @@ -0,0 +1,129 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Model.ApiClient; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Networking; + +/// <summary> +/// <see cref="BackgroundService"/> responsible for responding to auto-discovery messages. +/// </summary> +public sealed class AutoDiscoveryHost : BackgroundService +{ + /// <summary> + /// The port to listen on for auto-discovery messages. + /// </summary> + private const int PortNumber = 7359; + + private readonly ILogger<AutoDiscoveryHost> _logger; + private readonly IServerApplicationHost _appHost; + private readonly IConfigurationManager _configurationManager; + private readonly INetworkManager _networkManager; + + /// <summary> + /// Initializes a new instance of the <see cref="AutoDiscoveryHost" /> class. + /// </summary> + /// <param name="logger">The <see cref="ILogger{AutoDiscoveryHost}"/>.</param> + /// <param name="appHost">The <see cref="IServerApplicationHost"/>.</param> + /// <param name="configurationManager">The <see cref="IConfigurationManager"/>.</param> + /// <param name="networkManager">The <see cref="INetworkManager"/>.</param> + public AutoDiscoveryHost( + ILogger<AutoDiscoveryHost> logger, + IServerApplicationHost appHost, + IConfigurationManager configurationManager, + INetworkManager networkManager) + { + _logger = logger; + _appHost = appHost; + _configurationManager = configurationManager; + _networkManager = networkManager; + } + + /// <inheritdoc /> + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var networkConfig = _configurationManager.GetNetworkConfiguration(); + if (!networkConfig.AutoDiscovery) + { + return; + } + + var udpServers = new List<Task>(); + // Linux needs to bind to the broadcast addresses to receive broadcast traffic + if (OperatingSystem.IsLinux() && networkConfig.EnableIPv4) + { + udpServers.Add(ListenForAutoDiscoveryMessage(IPAddress.Broadcast, stoppingToken)); + } + + udpServers.AddRange(_networkManager.GetInternalBindAddresses() + .Select(intf => ListenForAutoDiscoveryMessage( + OperatingSystem.IsLinux() && intf.AddressFamily == AddressFamily.InterNetwork + ? NetworkUtils.GetBroadcastAddress(intf.Subnet) + : intf.Address, + stoppingToken))); + + await Task.WhenAll(udpServers).ConfigureAwait(false); + } + + private async Task ListenForAutoDiscoveryMessage(IPAddress address, CancellationToken cancellationToken) + { + using var udpClient = new UdpClient(new IPEndPoint(address, PortNumber)); + udpClient.MulticastLoopback = false; + + while (!cancellationToken.IsCancellationRequested) + { + try + { + var result = await udpClient.ReceiveAsync(cancellationToken).ConfigureAwait(false); + var text = Encoding.UTF8.GetString(result.Buffer); + if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase)) + { + await RespondToV2Message(udpClient, result.RemoteEndPoint, cancellationToken).ConfigureAwait(false); + } + } + catch (SocketException ex) + { + _logger.LogError(ex, "Failed to receive data from socket"); + } + catch (OperationCanceledException) + { + _logger.LogDebug("Broadcast socket operation cancelled"); + } + } + } + + private async Task RespondToV2Message(UdpClient udpClient, IPEndPoint endpoint, CancellationToken cancellationToken) + { + var localUrl = _appHost.GetSmartApiUrl(endpoint.Address); + if (string.IsNullOrEmpty(localUrl)) + { + _logger.LogWarning("Unable to respond to server discovery request because the local ip address could not be determined."); + return; + } + + var response = new ServerDiscoveryInfo(localUrl, _appHost.SystemId, _appHost.FriendlyName); + + try + { + _logger.LogDebug("Sending AutoDiscovery response"); + await udpClient + .SendAsync(JsonSerializer.SerializeToUtf8Bytes(response).AsMemory(), endpoint, cancellationToken) + .ConfigureAwait(false); + } + catch (SocketException ex) + { + _logger.LogError(ex, "Error sending response message"); + } + } +} diff --git a/src/Jellyfin.Networking/Udp/UdpServer.cs b/src/Jellyfin.Networking/Udp/UdpServer.cs deleted file mode 100644 index b130a5a5f..000000000 --- a/src/Jellyfin.Networking/Udp/UdpServer.cs +++ /dev/null @@ -1,136 +0,0 @@ -using System; -using System.Net; -using System.Net.Sockets; -using System.Text; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using MediaBrowser.Controller; -using MediaBrowser.Model.ApiClient; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; - -namespace Jellyfin.Networking.Udp; - -/// <summary> -/// Provides a Udp Server. -/// </summary> -public sealed class UdpServer : IDisposable -{ - /// <summary> - /// The _logger. - /// </summary> - private readonly ILogger _logger; - private readonly IServerApplicationHost _appHost; - private readonly IConfiguration _config; - - private readonly byte[] _receiveBuffer = new byte[8192]; - - private readonly Socket _udpSocket; - private readonly IPEndPoint _endpoint; - private bool _disposed; - - /// <summary> - /// Initializes a new instance of the <see cref="UdpServer" /> class. - /// </summary> - /// <param name="logger">The logger.</param> - /// <param name="appHost">The application host.</param> - /// <param name="configuration">The configuration manager.</param> - /// <param name="bindAddress"> The bind address.</param> - /// <param name="port">The port.</param> - public UdpServer( - ILogger logger, - IServerApplicationHost appHost, - IConfiguration configuration, - IPAddress bindAddress, - int port) - { - _logger = logger; - _appHost = appHost; - _config = configuration; - - _endpoint = new IPEndPoint(bindAddress, port); - - _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp) - { - MulticastLoopback = false, - }; - _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - } - - private async Task RespondToV2Message(EndPoint endpoint, CancellationToken cancellationToken) - { - string? localUrl = _config[AddressOverrideKey]; - if (string.IsNullOrEmpty(localUrl)) - { - localUrl = _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address); - } - - if (string.IsNullOrEmpty(localUrl)) - { - _logger.LogWarning("Unable to respond to server discovery request because the local ip address could not be determined."); - return; - } - - var response = new ServerDiscoveryInfo(localUrl, _appHost.SystemId, _appHost.FriendlyName); - - try - { - _logger.LogDebug("Sending AutoDiscovery response"); - await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false); - } - catch (SocketException ex) - { - _logger.LogError(ex, "Error sending response message"); - } - } - - /// <summary> - /// Starts the specified port. - /// </summary> - /// <param name="cancellationToken">The cancellation token to cancel operation.</param> - public void Start(CancellationToken cancellationToken) - { - _udpSocket.Bind(_endpoint); - - _ = Task.Run(async () => await BeginReceiveAsync(cancellationToken).ConfigureAwait(false), cancellationToken).ConfigureAwait(false); - } - - private async Task BeginReceiveAsync(CancellationToken cancellationToken) - { - while (!cancellationToken.IsCancellationRequested) - { - try - { - var endpoint = (EndPoint)new IPEndPoint(IPAddress.Any, 0); - var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, endpoint, cancellationToken).ConfigureAwait(false); - var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes); - if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase)) - { - await RespondToV2Message(result.RemoteEndPoint, cancellationToken).ConfigureAwait(false); - } - } - catch (SocketException ex) - { - _logger.LogError(ex, "Failed to receive data from socket"); - } - catch (OperationCanceledException) - { - _logger.LogDebug("Broadcast socket operation cancelled"); - } - } - } - - /// <inheritdoc /> - public void Dispose() - { - if (_disposed) - { - return; - } - - _udpSocket.Dispose(); - _disposed = true; - } -} diff --git a/src/Jellyfin.Networking/UdpServerEntryPoint.cs b/src/Jellyfin.Networking/UdpServerEntryPoint.cs deleted file mode 100644 index 61180c3c0..000000000 --- a/src/Jellyfin.Networking/UdpServerEntryPoint.cs +++ /dev/null @@ -1,143 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; -using Jellyfin.Networking.Udp; -using MediaBrowser.Common.Net; -using MediaBrowser.Controller; -using MediaBrowser.Controller.Plugins; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using IConfigurationManager = MediaBrowser.Common.Configuration.IConfigurationManager; - -namespace Jellyfin.Networking; - -/// <summary> -/// Class responsible for registering all UDP broadcast endpoints and their handlers. -/// </summary> -public sealed class UdpServerEntryPoint : IServerEntryPoint -{ - /// <summary> - /// The port of the UDP server. - /// </summary> - public const int PortNumber = 7359; - - /// <summary> - /// The logger. - /// </summary> - private readonly ILogger<UdpServerEntryPoint> _logger; - private readonly IServerApplicationHost _appHost; - private readonly IConfiguration _config; - private readonly IConfigurationManager _configurationManager; - private readonly INetworkManager _networkManager; - - /// <summary> - /// The UDP server. - /// </summary> - private readonly List<UdpServer> _udpServers; - private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - private bool _disposed; - - /// <summary> - /// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class. - /// </summary> - /// <param name="logger">Instance of the <see cref="ILogger{UdpServerEntryPoint}"/> interface.</param> - /// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param> - /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param> - /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param> - /// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param> - public UdpServerEntryPoint( - ILogger<UdpServerEntryPoint> logger, - IServerApplicationHost appHost, - IConfiguration configuration, - IConfigurationManager configurationManager, - INetworkManager networkManager) - { - _logger = logger; - _appHost = appHost; - _config = configuration; - _configurationManager = configurationManager; - _networkManager = networkManager; - _udpServers = new List<UdpServer>(); - } - - /// <inheritdoc /> - public Task RunAsync() - { - ObjectDisposedException.ThrowIf(_disposed, this); - - if (!_configurationManager.GetNetworkConfiguration().AutoDiscovery) - { - return Task.CompletedTask; - } - - try - { - // Linux needs to bind to the broadcast addresses to get broadcast traffic - // Windows receives broadcast fine when binding to just the interface, it is unable to bind to broadcast addresses - if (OperatingSystem.IsLinux()) - { - // Add global broadcast listener - var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber); - server.Start(_cancellationTokenSource.Token); - _udpServers.Add(server); - - // Add bind address specific broadcast listeners - // IPv6 is currently unsupported - var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork); - foreach (var intf in validInterfaces) - { - var broadcastAddress = NetworkUtils.GetBroadcastAddress(intf.Subnet); - _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", broadcastAddress, PortNumber); - - server = new UdpServer(_logger, _appHost, _config, broadcastAddress, PortNumber); - server.Start(_cancellationTokenSource.Token); - _udpServers.Add(server); - } - } - else - { - // Add bind address specific broadcast listeners - // IPv6 is currently unsupported - var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork); - foreach (var intf in validInterfaces) - { - var intfAddress = intf.Address; - _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", intfAddress, PortNumber); - - var server = new UdpServer(_logger, _appHost, _config, intfAddress, PortNumber); - server.Start(_cancellationTokenSource.Token); - _udpServers.Add(server); - } - } - } - catch (SocketException ex) - { - _logger.LogWarning(ex, "Unable to start AutoDiscovery listener on UDP port {PortNumber}", PortNumber); - } - - return Task.CompletedTask; - } - - /// <inheritdoc /> - public void Dispose() - { - if (_disposed) - { - return; - } - - _cancellationTokenSource.Cancel(); - _cancellationTokenSource.Dispose(); - foreach (var server in _udpServers) - { - server.Dispose(); - } - - _udpServers.Clear(); - _disposed = true; - } -} diff --git a/tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs b/tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs index 0254a1ec6..5034ad3c7 100644 --- a/tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs +++ b/tests/Jellyfin.Api.Tests/Controllers/ImageControllerTests.cs @@ -27,7 +27,7 @@ public static class ImageControllerTests [InlineData(null)] [InlineData("")] [InlineData("text/html")] - public static void TryGetImageExtensionFromContentType_InValid_False(string contentType) + public static void TryGetImageExtensionFromContentType_InValid_False(string? contentType) { Assert.False(ImageController.TryGetImageExtensionFromContentType(contentType, out var ex)); Assert.Null(ex); diff --git a/tests/Jellyfin.Extensions.Tests/CopyToExtensionsTests.cs b/tests/Jellyfin.Extensions.Tests/CopyToExtensionsTests.cs index d46beedd9..95f9a5fcf 100644 --- a/tests/Jellyfin.Extensions.Tests/CopyToExtensionsTests.cs +++ b/tests/Jellyfin.Extensions.Tests/CopyToExtensionsTests.cs @@ -8,20 +8,18 @@ namespace Jellyfin.Extensions.Tests { public static TheoryData<IReadOnlyList<int>, IList<int>, int, IList<int>> CopyTo_Valid_Correct_TestData() { - var data = new TheoryData<IReadOnlyList<int>, IList<int>, int, IList<int>>(); - - data.Add( - new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 0, new[] { 0, 1, 2, 3, 4, 5 }); - - data.Add( - new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 2, new[] { 5, 4, 0, 1, 2, 0 } ); + var data = new TheoryData<IReadOnlyList<int>, IList<int>, int, IList<int>> + { + { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 0, new[] { 0, 1, 2, 3, 4, 5 } }, + { new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 2, new[] { 5, 4, 0, 1, 2, 0 } } + }; return data; } [Theory] [MemberData(nameof(CopyTo_Valid_Correct_TestData))] - public static void CopyTo_Valid_Correct<T>(IReadOnlyList<T> source, IList<T> destination, int index, IList<T> expected) + public static void CopyTo_Valid_Correct(IReadOnlyList<int> source, IList<int> destination, int index, IList<int> expected) { source.CopyTo(destination, index); Assert.Equal(expected, destination); @@ -29,29 +27,21 @@ namespace Jellyfin.Extensions.Tests public static TheoryData<IReadOnlyList<int>, IList<int>, int> CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData() { - var data = new TheoryData<IReadOnlyList<int>, IList<int>, int>(); - - data.Add( - new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, -1 ); - - data.Add( - new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 6 ); - - data.Add( - new[] { 0, 1, 2 }, Array.Empty<int>(), 0 ); - - data.Add( - new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0 }, 0 ); - - data.Add( - new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 1 ); + var data = new TheoryData<IReadOnlyList<int>, IList<int>, int> + { + { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, -1 }, + { new[] { 0, 1, 2 }, new[] { 5, 4, 3, 2, 1, 0 }, 6 }, + { new[] { 0, 1, 2 }, Array.Empty<int>(), 0 }, + { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0 }, 0 }, + { new[] { 0, 1, 2, 3, 4, 5 }, new[] { 0, 0, 0, 0, 0, 0 }, 1 } + }; return data; } [Theory] [MemberData(nameof(CopyTo_Invalid_ThrowsArgumentOutOfRangeException_TestData))] - public static void CopyTo_Invalid_ThrowsArgumentOutOfRangeException<T>(IReadOnlyList<T> source, IList<T> destination, int index) + public static void CopyTo_Invalid_ThrowsArgumentOutOfRangeException(IReadOnlyList<int> source, IList<int> destination, int index) { Assert.Throws<ArgumentOutOfRangeException>(() => source.CopyTo(destination, index)); } diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs b/tests/Jellyfin.LiveTv.Tests/HdHomerunHostTests.cs index 13ac3ddb0..cb6eb9184 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunHostTests.cs +++ b/tests/Jellyfin.LiveTv.Tests/HdHomerunHostTests.cs @@ -6,13 +6,13 @@ using System.Threading; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; -using Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun; +using Jellyfin.LiveTv.TunerHosts.HdHomerun; using MediaBrowser.Model.LiveTv; using Moq; using Moq.Protected; using Xunit; -namespace Jellyfin.Server.Implementations.Tests.LiveTv +namespace Jellyfin.LiveTv.Tests { public class HdHomerunHostTests { diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunManagerTests.cs b/tests/Jellyfin.LiveTv.Tests/HdHomerunManagerTests.cs index fd499d9cf..4ab0bd723 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/HdHomerunManagerTests.cs +++ b/tests/Jellyfin.LiveTv.Tests/HdHomerunManagerTests.cs @@ -1,9 +1,9 @@ using System; using System.Text; -using Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun; +using Jellyfin.LiveTv.TunerHosts.HdHomerun; using Xunit; -namespace Jellyfin.Server.Implementations.Tests.LiveTv +namespace Jellyfin.LiveTv.Tests { public class HdHomerunManagerTests { diff --git a/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj b/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj new file mode 100644 index 000000000..f645f38c4 --- /dev/null +++ b/tests/Jellyfin.LiveTv.Tests/Jellyfin.LiveTv.Tests.csproj @@ -0,0 +1,29 @@ +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <TargetFramework>net8.0</TargetFramework> + </PropertyGroup> + + <ItemGroup> + <None Include="Test Data\**\*.*"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + </None> + </ItemGroup> + + <ItemGroup> + <PackageReference Include="AutoFixture" /> + <PackageReference Include="AutoFixture.AutoMoq" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" /> + <PackageReference Include="Moq" /> + <PackageReference Include="xunit" /> + <PackageReference Include="xunit.runner.visualstudio"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Xunit.SkippableFact" /> + <PackageReference Include="coverlet.collector" /> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\..\src\Jellyfin.LiveTv\Jellyfin.LiveTv.csproj" /> + </ItemGroup> +</Project> diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs b/tests/Jellyfin.LiveTv.Tests/Listings/XmlTvListingsProviderTests.cs index 92b4178fd..0fb7894e5 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/Listings/XmlTvListingsProviderTests.cs +++ b/tests/Jellyfin.LiveTv.Tests/Listings/XmlTvListingsProviderTests.cs @@ -6,13 +6,13 @@ using System.Threading; using System.Threading.Tasks; using AutoFixture; using AutoFixture.AutoMoq; -using Emby.Server.Implementations.LiveTv.Listings; +using Jellyfin.LiveTv.Listings; using MediaBrowser.Model.LiveTv; using Moq; using Moq.Protected; using Xunit; -namespace Jellyfin.Server.Implementations.Tests.LiveTv.Listings; +namespace Jellyfin.LiveTv.Tests.Listings; public class XmlTvListingsProviderTests { diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs b/tests/Jellyfin.LiveTv.Tests/RecordingHelperTests.cs index f107b1ef9..b4960dc0b 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/RecordingHelperTests.cs +++ b/tests/Jellyfin.LiveTv.Tests/RecordingHelperTests.cs @@ -1,9 +1,9 @@ using System; -using Emby.Server.Implementations.LiveTv.EmbyTV; +using Jellyfin.LiveTv.EmbyTV; using MediaBrowser.Controller.LiveTv; using Xunit; -namespace Jellyfin.Server.Implementations.Tests.LiveTv +namespace Jellyfin.LiveTv.Tests { public static class RecordingHelperTests { diff --git a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs b/tests/Jellyfin.LiveTv.Tests/SchedulesDirect/SchedulesDirectDeserializeTests.cs index d4f28f327..6975d56d9 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/LiveTv/SchedulesDirect/SchedulesDirectDeserializeTests.cs +++ b/tests/Jellyfin.LiveTv.Tests/SchedulesDirect/SchedulesDirectDeserializeTests.cs @@ -2,11 +2,11 @@ using System; using System.Collections.Generic; using System.IO; using System.Text.Json; -using Emby.Server.Implementations.LiveTv.Listings.SchedulesDirectDtos; using Jellyfin.Extensions.Json; +using Jellyfin.LiveTv.Listings.SchedulesDirectDtos; using Xunit; -namespace Jellyfin.Server.Implementations.Tests.LiveTv.SchedulesDirect +namespace Jellyfin.LiveTv.Tests.SchedulesDirect { public class SchedulesDirectDeserializeTests { diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/discover.json b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/10.10.10.100/discover.json index a4ad4ed44..a4ad4ed44 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/10.10.10.100/discover.json +++ b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/10.10.10.100/discover.json diff --git a/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/10.10.10.100/lineup.json b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/10.10.10.100/lineup.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/10.10.10.100/lineup.json @@ -0,0 +1 @@ +{} diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/192.168.1.182/discover.json b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/192.168.1.182/discover.json index 851f17bb2..851f17bb2 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/192.168.1.182/discover.json +++ b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/192.168.1.182/discover.json diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/192.168.1.182/lineup.json b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/192.168.1.182/lineup.json index 4cb5ebc8e..4cb5ebc8e 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/192.168.1.182/lineup.json +++ b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/192.168.1.182/lineup.json diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml index dd4aa8977..dd4aa8977 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml +++ b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/Listings/XmlTv/emptycategory.xml diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/notitle.xml b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/Listings/XmlTv/notitle.xml index 5a5be7997..5a5be7997 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/LiveTv/Listings/XmlTv/notitle.xml +++ b/tests/Jellyfin.LiveTv.Tests/Test Data/LiveTv/Listings/XmlTv/notitle.xml diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/headends_response.json b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/headends_response.json index 015afeecc..015afeecc 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/headends_response.json +++ b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/headends_response.json diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineup_response.json b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/lineup_response.json index 072089470..072089470 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineup_response.json +++ b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/lineup_response.json diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineups_response.json b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/lineups_response.json index 032a84e59..032a84e59 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/lineups_response.json +++ b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/lineups_response.json diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/metadata_programs_response.json b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/metadata_programs_response.json index 78166f09a..78166f09a 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/metadata_programs_response.json +++ b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/metadata_programs_response.json diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/programs_response.json b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/programs_response.json index fe2a94436..fe2a94436 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/programs_response.json +++ b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/programs_response.json diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_request.json b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/schedules_request.json index 5ef1bfb1c..5ef1bfb1c 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_request.json +++ b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/schedules_request.json diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_response.json b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/schedules_response.json index 4a97e5517..4a97e5517 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/schedules_response.json +++ b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/schedules_response.json diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_live_response.json b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/token_live_response.json index e5fb64a6f..e5fb64a6f 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_live_response.json +++ b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/token_live_response.json diff --git a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_offline_response.json b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/token_offline_response.json index b66a4ed0c..b66a4ed0c 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Test Data/SchedulesDirect/token_offline_response.json +++ b/tests/Jellyfin.LiveTv.Tests/Test Data/SchedulesDirect/token_offline_response.json diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs index 198dc63ef..344ac8971 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs +++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs @@ -46,7 +46,7 @@ namespace Jellyfin.MediaEncoding.Tests.Probing var internalMediaInfoResult = JsonSerializer.Deserialize<InternalMediaInfoResult>(bytes, _jsonOptions); MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_metadata.mkv", MediaProtocol.File); - Assert.Single(res.MediaStreams); + Assert.Equal(3, res.MediaStreams.Count); Assert.NotNull(res.VideoStream); Assert.Equal("4:3", res.VideoStream.AspectRatio); @@ -83,6 +83,14 @@ namespace Jellyfin.MediaEncoding.Tests.Probing Assert.Equal(1, res.VideoStream.BlPresentFlag); Assert.Equal(0, res.VideoStream.DvBlSignalCompatibilityId); + var audio1 = res.MediaStreams[1]; + Assert.Equal("eac3", audio1.Codec); + Assert.Equal(AudioSpatialFormat.DolbyAtmos, audio1.AudioSpatialFormat); + + var audio2 = res.MediaStreams[2]; + Assert.Equal("dts", audio2.Codec); + Assert.Equal(AudioSpatialFormat.DTSX, audio2.AudioSpatialFormat); + Assert.Empty(res.Chapters); Assert.Equal("Just color bars", res.Overview); } diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_metadata.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_metadata.json index 519d81179..a49c68690 100644 --- a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_metadata.json +++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_metadata.json @@ -61,6 +61,92 @@ "dv_bl_signal_compatibility_id": 0 } ] + }, + { + "index": 1, + "codec_name": "eac3", + "codec_long_name": "ATSC A/52B (AC-3, E-AC-3)", + "profile": "Dolby Digital Plus + Dolby Atmos", + "codec_type": "audio", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "sample_fmt": "fltp", + "sample_rate": "48000", + "channels": 6, + "channel_layout": "5.1(side)", + "bits_per_sample": 0, + "initial_padding": 0, + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "bit_rate": "640000", + "disposition": { + "default": 1, + "dub": 0, + "original": 1, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0, + "captions": 0, + "descriptions": 0, + "metadata": 0, + "dependent": 0, + "still_image": 0 + }, + "tags": { + "language": "eng" + } + }, + { + "index": 2, + "codec_name": "dts", + "codec_long_name": "DCA (DTS Coherent Acoustics)", + "profile": "DTS-HD MA + DTS:X", + "codec_type": "audio", + "codec_tag_string": "[0][0][0][0]", + "codec_tag": "0x0000", + "sample_fmt": "s32p", + "sample_rate": "48000", + "channels": 8, + "channel_layout": "7.1", + "bits_per_sample": 0, + "initial_padding": 0, + "r_frame_rate": "0/0", + "avg_frame_rate": "0/0", + "time_base": "1/1000", + "start_pts": 0, + "start_time": "0.000000", + "bits_per_raw_sample": "24", + "disposition": { + "default": 0, + "dub": 0, + "original": 0, + "comment": 0, + "lyrics": 0, + "karaoke": 0, + "forced": 0, + "hearing_impaired": 0, + "visual_impaired": 0, + "clean_effects": 0, + "attached_pic": 0, + "timed_thumbnails": 0, + "captions": 0, + "descriptions": 0, + "metadata": 0, + "dependent": 0, + "still_image": 0 + }, + "tags": { + "language": "eng" + } } ], "chapters": [ @@ -68,7 +154,7 @@ ], "format": { "filename": "some_metadata.mkv", - "nb_streams": 1, + "nb_streams": 3, "nb_programs": 0, "format_name": "matroska,webm", "format_long_name": "Matroska / WebM", diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs index 210ce4a47..2f84fa544 100644 --- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs +++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs @@ -27,7 +27,7 @@ namespace Jellyfin.Model.Tests [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")] - [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450 + [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported, "Remux")] // #6450 [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 // Firefox @@ -38,7 +38,7 @@ namespace Jellyfin.Model.Tests [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")] - [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450 + [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported, "Remux")] // #6450 [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 // Safari @@ -89,7 +89,7 @@ namespace Jellyfin.Model.Tests [InlineData("Chrome-NoHLS", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Chrome-NoHLS", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode", "http")] [InlineData("Chrome-NoHLS", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode", "http")] - [InlineData("Chrome-NoHLS", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450 + [InlineData("Chrome-NoHLS", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported, "Remux")] // #6450 [InlineData("Chrome-NoHLS", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Chrome-NoHLS", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 // TranscodeMedia @@ -177,7 +177,7 @@ namespace Jellyfin.Model.Tests [InlineData("Chrome", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Chrome", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] - [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450 + [InlineData("Chrome", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported, "Remux")] // #6450 [InlineData("Chrome", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Chrome", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 // Firefox @@ -187,7 +187,7 @@ namespace Jellyfin.Model.Tests [InlineData("Firefox", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Firefox", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Transcode")] [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioCodecNotSupported, "Transcode")] - [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported)] // #6450 + [InlineData("Firefox", "mkv-vp9-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.ContainerNotSupported, "Remux")] // #6450 [InlineData("Firefox", "mkv-vp9-ac3-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioCodecNotSupported)] // #6450 [InlineData("Firefox", "mkv-vp9-vorbis-vtt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 // Safari @@ -274,13 +274,16 @@ namespace Jellyfin.Model.Tests [Theory] // Chrome [InlineData("Chrome", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 + [InlineData("Chrome", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] [InlineData("Chrome", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectStream, TranscodeReason.AudioIsExternal)] // #6450 [InlineData("Chrome", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")] // Firefox [InlineData("Firefox", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 + [InlineData("Firefox", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] [InlineData("Firefox", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.SecondaryAudioNotSupported, "Transcode")] // Yatse [InlineData("Yatse", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 + [InlineData("Yatse", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] [InlineData("Yatse", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // #6450 // RokuSSPlus [InlineData("RokuSSPlus", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // #6450 @@ -291,11 +294,13 @@ namespace Jellyfin.Model.Tests [InlineData("AndroidTVExoPlayer", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] [InlineData("AndroidTVExoPlayer", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] // Tizen 3 Stereo - [InlineData("Tizen3-stereo", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] - [InlineData("Tizen3-stereo", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] + [InlineData("Tizen3-stereo", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] + [InlineData("Tizen3-stereo", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] + [InlineData("Tizen3-stereo", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] // Tizen 4 4K 5.1 - [InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] - [InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectPlay, (TranscodeReason)0, "Remux")] + [InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] + [InlineData("Tizen4-4K-5.1", "mp4-h264-ac3-aac-aac-srt-2600k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] + [InlineData("Tizen4-4K-5.1", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.DirectStream, TranscodeReason.SecondaryAudioNotSupported, "Remux")] public async Task BuildVideoItemWithDirectPlayExplicitStreams(string deviceName, string mediaSource, PlayMethod? playMethod, TranscodeReason why = default, string transcodeMode = "DirectStream", string transcodeProtocol = "") { var options = await GetMediaOptions(deviceName, mediaSource); @@ -419,14 +424,7 @@ namespace Jellyfin.Model.Tests if (targetAudioStream?.IsExternal == false) { // Check expected audio codecs (1) - if (streamInfo.TranscodeReasons.HasFlag(TranscodeReason.ContainerNotSupported)) - { - Assert.Contains(targetAudioStream.Codec, streamInfo.AudioCodecs); - } - else - { - Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs); - } + Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs); } } else if (transcodeMode.Equals("Remux", StringComparison.Ordinal)) diff --git a/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs b/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs index d39a22e30..f4c0d9fe8 100644 --- a/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs +++ b/tests/Jellyfin.Model.Tests/Entities/MediaStreamTests.cs @@ -192,7 +192,7 @@ namespace Jellyfin.Model.Tests.Entities [InlineData(4090, 3070, false, "4K")] [InlineData(7680, 4320, false, "8K")] [InlineData(8190, 6140, false, "8K")] - public void GetResolutionText_Valid(int? width, int? height, bool interlaced, string expected) + public void GetResolutionText_Valid(int? width, int? height, bool interlaced, string? expected) { var mediaStream = new MediaStream() { diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json index 53637b793..2e3e6e6de 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen3-stereo.json @@ -414,6 +414,19 @@ ], "CodecProfiles": [ { + "Type": "VideoAudio", + "Conditions": [ + { + "Condition": "Equals", + "Property": "IsSecondaryAudio", + "Value": "false", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "$type": "CodecProfile" + }, + { "Type": "Video", "Conditions": [ { diff --git a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json index d3ef22c25..156230471 100644 --- a/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json +++ b/tests/Jellyfin.Model.Tests/Test Data/DeviceProfile-Tizen4-4K-5.1.json @@ -414,6 +414,19 @@ ], "CodecProfiles": [ { + "Type": "VideoAudio", + "Conditions": [ + { + "Condition": "Equals", + "Property": "IsSecondaryAudio", + "Value": "false", + "IsRequired": false, + "$type": "ProfileCondition" + } + ], + "$type": "CodecProfile" + }, + { "Type": "Video", "Conditions": [ { diff --git a/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-aac-srt-2600k.json b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-aac-srt-2600k.json new file mode 100644 index 000000000..9d819c4ad --- /dev/null +++ b/tests/Jellyfin.Model.Tests/Test Data/MediaSourceInfo-mp4-h264-ac3-aac-aac-srt-2600k.json @@ -0,0 +1,102 @@ +{ + "Id": "a766d122b58e45d9492d17af77748bf5", + "Path": "/Media/MyVideo-720p.mp4", + "Container": "mov,mp4,m4a,3gp,3g2,mj2", + "Size": 835317696, + "Name": "MyVideo-720p", + "ETag": "579a34c6d5dfb21d81539a51220b6a23", + "RunTimeTicks": 25801230336, + "SupportsTranscoding": true, + "SupportsDirectStream": true, + "SupportsDirectPlay": true, + "SupportsProbing": true, + "MediaStreams": [ + { + "Codec": "h264", + "CodecTag": "avc1", + "Language": "eng", + "TimeBase": "1/11988", + "VideoRange": "SDR", + "DisplayTitle": "720p H264 SDR", + "NalLengthSize": "0", + "BitRate": 2032876, + "BitDepth": 8, + "RefFrames": 1, + "IsDefault": true, + "Height": 720, + "Width": 1280, + "AverageFrameRate": 23.976, + "RealFrameRate": 23.976, + "Profile": "High", + "Type": 1, + "AspectRatio": "16:9", + "PixelFormat": "yuv420p", + "Level": 41 + }, + { + "Codec": "ac3", + "CodecTag": "ac-3", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - Dolby Digital - 5.1 - Default", + "ChannelLayout": "5.1", + "BitRate": 384000, + "Channels": 6, + "SampleRate": 48000, + "IsDefault": true, + "Index": 1, + "Score": 202 + }, + { + "Codec": "aac", + "CodecTag": "mp4a", + "Language": "eng", + "TimeBase": "1/48000", + "DisplayTitle": "En - AAC - Stereo - Default", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": true, + "Profile": "LC", + "Index": 2, + "Score": 203 + }, + { + "Codec": "aac", + "CodecTag": "mp4a", + "Language": "rus", + "TimeBase": "1/48000", + "DisplayTitle": "Ru - AAC - Stereo - Default", + "ChannelLayout": "stereo", + "BitRate": 164741, + "Channels": 2, + "SampleRate": 48000, + "IsDefault": false, + "Profile": "LC", + "Index": 3, + "Score": 203 + }, + { + "Codec": "srt", + "Language": "eng", + "TimeBase": "1/1000000", + "localizedUndefined": "Undefined", + "localizedDefault": "Default", + "localizedForced": "Forced", + "DisplayTitle": "En - Default", + "BitRate": 92, + "IsDefault": true, + "Type": 2, + "Index": 4, + "Score": 6421, + "IsExternal": true, + "IsTextSubtitleStream": true, + "SupportsExternalStream": true, + "Path": "/Media/MyVideo-WEBDL-2160p.default.eng.srt" + } + ], + "Bitrate": 2590008, + "DefaultAudioStreamIndex": 1, + "DefaultSubtitleStreamIndex": 4 +} diff --git a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs index 62d60e5a4..5029a8793 100644 --- a/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs +++ b/tests/Jellyfin.Naming.Tests/Video/CleanDateTimeTests.cs @@ -52,9 +52,8 @@ namespace Jellyfin.Naming.Tests.Video [InlineData("My Movie 2013-12-09", "My Movie 2013-12-09", null)] [InlineData("My Movie 20131209", "My Movie 20131209", null)] [InlineData("My Movie 2013-12-09 2013", "My Movie 2013-12-09", 2013)] - [InlineData(null, null, null)] [InlineData("", "", null)] - public void CleanDateTimeTest(string input, string expectedName, int? expectedYear) + public void CleanDateTimeTest(string input, string? expectedName, int? expectedYear) { input = Path.GetFileName(input); diff --git a/tests/Jellyfin.Networking.Tests/Configuration/NetworkConfigurationTests.cs b/tests/Jellyfin.Networking.Tests/Configuration/NetworkConfigurationTests.cs index 30726f1d3..f337fe20b 100644 --- a/tests/Jellyfin.Networking.Tests/Configuration/NetworkConfigurationTests.cs +++ b/tests/Jellyfin.Networking.Tests/Configuration/NetworkConfigurationTests.cs @@ -6,7 +6,6 @@ namespace Jellyfin.Networking.Tests.Configuration; public static class NetworkConfigurationTests { [Theory] - [InlineData("", null)] [InlineData("", "")] [InlineData("/Test", "/Test")] [InlineData("/Test", "Test")] diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs index 2bc686a33..85963e5de 100644 --- a/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/EmbeddedImageProviderTests.cs @@ -55,7 +55,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo [InlineData("clearlogo.png", null, 1, ImageType.Logo, ImageFormat.Png)] // extract extension from name [InlineData("backdrop", "image/bmp", 2, ImageType.Backdrop, ImageFormat.Bmp)] // extract extension from mimetype [InlineData("poster", null, 3, ImageType.Primary, ImageFormat.Jpg)] // default extension to jpg - public async void GetImage_Attachment_ReturnsCorrectSelection(string filename, string mimetype, int targetIndex, ImageType type, ImageFormat? expectedFormat) + public async void GetImage_Attachment_ReturnsCorrectSelection(string filename, string? mimetype, int targetIndex, ImageType type, ImageFormat? expectedFormat) { var attachments = new List<MediaAttachment>(); string pathPrefix = "path"; @@ -103,7 +103,7 @@ namespace Jellyfin.Providers.Tests.MediaInfo [InlineData(null, "mjpeg", 1, ImageType.Primary, ImageFormat.Jpg)] [InlineData(null, "png", 1, ImageType.Primary, ImageFormat.Png)] [InlineData(null, "webp", 1, ImageType.Primary, ImageFormat.Webp)] - public async void GetImage_Embedded_ReturnsCorrectSelection(string label, string? codec, int targetIndex, ImageType type, ImageFormat? expectedFormat) + public async void GetImage_Embedded_ReturnsCorrectSelection(string? label, string? codec, int targetIndex, ImageType type, ImageFormat? expectedFormat) { var streams = new List<MediaStream>(); for (int i = 1; i <= targetIndex; i++) diff --git a/tests/Jellyfin.Providers.Tests/Tmdb/TmdbUtilsTests.cs b/tests/Jellyfin.Providers.Tests/Tmdb/TmdbUtilsTests.cs index efd2d9553..0bfa330cb 100644 --- a/tests/Jellyfin.Providers.Tests/Tmdb/TmdbUtilsTests.cs +++ b/tests/Jellyfin.Providers.Tests/Tmdb/TmdbUtilsTests.cs @@ -25,14 +25,11 @@ namespace Jellyfin.Providers.Tests.Tmdb } [Theory] - [InlineData(null, null, null)] - [InlineData(null, "en-US", null)] - [InlineData("en", null, "en")] [InlineData("en", "en-US", "en-US")] [InlineData("fr-CA", "fr-BE", "fr-CA")] [InlineData("fr-CA", "fr", "fr-CA")] [InlineData("de", "en-US", "de")] - public static void AdjustImageLanguage_Valid_Success(string imageLanguage, string requestLanguage, string expected) + public static void AdjustImageLanguage_Valid_Success(string imageLanguage, string requestLanguage, string? expected) { Assert.Equal(expected, TmdbUtils.AdjustImageLanguage(imageLanguage, requestLanguage)); } diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs index 16202aea9..5aa7c04f6 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/AudioResolverTests.cs @@ -63,7 +63,7 @@ public class AudioResolverTests null, Mock.Of<ILibraryManager>()) { - CollectionType = CollectionType.Books, + CollectionType = CollectionType.books, FileInfo = new FileSystemMetadata { FullName = parent, diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs index 92bac722b..cc2e47c33 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/EpisodeResolverTest.cs @@ -29,7 +29,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library null) { Parent = parent, - CollectionType = CollectionType.TvShows, + CollectionType = CollectionType.tvshows, FileInfo = new FileSystemMetadata { FullName = "All My Children/Season 01/Extras/All My Children S01E01 - Behind The Scenes.mkv" @@ -52,7 +52,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library null) { Parent = series, - CollectionType = CollectionType.TvShows, + CollectionType = CollectionType.tvshows, FileInfo = new FileSystemMetadata { FullName = "Extras/Extras S01E01.mkv" diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs index 1c35eb3f5..d1be07aa2 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/PathExtensionsTests.cs @@ -83,7 +83,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData(@"\home/jeff\myfile.mkv", '\\', @"\home\jeff\myfile.mkv")] [InlineData(@"\home/jeff\myfile.mkv", '/', "/home/jeff/myfile.mkv")] [InlineData("", '/', "")] - public void NormalizePath_SpecifyingSeparator_Normalizes(string path, char separator, string expectedPath) + public void NormalizePath_SpecifyingSeparator_Normalizes(string? path, char separator, string? expectedPath) { Assert.Equal(expectedPath, path.NormalizePath(separator)); } diff --git a/tests/Jellyfin.Server.Implementations.Tests/QuickConnect/QuickConnectManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/QuickConnect/QuickConnectManagerTests.cs index c32d89ea5..30f72f595 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/QuickConnect/QuickConnectManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/QuickConnect/QuickConnectManagerTests.cs @@ -85,10 +85,10 @@ namespace Jellyfin.Server.Implementations.Tests.QuickConnect } [Fact] - public void AuthorizeRequest_QuickConnectUnavailable_ThrowsAuthenticationException() + public async Task AuthorizeRequest_QuickConnectUnavailable_ThrowsAuthenticationException() { _config.QuickConnectAvailable = false; - Assert.ThrowsAsync<AuthenticationException>(() => _quickConnectManager.AuthorizeRequest(Guid.Empty, string.Empty)); + await Assert.ThrowsAsync<AuthenticationException>(() => _quickConnectManager.AuthorizeRequest(Guid.Empty, string.Empty)); } [Fact] diff --git a/tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionManagerTests.cs index ebd3a3891..e463d838e 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/SessionManager/SessionManagerTests.cs @@ -21,7 +21,7 @@ public class SessionManagerTests [Theory] [InlineData("", typeof(ArgumentException))] [InlineData(null, typeof(ArgumentNullException))] - public async Task GetAuthorizationToken_Should_ThrowException(string deviceId, Type exceptionType) + public async Task GetAuthorizationToken_Should_ThrowException(string? deviceId, Type exceptionType) { await using var sessionManager = new Emby.Server.Implementations.Session.SessionManager( NullLogger<Emby.Server.Implementations.Session.SessionManager>.Instance, diff --git a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs index 4e8aec9f1..2f2149504 100644 --- a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs +++ b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs @@ -7,6 +7,7 @@ using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Api.Models.StartupDtos; using Jellyfin.Api.Models.UserDtos; +using Jellyfin.Extensions; using Jellyfin.Extensions.Json; using MediaBrowser.Model.Dto; using Xunit; @@ -56,7 +57,7 @@ namespace Jellyfin.Server.Integration.Tests public static async Task<BaseItemDto> GetRootFolderDtoAsync(HttpClient client, Guid userId = default) { - if (userId.Equals(default)) + if (userId.IsEmpty()) { var userDto = await GetUserDtoAsync(client); userId = userDto.Id; diff --git a/tests/coverletArgs.runsettings b/tests/coverletArgs.runsettings index 3113957e0..7d4837389 100644 --- a/tests/coverletArgs.runsettings +++ b/tests/coverletArgs.runsettings @@ -4,14 +4,15 @@ <DataCollectors> <DataCollector friendlyName="XPlat code coverage"> <Configuration> - <Format>cobertura</Format> + <Format>cobertura</Format> <Exclude>[coverlet.*.tests?]*,[*]Coverlet.Core*,[*]Moq*</Exclude> <!-- [Assembly-Filter]Type-Filter --> <ExcludeByAttribute>Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute</ExcludeByAttribute> <SingleHit>false</SingleHit> - <UseSourceLink>true</UseSourceLink> + <UseSourceLink>false</UseSourceLink> <IncludeTestAssembly>false</IncludeTestAssembly> + <SkipAutoProps>true</SkipAutoProps> </Configuration> </DataCollector> </DataCollectors> </DataCollectionRunSettings> -</RunSettings>
\ No newline at end of file +</RunSettings> |
