Merge branch 'jellyfin:master' into cast-with-localhost-server

This commit is contained in:
Sky High 2024-01-10 18:18:13 +01:00 committed by GitHub
commit c83432f9e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
162 changed files with 1834 additions and 1361 deletions

View File

@ -3,7 +3,7 @@
"isRoot": true, "isRoot": true,
"tools": { "tools": {
"dotnet-ef": { "dotnet-ef": {
"version": "8.0.0", "version": "8.0.1",
"commands": [ "commands": [
"dotnet-ef" "dotnet-ef"
] ]

View File

@ -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
}
}

View File

@ -27,11 +27,11 @@ jobs:
dotnet-version: '8.0.x' dotnet-version: '8.0.x'
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@c0d1daa7f7e14667747d73a7dbbe8c074bc8bfe2 # v2.22.9 uses: github/codeql-action/init@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 # v3.23.0
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
queries: +security-extended queries: +security-extended
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@c0d1daa7f7e14667747d73a7dbbe8c074bc8bfe2 # v2.22.9 uses: github/codeql-action/autobuild@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 # v3.23.0
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@c0d1daa7f7e14667747d73a7dbbe8c074bc8bfe2 # v2.22.9 uses: github/codeql-action/analyze@e5f05b81d5b6ff8cfa111c80c22c5fd02a384118 # v3.23.0

View File

@ -25,7 +25,7 @@ jobs:
- name: Generate openapi.json - 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" 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 - name: Upload openapi.json
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0
with: with:
name: openapi-head name: openapi-head
retention-days: 14 retention-days: 14
@ -59,7 +59,7 @@ jobs:
- name: Generate openapi.json - 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" 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 - name: Upload openapi.json
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 uses: actions/upload-artifact@c7d193f32edcb7bfad88892161225aeda64e9392 # v4.0.0
with: with:
name: openapi-base name: openapi-base
retention-days: 14 retention-days: 14
@ -78,12 +78,12 @@ jobs:
- openapi-base - openapi-base
steps: steps:
- name: Download openapi-head - name: Download openapi-head
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0
with: with:
name: openapi-head name: openapi-head
path: openapi-head path: openapi-head
- name: Download openapi-base - name: Download openapi-base
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 uses: actions/download-artifact@f44cd7b40bfd40b6aa1cc1b9b5b7bf03d3c67110 # v4.1.0
with: with:
name: openapi-base name: openapi-base
path: openapi-base path: openapi-base

View File

@ -19,9 +19,9 @@ jobs:
runs-on: "${{ matrix.os }}" runs-on: "${{ matrix.os }}"
steps: 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: with:
dotnet-version: ${{ env.SDK_VERSION }} dotnet-version: ${{ env.SDK_VERSION }}
@ -34,7 +34,7 @@ jobs:
--verbosity minimal --verbosity minimal
- name: Merge code coverage results - name: Merge code coverage results
uses: danielpalme/ReportGenerator-GitHub-Action@4d510cbed8a05af5aefea46c7fd6e05b95844c89 # 5 uses: danielpalme/ReportGenerator-GitHub-Action@4d510cbed8a05af5aefea46c7fd6e05b95844c89 # 5.2.0
with: with:
reports: "**/coverage.cobertura.xml" reports: "**/coverage.cobertura.xml"
targetdir: "merged/" targetdir: "merged/"
@ -42,9 +42,3 @@ jobs:
# TODO - which action / tool to use to publish code coverage results? # TODO - which action / tool to use to publish code coverage results?
# - name: 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"

View File

@ -15,7 +15,7 @@ jobs:
if: ${{ github.repository == 'jellyfin/jellyfin' }} if: ${{ github.repository == 'jellyfin/jellyfin' }}
steps: steps:
- name: Remove from 'Current Release' project - 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') if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true continue-on-error: true
with: with:
@ -24,7 +24,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }} repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Release Next' project - 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' if: (github.event.pull_request || github.event.issue.pull_request) && github.event.action == 'opened'
continue-on-error: true continue-on-error: true
with: with:
@ -33,7 +33,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }} repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add to 'Current Release' project - 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') if: (github.event.pull_request || github.event.issue.pull_request) && !contains(github.event.*.labels.*.name, 'stable backport')
continue-on-error: true continue-on-error: true
with: 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)" 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 - 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 if: github.event.issue.pull_request == '' && github.event.comment.author_association == 'MEMBER' && steps.member_comments.outputs.number <= 1
continue-on-error: true continue-on-error: true
with: with:
@ -56,7 +56,7 @@ jobs:
repo-token: ${{ secrets.JF_BOT_TOKEN }} repo-token: ${{ secrets.JF_BOT_TOKEN }}
- name: Add issue to triage project - 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' if: github.event.issue.pull_request == '' && github.event.action == 'opened'
continue-on-error: true continue-on-error: true
with: with:

View File

@ -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": [ "recommendations": [
"ms-dotnettools.csharp", "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": [ "unwantedRecommendations": [
] ]

View File

@ -81,6 +81,7 @@
- [Maxr1998](https://github.com/Maxr1998) - [Maxr1998](https://github.com/Maxr1998)
- [mcarlton00](https://github.com/mcarlton00) - [mcarlton00](https://github.com/mcarlton00)
- [mitchfizz05](https://github.com/mitchfizz05) - [mitchfizz05](https://github.com/mitchfizz05)
- [mohd-akram](https://github.com/mohd-akram)
- [MrTimscampi](https://github.com/MrTimscampi) - [MrTimscampi](https://github.com/MrTimscampi)
- [n8225](https://github.com/n8225) - [n8225](https://github.com/n8225)
- [Nalsai](https://github.com/Nalsai) - [Nalsai](https://github.com/Nalsai)

View File

@ -15,48 +15,48 @@
<PackageVersion Include="Diacritics" Version="3.3.18" /> <PackageVersion Include="Diacritics" Version="3.3.18" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.0.0" /> <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="4.1.1" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" /> <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" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.4" /> <PackageVersion Include="IDisposableAnalyzers" Version="4.0.4" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="3.6.13" /> <PackageVersion Include="libse" Version="3.6.13" />
<PackageVersion Include="LrcParser" Version="2023.524.0" /> <PackageVersion Include="LrcParser" Version="2023.524.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.1" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="6.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.0" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="8.0.1" />
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" /> <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.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.0" /> <PackageVersion Include="Microsoft.Data.Sqlite" Version="8.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.0" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.0" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" 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.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.EnvironmentVariables" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" 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.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" 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.EntityFrameworkCore" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="8.0.1" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" /> <PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Http" 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.Abstractions" Version="8.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" 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="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageVersion Include="MimeTypes" Version="2.4.0" /> <PackageVersion Include="MimeTypes" Version="2.4.0" />
<PackageVersion Include="Mono.Nat" Version="3.0.4" /> <PackageVersion Include="Mono.Nat" Version="3.0.4" />
<PackageVersion Include="Moq" Version="4.18.4" /> <PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="NEbml" Version="0.11.0" /> <PackageVersion Include="NEbml" Version="0.11.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" /> <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="PlaylistsNET" Version="1.4.0" /> <PackageVersion Include="PlaylistsNET" Version="1.4.1" />
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.0" /> <PackageVersion Include="prometheus-net.AspNetCore" Version="8.2.1" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" /> <PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
<PackageVersion Include="prometheus-net" Version="8.2.0" /> <PackageVersion Include="prometheus-net" Version="8.2.1" />
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.0" /> <PackageVersion Include="Serilog.AspNetCore" Version="8.0.0" />
<PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" /> <PackageVersion Include="Serilog.Enrichers.Thread" Version="3.1.0" />
<PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.0" /> <PackageVersion Include="Serilog.Settings.Configuration" Version="8.0.0" />
@ -70,21 +70,21 @@
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.5" /> <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.5" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" /> <PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
<PackageVersion Include="Svg.Skia" Version="1.0.0.2" /> <PackageVersion Include="Svg.Skia" Version="1.0.0.2" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageVersion Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageVersion Include="System.Globalization" Version="4.3.0" /> <PackageVersion Include="System.Globalization" Version="4.3.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" /> <PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="8.0.0" /> <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="System.Threading.Tasks.Dataflow" Version="8.0.0" />
<PackageVersion Include="TagLibSharp" Version="2.3.0" /> <PackageVersion Include="TagLibSharp" Version="2.3.0" />
<PackageVersion Include="TMDbLib" Version="2.1.0" /> <PackageVersion Include="TMDbLib" Version="2.1.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" /> <PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" /> <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.SkippableFact" Version="1.4.13" />
<PackageVersion Include="xunit" Version="2.6.1" /> <PackageVersion Include="xunit" Version="2.6.5" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -76,6 +76,7 @@ using MediaBrowser.Controller.TV;
using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.MediaEncoding.BdInfo;
using MediaBrowser.MediaEncoding.Subtitles; using MediaBrowser.MediaEncoding.Subtitles;
using MediaBrowser.MediaEncoding.Transcoding;
using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
@ -583,7 +584,7 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>(); serviceCollection.AddSingleton<IAttachmentExtractor, MediaBrowser.MediaEncoding.Attachments.AttachmentExtractor>();
serviceCollection.AddSingleton<TranscodingJobHelper>(); serviceCollection.AddSingleton<ITranscodeManager, TranscodeManager>();
serviceCollection.AddScoped<MediaInfoHelper>(); serviceCollection.AddScoped<MediaInfoHelper>();
serviceCollection.AddScoped<AudioHelper>(); serviceCollection.AddScoped<AudioHelper>();
serviceCollection.AddScoped<DynamicHlsHelper>(); serviceCollection.AddScoped<DynamicHlsHelper>();

View File

@ -812,11 +812,16 @@ namespace Emby.Server.Implementations.Channels
{ {
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{ {
await using FileStream jsonStream = AsyncFile.OpenRead(cachePath); var jsonStream = AsyncFile.OpenRead(cachePath);
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); await using (jsonStream.ConfigureAwait(false))
if (cachedResult is not null)
{ {
return null; var cachedResult = await JsonSerializer
.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken)
.ConfigureAwait(false);
if (cachedResult is not null)
{
return null;
}
} }
} }
} }
@ -835,11 +840,16 @@ namespace Emby.Server.Implementations.Channels
{ {
if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow) if (_fileSystem.GetLastWriteTimeUtc(cachePath).Add(cacheLength) > DateTime.UtcNow)
{ {
await using FileStream jsonStream = AsyncFile.OpenRead(cachePath); var jsonStream = AsyncFile.OpenRead(cachePath);
var cachedResult = await JsonSerializer.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); await using (jsonStream.ConfigureAwait(false))
if (cachedResult is not null)
{ {
return null; var cachedResult = await JsonSerializer
.DeserializeAsync<ChannelItemResult>(jsonStream, _jsonOptions, cancellationToken)
.ConfigureAwait(false);
if (cachedResult is not null)
{
return null;
}
} }
} }
} }
@ -867,7 +877,7 @@ namespace Emby.Server.Implementations.Channels
throw new InvalidOperationException("Channel returned a null result from GetChannelItems"); throw new InvalidOperationException("Channel returned a null result from GetChannelItems");
} }
await CacheResponse(result, cachePath); await CacheResponse(result, cachePath).ConfigureAwait(false);
return result; return result;
} }
@ -883,8 +893,11 @@ namespace Emby.Server.Implementations.Channels
{ {
Directory.CreateDirectory(Path.GetDirectoryName(path)); Directory.CreateDirectory(Path.GetDirectoryName(path));
await using FileStream createStream = File.Create(path); var createStream = File.Create(path);
await JsonSerializer.SerializeAsync(createStream, result, _jsonOptions).ConfigureAwait(false); await using (createStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(createStream, result, _jsonOptions).ConfigureAwait(false);
}
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -104,6 +104,13 @@ namespace Emby.Server.Implementations.Data
if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult)) 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; result = dateTimeResult;
return true; return true;
} }

View File

@ -418,15 +418,6 @@ namespace Emby.Server.Implementations.Dto
{ {
dto.PlayAccess = item.GetPlayAccess(user); 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) private static int GetChildCount(Folder folder, User user)

View File

@ -32,26 +32,26 @@ namespace Emby.Server.Implementations.Images
switch (viewType) switch (viewType)
{ {
case CollectionType.Movies: case CollectionType.movies:
includeItemTypes = new[] { BaseItemKind.Movie }; includeItemTypes = new[] { BaseItemKind.Movie };
break; break;
case CollectionType.TvShows: case CollectionType.tvshows:
includeItemTypes = new[] { BaseItemKind.Series }; includeItemTypes = new[] { BaseItemKind.Series };
break; break;
case CollectionType.Music: case CollectionType.music:
includeItemTypes = new[] { BaseItemKind.MusicAlbum }; includeItemTypes = new[] { BaseItemKind.MusicAlbum };
break; break;
case CollectionType.MusicVideos: case CollectionType.musicvideos:
includeItemTypes = new[] { BaseItemKind.MusicVideo }; includeItemTypes = new[] { BaseItemKind.MusicVideo };
break; break;
case CollectionType.Books: case CollectionType.books:
includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook }; includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook };
break; break;
case CollectionType.BoxSets: case CollectionType.boxsets:
includeItemTypes = new[] { BaseItemKind.BoxSet }; includeItemTypes = new[] { BaseItemKind.BoxSet };
break; break;
case CollectionType.HomeVideos: case CollectionType.homevideos:
case CollectionType.Photos: case CollectionType.photos:
includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo }; includeItemTypes = new[] { BaseItemKind.Video, BaseItemKind.Photo };
break; break;
default: default:
@ -59,7 +59,7 @@ namespace Emby.Server.Implementations.Images
break; break;
} }
var recursive = viewType != CollectionType.Playlists; var recursive = viewType != CollectionType.playlists;
return view.GetItemList(new InternalItemsQuery return view.GetItemList(new InternalItemsQuery
{ {

View File

@ -36,7 +36,7 @@ namespace Emby.Server.Implementations.Images
var view = (UserView)item; var view = (UserView)item;
var isUsingCollectionStrip = IsUsingCollectionStrip(view); 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 var result = view.GetItemList(new InternalItemsQuery
{ {
@ -114,9 +114,9 @@ namespace Emby.Server.Implementations.Images
{ {
CollectionType[] collectionStripViewTypes = CollectionType[] collectionStripViewTypes =
{ {
CollectionType.Movies, CollectionType.movies,
CollectionType.TvShows, CollectionType.tvshows,
CollectionType.Playlists CollectionType.playlists
}; };
return view?.ViewType is not null && collectionStripViewTypes.Contains(view.ViewType.Value); return view?.ViewType is not null && collectionStripViewTypes.Contains(view.ViewType.Value);

View File

@ -12,7 +12,7 @@ using MediaBrowser.Model.Dto;
namespace Emby.Server.Implementations.Library namespace Emby.Server.Implementations.Library
{ {
public class ExclusiveLiveStream : ILiveStream public sealed class ExclusiveLiveStream : ILiveStream
{ {
private readonly Func<Task> _closeFn; private readonly Func<Task> _closeFn;
@ -51,5 +51,10 @@ namespace Emby.Server.Implementations.Library
{ {
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <inheritdoc />
public void Dispose()
{
}
} }
} }

View File

@ -1514,7 +1514,7 @@ namespace Emby.Server.Implementations.Library
{ {
if (item is UserView view) if (item is UserView view)
{ {
if (view.ViewType == CollectionType.LiveTv) if (view.ViewType == CollectionType.livetv)
{ {
return new[] { view.Id }; return new[] { view.Id };
} }
@ -1543,7 +1543,7 @@ namespace Emby.Server.Implementations.Library
} }
// Handle grouping // 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) && user.GetPreference(PreferenceKind.GroupedFolders).Length > 0)
{ {
return GetUserRootFolder() return GetUserRootFolder()

View File

@ -61,7 +61,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
List<FileSystemMetadata> files, List<FileSystemMetadata> files,
CollectionType? collectionType) CollectionType? collectionType)
{ {
if (collectionType == CollectionType.Books) if (collectionType == CollectionType.books)
{ {
return ResolveMultipleAudio(parent, files, true); return ResolveMultipleAudio(parent, files, true);
} }
@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var collectionType = args.GetCollectionType(); var collectionType = args.GetCollectionType();
var isBooksCollectionType = collectionType == CollectionType.Books; var isBooksCollectionType = collectionType == CollectionType.books;
if (args.IsDirectory) if (args.IsDirectory)
{ {
@ -112,7 +112,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
MediaBrowser.Controller.Entities.Audio.Audio item = null; 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 // Use regular audio type for mixed libraries, owned items and music
if (isMixedCollectionType || if (isMixedCollectionType ||

View File

@ -55,7 +55,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
protected override MusicAlbum Resolve(ItemResolveArgs args) protected override MusicAlbum Resolve(ItemResolveArgs args)
{ {
var collectionType = args.GetCollectionType(); 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 there's a collection type and it's not music, don't allow it.
if (!isMusicMediaFolder) if (!isMusicMediaFolder)

View File

@ -65,7 +65,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
var collectionType = args.GetCollectionType(); 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 there's a collection type and it's not music, it can't be a music artist
if (!isMusicMediaFolder) if (!isMusicMediaFolder)

View File

@ -23,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
var collectionType = args.GetCollectionType(); var collectionType = args.GetCollectionType();
// Only process items that are in a collection folder containing books // Only process items that are in a collection folder containing books
if (collectionType != CollectionType.Books) if (collectionType != CollectionType.books)
{ {
return null; return null;
} }

View File

@ -31,11 +31,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
private static readonly CollectionType[] _validCollectionTypes = new[] private static readonly CollectionType[] _validCollectionTypes = new[]
{ {
CollectionType.Movies, CollectionType.movies,
CollectionType.HomeVideos, CollectionType.homevideos,
CollectionType.MusicVideos, CollectionType.musicvideos,
CollectionType.TvShows, CollectionType.tvshows,
CollectionType.Photos CollectionType.photos
}; };
/// <summary> /// <summary>
@ -100,12 +100,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
Video movie = null; Video movie = null;
var files = args.GetActualFileSystemChildren().ToList(); 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); 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); 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); 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); 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; Video item = null;
if (collectionType == CollectionType.MusicVideos) if (collectionType == CollectionType.musicvideos)
{ {
item = ResolveVideo<MusicVideo>(args, false); item = ResolveVideo<MusicVideo>(args, false);
} }
// To find a movie file, the collection type must be movies or boxsets // 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); 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); item = ResolveVideo<Video>(args, false);
} }
@ -195,12 +195,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
return null; return null;
} }
if (collectionType is CollectionType.MusicVideos) if (collectionType is CollectionType.musicvideos)
{ {
return ResolveVideos<MusicVideo>(parent, files, true, collectionType, false); 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); 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); return ResolveVideos<Movie>(parent, files, false, collectionType, true);
} }
if (collectionType == CollectionType.Movies) if (collectionType == CollectionType.movies)
{ {
return ResolveVideos<Movie>(parent, files, true, collectionType, true); return ResolveVideos<Movie>(parent, files, true, collectionType, true);
} }
if (collectionType == CollectionType.TvShows) if (collectionType == CollectionType.tvshows)
{ {
return ResolveVideos<Episode>(parent, files, false, collectionType, true); 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 multiDiscFolders = new List<FileSystemMetadata>();
var libraryOptions = args.LibraryOptions; var libraryOptions = args.LibraryOptions;
var supportPhotos = collectionType == CollectionType.HomeVideos && libraryOptions.EnablePhotos; var supportPhotos = collectionType == CollectionType.homevideos && libraryOptions.EnablePhotos;
var photos = new List<FileSystemMetadata>(); var photos = new List<FileSystemMetadata>();
// Search for a folder rip // 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) ?? var result = ResolveVideos<T>(parent, fileSystemEntries, SupportsMultiVersion, collectionType, parseName) ??
new MultiItemResolverResult(); new MultiItemResolverResult();
var isPhotosCollection = collectionType == CollectionType.HomeVideos || collectionType == CollectionType.Photos; var isPhotosCollection = collectionType == CollectionType.homevideos || collectionType == CollectionType.photos;
if (!isPhotosCollection && result.Items.Count == 1) if (!isPhotosCollection && result.Items.Count == 1)
{ {
var videoPath = result.Items[0].Path; var videoPath = result.Items[0].Path;

View File

@ -46,8 +46,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
// Must be an image file within a photo collection // Must be an image file within a photo collection
var collectionType = args.GetCollectionType(); var collectionType = args.GetCollectionType();
if (collectionType == CollectionType.Photos if (collectionType == CollectionType.photos
|| (collectionType == CollectionType.HomeVideos && args.LibraryOptions.EnablePhotos)) || (collectionType == CollectionType.homevideos && args.LibraryOptions.EnablePhotos))
{ {
if (HasPhotos(args)) if (HasPhotos(args))
{ {

View File

@ -61,8 +61,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
// Must be an image file within a photo collection // Must be an image file within a photo collection
var collectionType = args.CollectionType; var collectionType = args.CollectionType;
if (collectionType == CollectionType.Photos if (collectionType == CollectionType.photos
|| (collectionType == CollectionType.HomeVideos && args.LibraryOptions.EnablePhotos)) || (collectionType == CollectionType.homevideos && args.LibraryOptions.EnablePhotos))
{ {
if (IsImageFile(args.Path, _imageProcessor)) if (IsImageFile(args.Path, _imageProcessor))
{ {

View File

@ -23,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
private CollectionType?[] _musicPlaylistCollectionTypes = private CollectionType?[] _musicPlaylistCollectionTypes =
{ {
null, null,
CollectionType.Music CollectionType.music
}; };
/// <inheritdoc/> /// <inheritdoc/>

View File

@ -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 // 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 // Also handle flat tv folders
if (season is not null if (season is not null
|| args.GetCollectionType() == CollectionType.TvShows || args.GetCollectionType() == CollectionType.tvshows
|| args.HasParent<Series>()) || args.HasParent<Series>())
{ {
var episode = ResolveVideo<Episode>(args, false); var episode = ResolveVideo<Episode>(args, false);

View File

@ -60,11 +60,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var seriesInfo = Naming.TV.SeriesResolver.Resolve(_namingOptions, args.Path); var seriesInfo = Naming.TV.SeriesResolver.Resolve(_namingOptions, args.Path);
var collectionType = args.GetCollectionType(); 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 // TODO refactor into separate class or something, this is copied from LibraryManager.GetConfiguredContentType
var configuredContentType = args.GetConfiguredContentType(); var configuredContentType = args.GetConfiguredContentType();
if (configuredContentType != CollectionType.TvShows) if (configuredContentType != CollectionType.tvshows)
{ {
return new Series return new Series
{ {

View File

@ -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> /// <summary>
/// Save the provided user data for the given user. Batch operation. Does not fire any events or update the cache. /// Save the provided user data for the given user. Batch operation. Does not fire any events or update the cache.
/// </summary> /// </summary>

View File

@ -64,7 +64,7 @@ namespace Emby.Server.Implementations.Library
var folderViewType = collectionFolder?.CollectionType; var folderViewType = collectionFolder?.CollectionType;
// Playlist library requires special handling because the folder only references user playlists // 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) var items = folder.GetItemList(new InternalItemsQuery(user)
{ {
@ -99,14 +99,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) var parents = groupedFolders.Where(i => i.CollectionType == viewType || i.CollectionType is null)
.ToList(); .ToList();
if (parents.Count > 0) if (parents.Count > 0)
{ {
var localizationKey = viewType == CollectionType.TvShows var localizationKey = viewType == CollectionType.tvshows
? "TvShows" ? "TvShows"
: "Movies"; : "Movies";
@ -117,7 +117,7 @@ namespace Emby.Server.Implementations.Library
if (_config.Configuration.EnableFolderView) if (_config.Configuration.EnableFolderView)
{ {
var name = _localizationManager.GetLocalizedString("Folders"); 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) if (query.IncludeExternalContent)
@ -279,7 +279,7 @@ namespace Emby.Server.Implementations.Library
var isPlayed = request.IsPlayed; 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; isPlayed = null;
} }
@ -305,11 +305,11 @@ namespace Emby.Server.Implementations.Library
var hasCollectionType = parents.OfType<UserView>().ToArray(); var hasCollectionType = parents.OfType<UserView>().ToArray();
if (hasCollectionType.Length > 0) if (hasCollectionType.Length > 0)
{ {
if (hasCollectionType.All(i => i.CollectionType == CollectionType.Movies)) if (hasCollectionType.All(i => i.CollectionType == CollectionType.movies))
{ {
includeItemTypes = new[] { BaseItemKind.Movie }; 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 }; includeItemTypes = new[] { BaseItemKind.Episode };
} }
@ -324,18 +324,18 @@ namespace Emby.Server.Implementations.Library
{ {
switch (parent.CollectionType) switch (parent.CollectionType)
{ {
case CollectionType.Books: case CollectionType.books:
mediaTypes.Add(MediaType.Book); mediaTypes.Add(MediaType.Book);
mediaTypes.Add(MediaType.Audio); mediaTypes.Add(MediaType.Audio);
break; break;
case CollectionType.Music: case CollectionType.music:
mediaTypes.Add(MediaType.Audio); mediaTypes.Add(MediaType.Audio);
break; break;
case CollectionType.Photos: case CollectionType.photos:
mediaTypes.Add(MediaType.Photo); mediaTypes.Add(MediaType.Photo);
mediaTypes.Add(MediaType.Video); mediaTypes.Add(MediaType.Video);
break; break;
case CollectionType.HomeVideos: case CollectionType.homevideos:
mediaTypes.Add(MediaType.Photo); mediaTypes.Add(MediaType.Photo);
mediaTypes.Add(MediaType.Video); mediaTypes.Add(MediaType.Video);
break; break;

View File

@ -8,13 +8,14 @@ using System.Threading.Tasks;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV namespace Emby.Server.Implementations.LiveTv.EmbyTV
{ {
public class DirectRecorder : IRecorder public sealed class DirectRecorder : IRecorder
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
@ -46,7 +47,15 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
{ {
Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile))); 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(); onStarted();
@ -80,24 +89,31 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile))); 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 await _streamHelper.CopyUntilCancelled(
using var durationToken = new CancellationTokenSource(duration); await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false),
using var linkedCancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token); output,
cancellationToken = linkedCancellationToken.Token; IODefaults.CopyToBufferSize,
cancellationToken).ConfigureAwait(false);
await _streamHelper.CopyUntilCancelled( _logger.LogInformation("Recording completed to file {0}", targetFile);
await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false), }
output, }
IODefaults.CopyToBufferSize,
cancellationToken).ConfigureAwait(false);
_logger.LogInformation("Recording completed to file {0}", targetFile); /// <inheritdoc />
public void Dispose()
{
} }
} }
} }

View File

@ -37,12 +37,11 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Providers; using MediaBrowser.Model.Providers;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV namespace Emby.Server.Implementations.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"; public const string DateAddedFormat = "yyyy-MM-dd HH:mm:ss";
@ -74,7 +73,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim(1, 1); private readonly SemaphoreSlim _recordingDeleteSemaphore = new SemaphoreSlim(1, 1);
private bool _disposed = false; private bool _disposed;
public EmbyTV( public EmbyTV(
IServerApplicationHost appHost, IServerApplicationHost appHost,
@ -1270,7 +1269,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
directStreamProvider = liveStreamResponse.Item2; directStreamProvider = liveStreamResponse.Item2;
} }
var recorder = GetRecorder(mediaStreamInfo); using var recorder = GetRecorder(mediaStreamInfo);
recordPath = recorder.GetOutputPath(mediaStreamInfo, recordPath); recordPath = recorder.GetOutputPath(mediaStreamInfo, recordPath);
recordPath = EnsureFileUnique(recordPath, timer.Id); recordPath = EnsureFileUnique(recordPath, timer.Id);
@ -2524,22 +2523,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
/// <inheritdoc /> /// <inheritdoc />
public void Dispose() public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{ {
if (_disposed) if (_disposed)
{ {
return; return;
} }
if (disposing) _recordingDeleteSemaphore.Dispose();
{
_recordingDeleteSemaphore.Dispose();
}
foreach (var pair in _activeRecordings.ToList()) foreach (var pair in _activeRecordings.ToList())
{ {

View File

@ -25,7 +25,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.EmbyTV namespace Emby.Server.Implementations.LiveTv.EmbyTV
{ {
public class EncodedRecorder : IRecorder, IDisposable public class EncodedRecorder : IRecorder
{ {
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
@ -34,10 +34,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private bool _hasExited; private bool _hasExited;
private Stream _logFileStream; private FileStream _logFileStream;
private string _targetPath; private string _targetPath;
private Process _process; private Process _process;
private bool _disposed = false; private bool _disposed;
public EncodedRecorder( public EncodedRecorder(
ILogger logger, 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 try
{ {

View File

@ -8,7 +8,7 @@ using MediaBrowser.Model.Dto;
namespace Emby.Server.Implementations.LiveTv.EmbyTV namespace Emby.Server.Implementations.LiveTv.EmbyTV
{ {
public interface IRecorder public interface IRecorder : IDisposable
{ {
/// <summary> /// <summary>
/// Records the specified media source. /// Records the specified media source.

View File

@ -287,7 +287,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
IsMovie = IsMovie(details), IsMovie = IsMovie(details),
Etag = programInfo.Md5, Etag = programInfo.Md5,
IsLive = string.Equals(programInfo.LiveTapeDelay, "live", StringComparison.OrdinalIgnoreCase), 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; var showId = programId;
@ -414,7 +414,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
return null; return null;
} }
if (uri.IndexOf("http", StringComparison.OrdinalIgnoreCase) != -1) if (uri.Contains("http", StringComparison.OrdinalIgnoreCase))
{ {
return uri; return uri;
} }

View File

@ -84,38 +84,53 @@ namespace Emby.Server.Implementations.LiveTv.Listings
_logger.LogInformation("Downloading xmltv listings from {Path}", info.Path); _logger.LogInformation("Downloading xmltv listings from {Path}", info.Path);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(info.Path, cancellationToken).ConfigureAwait(false); 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); var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(false); await using (stream.ConfigureAwait(false))
{
return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(false);
}
} }
else else
{ {
await using var stream = AsyncFile.OpenRead(info.Path); var stream = AsyncFile.OpenRead(info.Path);
return await UnzipIfNeededAndCopy(info.Path, stream, cacheFile, cancellationToken).ConfigureAwait(false); 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) 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); 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)) await using (fileStream.ConfigureAwait(false))
{ {
try if (Path.GetExtension(originalUrl.AsSpan().LeftPart('?')).Equals(".gz", StringComparison.OrdinalIgnoreCase))
{ {
using var reader = new GZipStream(stream, CompressionMode.Decompress); try
await reader.CopyToAsync(fileStream, cancellationToken).ConfigureAwait(false); {
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) public async Task<IEnumerable<ProgramInfo>> GetProgramsAsync(ListingsProviderInfo info, string channelId, DateTime startDateUtc, DateTime endDateUtc, CancellationToken cancellationToken)

View File

@ -105,9 +105,9 @@ namespace Emby.Server.Implementations.LiveTv
/// <value>The services.</value> /// <value>The services.</value>
public IReadOnlyList<ILiveTvService> Services => _services; public IReadOnlyList<ILiveTvService> Services => _services;
public ITunerHost[] TunerHosts => _tunerHosts; public IReadOnlyList<ITunerHost> TunerHosts => _tunerHosts;
public IListingsProvider[] ListingProviders => _listingProviders; public IReadOnlyList<IListingsProvider> ListingProviders => _listingProviders;
private LiveTvOptions GetConfiguration() private LiveTvOptions GetConfiguration()
{ {
@ -1095,13 +1095,12 @@ namespace Emby.Server.Implementations.LiveTv
// Load these now which will prefetch metadata // Load these now which will prefetch metadata
var dtoOptions = new DtoOptions(); var dtoOptions = new DtoOptions();
var fields = dtoOptions.Fields.ToList(); var fields = dtoOptions.Fields.ToList();
fields.Remove(ItemFields.BasicSyncInfo);
dtoOptions.Fields = fields.ToArray(); dtoOptions.Fields = fields.ToArray();
progress.Report(100); 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); progress.Report(10);
@ -2168,7 +2167,7 @@ namespace Emby.Server.Implementations.LiveTv
public Folder GetInternalLiveTvFolder(CancellationToken cancellationToken) public Folder GetInternalLiveTvFolder(CancellationToken cancellationToken)
{ {
var name = _localization.GetLocalizedString("HeaderLiveTV"); var name = _localization.GetLocalizedString("HeaderLiveTV");
return _libraryManager.GetNamedView(name, CollectionType.LiveTv, name); return _libraryManager.GetNamedView(name, CollectionType.livetv, name);
} }
public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true) public async Task<TunerHostInfo> SaveTunerHost(TunerHostInfo info, bool dataSourceChanged = true)

View File

@ -67,7 +67,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return list; return list;
} }
protected virtual List<TunerHostInfo> GetTunerHosts() protected virtual IList<TunerHostInfo> GetTunerHosts()
{ {
return GetConfiguration().TunerHosts return GetConfiguration().TunerHosts
.Where(i => string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase)) .Where(i => string.Equals(i.Type, Type, StringComparison.OrdinalIgnoreCase))
@ -96,8 +96,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
try try
{ {
Directory.CreateDirectory(Path.GetDirectoryName(channelCacheFile)); Directory.CreateDirectory(Path.GetDirectoryName(channelCacheFile));
await using var writeStream = AsyncFile.OpenWrite(channelCacheFile); var writeStream = AsyncFile.OpenWrite(channelCacheFile);
await JsonSerializer.SerializeAsync(writeStream, channels, cancellationToken: cancellationToken).ConfigureAwait(false); await using (writeStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(writeStream, channels, cancellationToken: cancellationToken).ConfigureAwait(false);
}
} }
catch (IOException) catch (IOException)
{ {
@ -112,10 +115,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
{ {
try try
{ {
await using var readStream = AsyncFile.OpenRead(channelCacheFile); var readStream = AsyncFile.OpenRead(channelCacheFile);
var channels = await JsonSerializer.DeserializeAsync<List<ChannelInfo>>(readStream, cancellationToken: cancellationToken) await using (readStream.ConfigureAwait(false))
.ConfigureAwait(false); {
list.AddRange(channels); var channels = await JsonSerializer
.DeserializeAsync<List<ChannelInfo>>(readStream, cancellationToken: cancellationToken)
.ConfigureAwait(false);
list.AddRange(channels);
}
} }
catch (IOException) catch (IOException)
{ {
@ -159,9 +166,9 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return new List<MediaSourceInfo>(); 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); ArgumentException.ThrowIfNullOrEmpty(channelId);

View File

@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{ {
var model = ModelNumber ?? string.Empty; var model = ModelNumber ?? string.Empty;
if (model.IndexOf("hdtc", StringComparison.OrdinalIgnoreCase) != -1) if (model.Contains("hdtc", StringComparison.OrdinalIgnoreCase))
{ {
return true; return true;
} }

View File

@ -527,7 +527,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
return list; 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; var tunerCount = tunerHost.TunerCount;

View File

@ -112,6 +112,21 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return stream; 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) protected async Task DeleteTempFiles(string path, int retryCount = 0)
{ {
if (retryCount == 0) if (retryCount == 0)
@ -134,7 +149,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
} }
} }
private void TrySeek(Stream stream, long offset) private void TrySeek(FileStream stream, long offset)
{ {
if (!stream.CanSeek) if (!stream.CanSeek)
{ {

View File

@ -96,7 +96,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
return Task.FromResult(list); 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; var tunerCount = tunerHost.TunerCount;

View File

@ -66,7 +66,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
.ConfigureAwait(false); .ConfigureAwait(false);
response.EnsureSuccessStatusCode(); 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) private async Task<List<ChannelInfo>> GetChannelsAsync(TextReader reader, string channelIdPrefix, string tunerHostId)

View File

@ -83,14 +83,27 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath); Logger.LogInformation("Beginning {StreamType} stream to {FilePath}", GetType().Name, TempFilePath);
using (response) using (response)
{ {
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); 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 using (stream.ConfigureAwait(false))
await StreamHelper.CopyToAsync( {
stream, var fileStream = new FileStream(
fileStream, TempFilePath,
IODefaults.CopyToBufferSize, FileMode.Create,
() => Resolve(openTaskCompletionSource), FileAccess.Write,
cancellationToken).ConfigureAwait(false); 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) catch (OperationCanceledException ex)

View File

@ -0,0 +1 @@
{}

View File

@ -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.", "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", "TaskKeyframeExtractor": "Extractor de fotogrames clau",
"External": "Extern", "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."
} }

View File

@ -83,8 +83,8 @@
"UserDeletedWithName": "Uživatel {0} byl smazán", "UserDeletedWithName": "Uživatel {0} byl smazán",
"UserDownloadingItemWithValues": "{0} stahuje {1}", "UserDownloadingItemWithValues": "{0} stahuje {1}",
"UserLockedOutWithName": "Uživatel {0} byl odemčen", "UserLockedOutWithName": "Uživatel {0} byl odemčen",
"UserOfflineFromDevice": "{0} se odpojil od {1}", "UserOfflineFromDevice": "{0} se odpojil ze zařízení {1}",
"UserOnlineFromDevice": "{0} se připojil z {1}", "UserOnlineFromDevice": "{0} se připojil ze zařízení {1}",
"UserPasswordChangedWithName": "Provedena změna hesla pro uživatele {0}", "UserPasswordChangedWithName": "Provedena změna hesla pro uživatele {0}",
"UserPolicyUpdatedWithName": "Zásady uživatele pro {0} byly aktualizovány", "UserPolicyUpdatedWithName": "Zásady uživatele pro {0} byly aktualizovány",
"UserStartedPlayingItemWithValues": "{0} spustil přehrávání {1}", "UserStartedPlayingItemWithValues": "{0} spustil přehrávání {1}",

View File

@ -123,5 +123,7 @@
"External": "Externo", "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.", "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", "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"
} }

View File

@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "فریم های کلیدی را از فایل های ویدئویی استخراج می کند تا لیست های پخش HLS دقیق تری ایجاد کند. این کار ممکن است برای مدت طولانی اجرا شود.", "TaskKeyframeExtractorDescription": "فریم های کلیدی را از فایل های ویدئویی استخراج می کند تا لیست های پخش HLS دقیق تری ایجاد کند. این کار ممکن است برای مدت طولانی اجرا شود.",
"TaskKeyframeExtractor": "استخراج کننده فریم کلیدی", "TaskKeyframeExtractor": "استخراج کننده فریم کلیدی",
"External": "خارجی", "External": "خارجی",
"HearingImpaired": "مشکل شنوایی" "HearingImpaired": "مشکل شنوایی",
"TaskRefreshTrickplayImages": "تولید تصاویر Trickplay",
"TaskRefreshTrickplayImagesDescription": "تولید پیش‌نمایش های trickplay برای ویدیو های فعال شده در کتابخانه."
} }

View File

@ -124,5 +124,6 @@
"TaskKeyframeExtractor": "Tagabunot ng Keyframe", "TaskKeyframeExtractor": "Tagabunot ng Keyframe",
"TaskKeyframeExtractorDescription": "Nagbubunot ng keyframe mula sa mga bidyo upang makabuo ng mas tumpak na HLS playlist. Maaaring matagal ito gawin.", "TaskKeyframeExtractorDescription": "Nagbubunot ng keyframe mula sa mga bidyo upang makabuo ng mas tumpak na HLS playlist. Maaaring matagal ito gawin.",
"External": "External", "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."
} }

View File

@ -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}-ն միացված է"
}

View File

@ -123,5 +123,7 @@
"TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული დამატებების განახლებების გადმოწერა და დაყენება.", "TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული დამატებების განახლებების გადმოწერა და დაყენება.",
"TaskCleanTranscodeDescription": "ერთ დღეზე უფრო ძველი ტრანსკოდირების ფაილების წაშლა.", "TaskCleanTranscodeDescription": "ერთ დღეზე უფრო ძველი ტრანსკოდირების ფაილების წაშლა.",
"TaskDownloadMissingSubtitlesDescription": "მეტამონაცემებზე დაყრდნობით ინტერნეტში ნაკლული სუბტიტრების ძებნა.", "TaskDownloadMissingSubtitlesDescription": "მეტამონაცემებზე დაყრდნობით ინტერნეტში ნაკლული სუბტიტრების ძებნა.",
"TaskOptimizeDatabaseDescription": "ბაზს შეკუშვა და ადგილის გათავისუფლება. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს." "TaskOptimizeDatabaseDescription": "ბაზს შეკუშვა და ადგილის გათავისუფლება. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს.",
"TaskRefreshTrickplayImagesDescription": "ქმნის trickplay წინასწარ ხედებს ვიდეოებისთვის ჩართულ ბიბლიოთეკებში.",
"TaskRefreshTrickplayImages": "Trickplay სურათების გენერირება"
} }

View File

@ -123,5 +123,7 @@
"External": "Ārējais", "External": "Ārējais",
"HearingImpaired": "Ar dzirdes traucējumiem", "HearingImpaired": "Ar dzirdes traucējumiem",
"TaskKeyframeExtractor": "Atslēgkadru ekstraktors", "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."
} }

View File

@ -124,5 +124,7 @@
"TaskKeyframeExtractorDescription": "Trekker ut nøkkelbilder fra videofiler for å skape mere nøyaktige HLS-spillelister. Denne oppgaven kan ta lang tid.", "TaskKeyframeExtractorDescription": "Trekker ut nøkkelbilder fra videofiler for å skape mere nøyaktige HLS-spillelister. Denne oppgaven kan ta lang tid.",
"TaskKeyframeExtractor": "Nøkkelbilde-uttrekker", "TaskKeyframeExtractor": "Nøkkelbilde-uttrekker",
"External": "Ekstern", "External": "Ekstern",
"HearingImpaired": "Hørselshemmet" "HearingImpaired": "Hørselshemmet",
"TaskRefreshTrickplayImages": "Generer Trickplay bilder",
"TaskRefreshTrickplayImagesDescription": "Oppretter trickplay-forhåndsvisninger for videoer i aktiverte biblioteker."
} }

View File

@ -117,5 +117,6 @@
"TaskCleanActivityLog": "Slett aktivitetslogg", "TaskCleanActivityLog": "Slett aktivitetslogg",
"Undefined": "Udefinert", "Undefined": "Udefinert",
"Forced": "Tvungen", "Forced": "Tvungen",
"Default": "Standard" "Default": "Standard",
"External": "Ekstern"
} }

View File

@ -38,5 +38,18 @@
"HeaderFavoriteSongs": "ఇష్టమైన పాటలు", "HeaderFavoriteSongs": "ఇష్టమైన పాటలు",
"HeaderLiveTV": "ప్రత్యక్ష TV", "HeaderLiveTV": "ప్రత్యక్ష TV",
"HeaderNextUp": "తదుపరి", "HeaderNextUp": "తదుపరి",
"HeaderRecordingGroups": "రికార్డింగ్ గుంపులు" "HeaderRecordingGroups": "రికార్డింగ్ గుంపులు",
"MessageApplicationUpdated": "జెల్లీఫిన్ సర్వర్ అప్‌డేట్ చేయడం పూర్తి అయ్యింది",
"MessageApplicationUpdatedTo": "జెల్లీఫిన్ సర్వర్ {0} వెర్షన్ కి అప్‌డేట్ చెయ్యబడింది",
"MessageServerConfigurationUpdated": "సర్వర్ కన్ఫిగరేషన్ అప్డేట్ చేయబడింది",
"NewVersionIsAvailable": "జెల్లీఫిన్ సర్వర్ యొక్క కొత్త వెర్షన్ డౌన్‌లోడ్ చేసుకోవడానికి అందుబాటులో ఉంది.",
"NotificationOptionApplicationUpdateInstalled": "అప్లికేషన్ అప్‌డేట్ ఇన్‌స్టాల్ చేయబడింది",
"ItemAddedWithName": "{0} లైబ్రరీకి జోడించబడింది",
"ItemRemovedWithName": "లైబ్రరీ నుండి {0} తీసివేయబడింది",
"LabelIpAddressValue": "ఐపీ చిరునామా: {0}",
"LabelRunningTimeValue": "నడుస్తున్న సమయం: {0}",
"Latest": "తాజా",
"NameInstallFailed": "{0} ఇన్‌స్టాలేషన్ విఫలమైంది",
"NameSeasonUnknown": "భాగం తెలియదు",
"NotificationOptionApplicationUpdateAvailable": "అప్లికేషన్ అప్‌డేట్ అందుబాటులో ఉంది"
} }

View File

@ -89,7 +89,7 @@
"UserPolicyUpdatedWithName": "{0} için kullanıcı politikası güncellendi", "UserPolicyUpdatedWithName": "{0} için kullanıcı politikası güncellendi",
"UserStartedPlayingItemWithValues": "{0}, {2} cihazında {1} izliyor", "UserStartedPlayingItemWithValues": "{0}, {2} cihazında {1} izliyor",
"UserStoppedPlayingItemWithValues": "{0}, {2} cihazında {1} izlemeyi bitirdi", "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}", "ValueSpecialEpisodeName": "Özel - {0}",
"VersionNumber": "Sürüm {0}", "VersionNumber": "Sürüm {0}",
"TaskCleanCache": "Önbellek Dizinini Temizle", "TaskCleanCache": "Önbellek Dizinini Temizle",
@ -111,7 +111,7 @@
"TaskCleanLogs": "Günlük Dizinini Temizle", "TaskCleanLogs": "Günlük Dizinini Temizle",
"TaskRefreshLibraryDescription": "Medya kütüphanenize eklenen yeni dosyaları arar ve ortam bilgilerini yeniler.", "TaskRefreshLibraryDescription": "Medya kütüphanenize eklenen yeni dosyaları arar ve ortam bilgilerini yeniler.",
"TaskRefreshLibrary": "Medya Kütüphanesini Tara", "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", "TaskRefreshChapterImages": "Bölüm Resimlerini Çıkar",
"TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler.", "TaskCleanCacheDescription": "Sistem tarafından artık ihtiyaç duyulmayan önbellek dosyalarını siler.",
"TaskCleanActivityLog": "Etkinlik Günlüğünü Temizle", "TaskCleanActivityLog": "Etkinlik Günlüğünü Temizle",

View File

@ -18,16 +18,16 @@
"HeaderContinueWatching": "Продовжити перегляд", "HeaderContinueWatching": "Продовжити перегляд",
"HeaderAlbumArtists": "Виконавці альбому", "HeaderAlbumArtists": "Виконавці альбому",
"Genres": "Жанри", "Genres": "Жанри",
"Folders": "Каталоги", "Folders": "Теки",
"Favorites": "Обрані", "Favorites": "Обрані",
"DeviceOnlineWithName": "Пристрій {0} підключився", "DeviceOnlineWithName": "Пристрій {0} підключився",
"DeviceOfflineWithName": "Пристрій {0} відключився", "DeviceOfflineWithName": "Пристрій {0} відключився",
"Collections": "Добірки", "Collections": "Колекції",
"ChapterNameValue": "Розділ {0}", "ChapterNameValue": "Сцена {0}",
"Channels": "Канали", "Channels": "Канали",
"CameraImageUploadedFrom": "Нова фотографія завантажена з {0}", "CameraImageUploadedFrom": "Нову фотографію завантажено з {0}",
"Books": "Книги", "Books": "Книги",
"AuthenticationSucceededWithUserName": "{0} успішно автентифіковано", "AuthenticationSucceededWithUserName": "{0} успішно авторизовано",
"Artists": "Виконавці", "Artists": "Виконавці",
"Application": "Додаток", "Application": "Додаток",
"AppDeviceValues": "Додаток: {0}, Пристрій: {1}", "AppDeviceValues": "Додаток: {0}, Пристрій: {1}",
@ -83,7 +83,7 @@
"SubtitleDownloadFailureFromForItem": "Не вдалося завантажити субтитри з {0} для {1}", "SubtitleDownloadFailureFromForItem": "Не вдалося завантажити субтитри з {0} для {1}",
"StartupEmbyServerIsLoading": "Jellyfin Server завантажується. Будь ласка, спробуйте трішки пізніше.", "StartupEmbyServerIsLoading": "Jellyfin Server завантажується. Будь ласка, спробуйте трішки пізніше.",
"Songs": "Пісні", "Songs": "Пісні",
"Shows": "Шоу", "Shows": "Телепередачі",
"ServerNameNeedsToBeRestarted": "{0} потрібно перезапустити", "ServerNameNeedsToBeRestarted": "{0} потрібно перезапустити",
"ScheduledTaskStartedWithName": "{0} розпочато", "ScheduledTaskStartedWithName": "{0} розпочато",
"ScheduledTaskFailedWithName": "{0} незавершено, збій", "ScheduledTaskFailedWithName": "{0} незавершено, збій",

View File

@ -696,7 +696,7 @@
"TwoLetterISORegionName": "SI" "TwoLetterISORegionName": "SI"
}, },
{ {
"DisplayName": "Soomaaliya", "DisplayName": "Somalia",
"Name": "SO", "Name": "SO",
"ThreeLetterISORegionName": "SOM", "ThreeLetterISORegionName": "SOM",
"TwoLetterISORegionName": "SO" "TwoLetterISORegionName": "SO"

View File

@ -25,7 +25,7 @@ namespace Emby.Server.Implementations.Playlists
public override bool SupportsInheritedParentImages => false; public override bool SupportsInheritedParentImages => false;
[JsonIgnore] [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) protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
{ {

View File

@ -1670,7 +1670,6 @@ namespace Emby.Server.Implementations.Session
var fields = dtoOptions.Fields.ToList(); var fields = dtoOptions.Fields.ToList();
fields.Remove(ItemFields.BasicSyncInfo);
fields.Remove(ItemFields.CanDelete); fields.Remove(ItemFields.CanDelete);
fields.Remove(ItemFields.CanDownload); fields.Remove(ItemFields.CanDownload);
fields.Remove(ItemFields.ChildCount); fields.Remove(ItemFields.ChildCount);

View File

@ -6,6 +6,7 @@ using Jellyfin.Api.Attributes;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;

View File

@ -42,16 +42,15 @@ public class DevicesController : BaseJellyfinApiController
/// <summary> /// <summary>
/// Get Devices. /// Get Devices.
/// </summary> /// </summary>
/// <param name="supportsSync">Gets or sets a value indicating whether [supports synchronize].</param>
/// <param name="userId">Gets or sets the user identifier.</param> /// <param name="userId">Gets or sets the user identifier.</param>
/// <response code="200">Devices retrieved.</response> /// <response code="200">Devices retrieved.</response>
/// <returns>An <see cref="OkResult"/> containing the list of devices.</returns> /// <returns>An <see cref="OkResult"/> containing the list of devices.</returns>
[HttpGet] [HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)] [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); userId = RequestHelpers.GetUserId(User, userId);
return await _deviceManager.GetDevicesForUser(userId, supportsSync).ConfigureAwait(false); return await _deviceManager.GetDevicesForUser(userId).ConfigureAwait(false);
} }
/// <summary> /// <summary>

View File

@ -9,8 +9,8 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes; using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.Models.PlaybackDtos;
using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Api.Models.StreamingDtos;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
@ -19,6 +19,7 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.MediaEncoding.Encoder; using MediaBrowser.MediaEncoding.Encoder;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
@ -51,7 +52,7 @@ public class DynamicHlsController : BaseJellyfinApiController
private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly TranscodingJobHelper _transcodingJobHelper; private readonly ITranscodeManager _transcodeManager;
private readonly ILogger<DynamicHlsController> _logger; private readonly ILogger<DynamicHlsController> _logger;
private readonly EncodingHelper _encodingHelper; private readonly EncodingHelper _encodingHelper;
private readonly IDynamicHlsPlaylistGenerator _dynamicHlsPlaylistGenerator; 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="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> 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="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="logger">Instance of the <see cref="ILogger{DynamicHlsController}"/> interface.</param>
/// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param> /// <param name="dynamicHlsHelper">Instance of <see cref="DynamicHlsHelper"/>.</param>
/// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
@ -79,7 +80,7 @@ public class DynamicHlsController : BaseJellyfinApiController
IServerConfigurationManager serverConfigurationManager, IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
IFileSystem fileSystem, IFileSystem fileSystem,
TranscodingJobHelper transcodingJobHelper, ITranscodeManager transcodeManager,
ILogger<DynamicHlsController> logger, ILogger<DynamicHlsController> logger,
DynamicHlsHelper dynamicHlsHelper, DynamicHlsHelper dynamicHlsHelper,
EncodingHelper encodingHelper, EncodingHelper encodingHelper,
@ -91,7 +92,7 @@ public class DynamicHlsController : BaseJellyfinApiController
_serverConfigurationManager = serverConfigurationManager; _serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_transcodingJobHelper = transcodingJobHelper; _transcodeManager = transcodeManager;
_logger = logger; _logger = logger;
_dynamicHlsHelper = dynamicHlsHelper; _dynamicHlsHelper = dynamicHlsHelper;
_encodingHelper = encodingHelper; _encodingHelper = encodingHelper;
@ -283,17 +284,17 @@ public class DynamicHlsController : BaseJellyfinApiController
_serverConfigurationManager, _serverConfigurationManager,
_mediaEncoder, _mediaEncoder,
_encodingHelper, _encodingHelper,
_transcodingJobHelper, _transcodeManager,
TranscodingJobType, TranscodingJobType,
cancellationToken) cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
TranscodingJobDto? job = null; TranscodingJob? job = null;
var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8"); var playlistPath = Path.ChangeExtension(state.OutputFilePath, ".m3u8");
if (!System.IO.File.Exists(playlistPath)) if (!System.IO.File.Exists(playlistPath))
{ {
var transcodingLock = _transcodingJobHelper.GetTranscodingLock(playlistPath); var transcodingLock = _transcodeManager.GetTranscodingLock(playlistPath);
await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false); await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try try
{ {
@ -302,11 +303,11 @@ public class DynamicHlsController : BaseJellyfinApiController
// If the playlist doesn't already exist, startup ffmpeg // If the playlist doesn't already exist, startup ffmpeg
try try
{ {
job = await _transcodingJobHelper.StartFfMpeg( job = await _transcodeManager.StartFfMpeg(
state, state,
playlistPath, playlistPath,
GetCommandLineArguments(playlistPath, state, true, 0), GetCommandLineArguments(playlistPath, state, true, 0),
Request, Request.HttpContext.User.GetUserId(),
TranscodingJobType, TranscodingJobType,
cancellationTokenSource) cancellationTokenSource)
.ConfigureAwait(false); .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) if (job is not null)
{ {
_transcodingJobHelper.OnTranscodeEndRequest(job); _transcodeManager.OnTranscodeEndRequest(job);
} }
var playlistText = HlsHelpers.GetLivePlaylistText(playlistPath, state); var playlistText = HlsHelpers.GetLivePlaylistText(playlistPath, state);
@ -1383,7 +1384,7 @@ public class DynamicHlsController : BaseJellyfinApiController
_serverConfigurationManager, _serverConfigurationManager,
_mediaEncoder, _mediaEncoder,
_encodingHelper, _encodingHelper,
_transcodingJobHelper, _transcodeManager,
TranscodingJobType, TranscodingJobType,
cancellationTokenSource.Token) cancellationTokenSource.Token)
.ConfigureAwait(false); .ConfigureAwait(false);
@ -1421,7 +1422,7 @@ public class DynamicHlsController : BaseJellyfinApiController
_serverConfigurationManager, _serverConfigurationManager,
_mediaEncoder, _mediaEncoder,
_encodingHelper, _encodingHelper,
_transcodingJobHelper, _transcodeManager,
TranscodingJobType, TranscodingJobType,
cancellationToken) cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
@ -1432,16 +1433,16 @@ public class DynamicHlsController : BaseJellyfinApiController
var segmentExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer); var segmentExtension = EncodingHelper.GetSegmentFileExtension(state.Request.SegmentContainer);
TranscodingJobDto? job; TranscodingJob? job;
if (System.IO.File.Exists(segmentPath)) 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); _logger.LogDebug("returning {0} [it exists, try 1]", segmentPath);
return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false); 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); await transcodingLock.WaitAsync(cancellationToken).ConfigureAwait(false);
var released = false; var released = false;
var startTranscoding = false; var startTranscoding = false;
@ -1450,7 +1451,7 @@ public class DynamicHlsController : BaseJellyfinApiController
{ {
if (System.IO.File.Exists(segmentPath)) if (System.IO.File.Exists(segmentPath))
{ {
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
transcodingLock.Release(); transcodingLock.Release();
released = true; released = true;
_logger.LogDebug("returning {0} [it exists, try 2]", segmentPath); _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 // If the playlist doesn't already exist, startup ffmpeg
try try
{ {
await _transcodingJobHelper.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false) await _transcodeManager.KillTranscodingJobs(streamingRequest.DeviceId, streamingRequest.PlaySessionId, p => false)
.ConfigureAwait(false); .ConfigureAwait(false);
if (currentTranscodingIndex.HasValue) if (currentTranscodingIndex.HasValue)
@ -1499,11 +1500,11 @@ public class DynamicHlsController : BaseJellyfinApiController
streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks; streamingRequest.StartTimeTicks = streamingRequest.CurrentRuntimeTicks;
state.WaitForPath = segmentPath; state.WaitForPath = segmentPath;
job = await _transcodingJobHelper.StartFfMpeg( job = await _transcodeManager.StartFfMpeg(
state, state,
playlistPath, playlistPath,
GetCommandLineArguments(playlistPath, state, false, segmentId), GetCommandLineArguments(playlistPath, state, false, segmentId),
Request, Request.HttpContext.User.GetUserId(),
TranscodingJobType, TranscodingJobType,
cancellationTokenSource).ConfigureAwait(false); cancellationTokenSource).ConfigureAwait(false);
} }
@ -1517,7 +1518,7 @@ public class DynamicHlsController : BaseJellyfinApiController
} }
else else
{ {
job = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType); job = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType);
if (job?.TranscodingThrottler is not null) if (job?.TranscodingThrottler is not null)
{ {
await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false); await job.TranscodingThrottler.UnpauseTranscoding().ConfigureAwait(false);
@ -1534,7 +1535,7 @@ public class DynamicHlsController : BaseJellyfinApiController
} }
_logger.LogDebug("returning {0} [general case]", segmentPath); _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); return await GetSegmentResult(state, playlistPath, segmentPath, segmentExtension, segmentId, job, cancellationToken).ConfigureAwait(false);
} }
@ -1922,7 +1923,7 @@ public class DynamicHlsController : BaseJellyfinApiController
string segmentPath, string segmentPath,
string segmentExtension, string segmentExtension,
int segmentIndex, int segmentIndex,
TranscodingJobDto? transcodingJob, TranscodingJob? transcodingJob,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
var segmentExists = System.IO.File.Exists(segmentPath); var segmentExists = System.IO.File.Exists(segmentPath);
@ -1991,7 +1992,7 @@ public class DynamicHlsController : BaseJellyfinApiController
return GetSegmentResult(state, segmentPath, transcodingJob); 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; var segmentEndingPositionTicks = state.Request.CurrentRuntimeTicks + state.Request.ActualSegmentLengthTicks;
@ -2001,7 +2002,7 @@ public class DynamicHlsController : BaseJellyfinApiController
if (transcodingJob is not null) if (transcodingJob is not null)
{ {
transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks); transcodingJob.DownloadPositionTicks = Math.Max(transcodingJob.DownloadPositionTicks ?? segmentEndingPositionTicks, segmentEndingPositionTicks);
_transcodingJobHelper.OnTranscodeEndRequest(transcodingJob); _transcodeManager.OnTranscodeEndRequest(transcodingJob);
} }
return Task.CompletedTask; return Task.CompletedTask;
@ -2012,7 +2013,7 @@ public class DynamicHlsController : BaseJellyfinApiController
private int? GetCurrentTranscodingIndex(string playlist, string segmentExtension) 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) if (job is null || job.HasExited)
{ {

View File

@ -131,8 +131,8 @@ public class GenresController : BaseJellyfinApiController
QueryResult<(BaseItem, ItemCounts)> result; QueryResult<(BaseItem, ItemCounts)> result;
if (parentItem is ICollectionFolder parentCollectionFolder if (parentItem is ICollectionFolder parentCollectionFolder
&& (parentCollectionFolder.CollectionType == CollectionType.Music && (parentCollectionFolder.CollectionType == CollectionType.music
|| parentCollectionFolder.CollectionType == CollectionType.MusicVideos)) || parentCollectionFolder.CollectionType == CollectionType.musicvideos))
{ {
result = _libraryManager.GetMusicGenres(query); result = _libraryManager.GetMusicGenres(query);
} }

View File

@ -24,22 +24,22 @@ public class HlsSegmentController : BaseJellyfinApiController
{ {
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly TranscodingJobHelper _transcodingJobHelper; private readonly ITranscodeManager _transcodeManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="HlsSegmentController"/> class. /// Initializes a new instance of the <see cref="HlsSegmentController"/> class.
/// </summary> /// </summary>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> 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( public HlsSegmentController(
IFileSystem fileSystem, IFileSystem fileSystem,
IServerConfigurationManager serverConfigurationManager, IServerConfigurationManager serverConfigurationManager,
TranscodingJobHelper transcodingJobHelper) ITranscodeManager transcodeManager)
{ {
_fileSystem = fileSystem; _fileSystem = fileSystem;
_serverConfigurationManager = serverConfigurationManager; _serverConfigurationManager = serverConfigurationManager;
_transcodingJobHelper = transcodingJobHelper; _transcodeManager = transcodeManager;
} }
/// <summary> /// <summary>
@ -112,7 +112,7 @@ public class HlsSegmentController : BaseJellyfinApiController
[FromQuery, Required] string deviceId, [FromQuery, Required] string deviceId,
[FromQuery, Required] string playSessionId) [FromQuery, Required] string playSessionId)
{ {
_transcodingJobHelper.KillTranscodingJobs(deviceId, playSessionId, path => true); _transcodeManager.KillTranscodingJobs(deviceId, playSessionId, _ => true);
return NoContent(); return NoContent();
} }
@ -174,13 +174,13 @@ public class HlsSegmentController : BaseJellyfinApiController
private ActionResult GetFileResult(string path, string playlistPath) private ActionResult GetFileResult(string path, string playlistPath)
{ {
var transcodingJob = _transcodingJobHelper.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls); var transcodingJob = _transcodeManager.OnTranscodeBeginRequest(playlistPath, TranscodingJobType.Hls);
Response.OnCompleted(() => Response.OnCompleted(() =>
{ {
if (transcodingJob is not null) if (transcodingJob is not null)
{ {
_transcodingJobHelper.OnTranscodeEndRequest(transcodingJob); _transcodeManager.OnTranscodeEndRequest(transcodingJob);
} }
return Task.CompletedTask; return Task.CompletedTask;

View File

@ -171,7 +171,7 @@ public class ItemUpdateController : BaseJellyfinApiController
info.ContentTypeOptions = GetContentTypeOptions(true).ToArray(); info.ContentTypeOptions = GetContentTypeOptions(true).ToArray();
info.ContentType = configuredContentType; info.ContentType = configuredContentType;
if (inheritedContentType is null || inheritedContentType == CollectionType.TvShows) if (inheritedContentType is null || inheritedContentType == CollectionType.tvshows)
{ {
info.ContentTypeOptions = info.ContentTypeOptions info.ContentTypeOptions = info.ContentTypeOptions
.Where(i => string.IsNullOrWhiteSpace(i.Value) .Where(i => string.IsNullOrWhiteSpace(i.Value)

View File

@ -34,6 +34,7 @@ public class ItemsController : BaseJellyfinApiController
private readonly IDtoService _dtoService; private readonly IDtoService _dtoService;
private readonly ILogger<ItemsController> _logger; private readonly ILogger<ItemsController> _logger;
private readonly ISessionManager _sessionManager; private readonly ISessionManager _sessionManager;
private readonly IUserDataManager _userDataRepository;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ItemsController"/> class. /// Initializes a new instance of the <see cref="ItemsController"/> class.
@ -44,13 +45,15 @@ public class ItemsController : BaseJellyfinApiController
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param> /// <param name="dtoService">Instance of the <see cref="IDtoService"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger"/> 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="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
/// <param name="userDataRepository">Instance of the <see cref="IUserDataManager"/> interface.</param>
public ItemsController( public ItemsController(
IUserManager userManager, IUserManager userManager,
ILibraryManager libraryManager, ILibraryManager libraryManager,
ILocalizationManager localization, ILocalizationManager localization,
IDtoService dtoService, IDtoService dtoService,
ILogger<ItemsController> logger, ILogger<ItemsController> logger,
ISessionManager sessionManager) ISessionManager sessionManager,
IUserDataManager userDataRepository)
{ {
_userManager = userManager; _userManager = userManager;
_libraryManager = libraryManager; _libraryManager = libraryManager;
@ -58,6 +61,7 @@ public class ItemsController : BaseJellyfinApiController
_dtoService = dtoService; _dtoService = dtoService;
_logger = logger; _logger = logger;
_sessionManager = sessionManager; _sessionManager = sessionManager;
_userDataRepository = userDataRepository;
} }
/// <summary> /// <summary>
@ -275,7 +279,7 @@ public class ItemsController : BaseJellyfinApiController
collectionType = hasCollectionType.CollectionType; collectionType = hasCollectionType.CollectionType;
} }
if (collectionType == CollectionType.Playlists) if (collectionType == CollectionType.playlists)
{ {
recursive = true; recursive = true;
includeItemTypes = new[] { BaseItemKind.Playlist }; includeItemTypes = new[] { BaseItemKind.Playlist };
@ -881,4 +885,64 @@ public class ItemsController : BaseJellyfinApiController
itemsResult.TotalRecordCount, itemsResult.TotalRecordCount,
returnItems); 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);
}
} }

View File

@ -927,15 +927,15 @@ public class LibraryController : BaseJellyfinApiController
{ {
return contentType switch return contentType switch
{ {
CollectionType.BoxSets => new[] { "BoxSet" }, CollectionType.boxsets => new[] { "BoxSet" },
CollectionType.Playlists => new[] { "Playlist" }, CollectionType.playlists => new[] { "Playlist" },
CollectionType.Movies => new[] { "Movie" }, CollectionType.movies => new[] { "Movie" },
CollectionType.TvShows => new[] { "Series", "Season", "Episode" }, CollectionType.tvshows => new[] { "Series", "Season", "Episode" },
CollectionType.Books => new[] { "Book" }, CollectionType.books => new[] { "Book" },
CollectionType.Music => new[] { "MusicArtist", "MusicAlbum", "Audio", "MusicVideo" }, CollectionType.music => new[] { "MusicArtist", "MusicAlbum", "Audio", "MusicVideo" },
CollectionType.HomeVideos => new[] { "Video", "Photo" }, CollectionType.homevideos => new[] { "Video", "Photo" },
CollectionType.Photos => new[] { "Video", "Photo" }, CollectionType.photos => new[] { "Video", "Photo" },
CollectionType.MusicVideos => new[] { "MusicVideo" }, CollectionType.musicvideos => new[] { "MusicVideo" },
_ => new[] { "Series", "Season", "Episode", "Movie" } _ => new[] { "Series", "Season", "Episode", "Movie" }
}; };
} }

View File

@ -24,6 +24,8 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.LiveTv;
@ -47,7 +49,7 @@ public class LiveTvController : BaseJellyfinApiController
private readonly IDtoService _dtoService; private readonly IDtoService _dtoService;
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
private readonly IConfigurationManager _configurationManager; private readonly IConfigurationManager _configurationManager;
private readonly TranscodingJobHelper _transcodingJobHelper; private readonly ITranscodeManager _transcodeManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="LiveTvController"/> class. /// Initializes a new instance of the <see cref="LiveTvController"/> class.
@ -59,7 +61,7 @@ public class LiveTvController : BaseJellyfinApiController
/// <param name="dtoService">Instance of the <see cref="IDtoService"/> 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="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> 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( public LiveTvController(
ILiveTvManager liveTvManager, ILiveTvManager liveTvManager,
IUserManager userManager, IUserManager userManager,
@ -68,7 +70,7 @@ public class LiveTvController : BaseJellyfinApiController
IDtoService dtoService, IDtoService dtoService,
IMediaSourceManager mediaSourceManager, IMediaSourceManager mediaSourceManager,
IConfigurationManager configurationManager, IConfigurationManager configurationManager,
TranscodingJobHelper transcodingJobHelper) ITranscodeManager transcodeManager)
{ {
_liveTvManager = liveTvManager; _liveTvManager = liveTvManager;
_userManager = userManager; _userManager = userManager;
@ -77,7 +79,7 @@ public class LiveTvController : BaseJellyfinApiController
_dtoService = dtoService; _dtoService = dtoService;
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;
_configurationManager = configurationManager; _configurationManager = configurationManager;
_transcodingJobHelper = transcodingJobHelper; _transcodeManager = transcodeManager;
} }
/// <summary> /// <summary>
@ -1171,7 +1173,7 @@ public class LiveTvController : BaseJellyfinApiController
return NotFound(); return NotFound();
} }
var stream = new ProgressiveFileStream(path, null, _transcodingJobHelper); var stream = new ProgressiveFileStream(path, null, _transcodeManager);
return new FileStreamResult(stream, MimeTypes.GetMimeType(path)); return new FileStreamResult(stream, MimeTypes.GetMimeType(path));
} }

View File

@ -8,6 +8,7 @@ using Jellyfin.Api.ModelBinders;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Session; using MediaBrowser.Model.Session;
@ -30,7 +31,7 @@ public class PlaystateController : BaseJellyfinApiController
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly ISessionManager _sessionManager; private readonly ISessionManager _sessionManager;
private readonly ILogger<PlaystateController> _logger; private readonly ILogger<PlaystateController> _logger;
private readonly TranscodingJobHelper _transcodingJobHelper; private readonly ITranscodeManager _transcodeManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PlaystateController"/> class. /// 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="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> 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="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( public PlaystateController(
IUserManager userManager, IUserManager userManager,
IUserDataManager userDataRepository, IUserDataManager userDataRepository,
ILibraryManager libraryManager, ILibraryManager libraryManager,
ISessionManager sessionManager, ISessionManager sessionManager,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
TranscodingJobHelper transcodingJobHelper) ITranscodeManager transcodeManager)
{ {
_userManager = userManager; _userManager = userManager;
_userDataRepository = userDataRepository; _userDataRepository = userDataRepository;
@ -55,7 +56,7 @@ public class PlaystateController : BaseJellyfinApiController
_sessionManager = sessionManager; _sessionManager = sessionManager;
_logger = loggerFactory.CreateLogger<PlaystateController>(); _logger = loggerFactory.CreateLogger<PlaystateController>();
_transcodingJobHelper = transcodingJobHelper; _transcodeManager = transcodeManager;
} }
/// <summary> /// <summary>
@ -188,7 +189,7 @@ public class PlaystateController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult PingPlaybackSession([FromQuery, Required] string playSessionId) public ActionResult PingPlaybackSession([FromQuery, Required] string playSessionId)
{ {
_transcodingJobHelper.PingTranscodingJob(playSessionId, null); _transcodeManager.PingTranscodingJob(playSessionId, null);
return NoContent(); return NoContent();
} }
@ -205,7 +206,7 @@ public class PlaystateController : BaseJellyfinApiController
_logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty); _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId)) 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); 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); _logger.LogDebug("ReportPlaybackStopped PlaySessionId: {0}", playbackStopInfo.PlaySessionId ?? string.Empty);
if (!string.IsNullOrWhiteSpace(playbackStopInfo.PlaySessionId)) 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); playbackStopInfo.SessionId = await RequestHelpers.GetSessionId(_sessionManager, _userManager, HttpContext).ConfigureAwait(false);
@ -388,7 +389,7 @@ public class PlaystateController : BaseJellyfinApiController
{ {
if (method == PlayMethod.Transcode) 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) if (job is null)
{ {
return PlayMethod.DirectPlay; return PlayMethod.DirectPlay;

View File

@ -385,7 +385,6 @@ public class SessionController : BaseJellyfinApiController
/// <param name="playableMediaTypes">A list of playable media types, comma delimited. Audio, Video, Book, Photo.</param> /// <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="supportedCommands">A list of supported remote control commands, comma delimited.</param>
/// <param name="supportsMediaControl">Determines whether media can be played remotely..</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> /// <param name="supportsPersistentIdentifier">Determines whether the device supports a unique identifier.</param>
/// <response code="204">Capabilities posted.</response> /// <response code="204">Capabilities posted.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns> /// <returns>A <see cref="NoContentResult"/>.</returns>
@ -397,7 +396,6 @@ public class SessionController : BaseJellyfinApiController
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] playableMediaTypes, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] MediaType[] playableMediaTypes,
[FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands, [FromQuery, ModelBinder(typeof(CommaDelimitedArrayModelBinder))] GeneralCommandType[] supportedCommands,
[FromQuery] bool supportsMediaControl = false, [FromQuery] bool supportsMediaControl = false,
[FromQuery] bool supportsSync = false,
[FromQuery] bool supportsPersistentIdentifier = true) [FromQuery] bool supportsPersistentIdentifier = true)
{ {
if (string.IsNullOrWhiteSpace(id)) if (string.IsNullOrWhiteSpace(id))
@ -410,7 +408,6 @@ public class SessionController : BaseJellyfinApiController
PlayableMediaTypes = playableMediaTypes, PlayableMediaTypes = playableMediaTypes,
SupportedCommands = supportedCommands, SupportedCommands = supportedCommands,
SupportsMediaControl = supportsMediaControl, SupportsMediaControl = supportsMediaControl,
SupportsSync = supportsSync,
SupportsPersistentIdentifier = supportsPersistentIdentifier SupportsPersistentIdentifier = supportsPersistentIdentifier
}); });
return NoContent(); return NoContent();

View File

@ -11,6 +11,7 @@ using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;

View File

@ -90,7 +90,6 @@ public class UserViewsController : BaseJellyfinApiController
fields.Add(ItemFields.PrimaryImageAspectRatio); fields.Add(ItemFields.PrimaryImageAspectRatio);
fields.Add(ItemFields.DisplayPreferencesId); fields.Add(ItemFields.DisplayPreferencesId);
fields.Remove(ItemFields.BasicSyncInfo);
dtoOptions.Fields = fields.ToArray(); dtoOptions.Fields = fields.ToArray();
var user = _userManager.GetUserById(userId); var user = _userManager.GetUserById(userId);

View File

@ -11,7 +11,6 @@ using Jellyfin.Api.Constants;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Helpers; using Jellyfin.Api.Helpers;
using Jellyfin.Api.ModelBinders; using Jellyfin.Api.ModelBinders;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Api; using MediaBrowser.Common.Api;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
@ -20,6 +19,7 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@ -43,7 +43,7 @@ public class VideosController : BaseJellyfinApiController
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly TranscodingJobHelper _transcodingJobHelper; private readonly ITranscodeManager _transcodeManager;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly EncodingHelper _encodingHelper; private readonly EncodingHelper _encodingHelper;
@ -58,7 +58,7 @@ public class VideosController : BaseJellyfinApiController
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> 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="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="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
public VideosController( public VideosController(
@ -68,7 +68,7 @@ public class VideosController : BaseJellyfinApiController
IMediaSourceManager mediaSourceManager, IMediaSourceManager mediaSourceManager,
IServerConfigurationManager serverConfigurationManager, IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
TranscodingJobHelper transcodingJobHelper, ITranscodeManager transcodeManager,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
EncodingHelper encodingHelper) EncodingHelper encodingHelper)
{ {
@ -78,7 +78,7 @@ public class VideosController : BaseJellyfinApiController
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager; _serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_transcodingJobHelper = transcodingJobHelper; _transcodeManager = transcodeManager;
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_encodingHelper = encodingHelper; _encodingHelper = encodingHelper;
} }
@ -427,7 +427,7 @@ public class VideosController : BaseJellyfinApiController
_serverConfigurationManager, _serverConfigurationManager,
_mediaEncoder, _mediaEncoder,
_encodingHelper, _encodingHelper,
_transcodingJobHelper, _transcodeManager,
_transcodingJobType, _transcodingJobType,
cancellationTokenSource.Token) cancellationTokenSource.Token)
.ConfigureAwait(false); .ConfigureAwait(false);
@ -466,7 +466,7 @@ public class VideosController : BaseJellyfinApiController
if (state.MediaSource.IsInfiniteStream) if (state.MediaSource.IsInfiniteStream)
{ {
var liveStream = new ProgressiveFileStream(state.MediaPath, null, _transcodingJobHelper); var liveStream = new ProgressiveFileStream(state.MediaPath, null, _transcodeManager);
return File(liveStream, contentType); return File(liveStream, contentType);
} }
@ -482,7 +482,7 @@ public class VideosController : BaseJellyfinApiController
state, state,
isHeadRequest, isHeadRequest,
HttpContext, HttpContext,
_transcodingJobHelper, _transcodeManager,
ffmpegCommandLineArguments, ffmpegCommandLineArguments,
_transcodingJobType, _transcodingJobType,
cancellationTokenSource).ConfigureAwait(false); cancellationTokenSource).ConfigureAwait(false);

View File

@ -2,13 +2,13 @@
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
@ -26,7 +26,7 @@ public class AudioHelper
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly TranscodingJobHelper _transcodingJobHelper; private readonly ITranscodeManager _transcodeManager;
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly IHttpContextAccessor _httpContextAccessor; private readonly IHttpContextAccessor _httpContextAccessor;
private readonly EncodingHelper _encodingHelper; private readonly EncodingHelper _encodingHelper;
@ -39,7 +39,7 @@ public class AudioHelper
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> 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="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="httpClientFactory">Instance of the <see cref="IHttpClientFactory"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
/// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
@ -49,7 +49,7 @@ public class AudioHelper
IMediaSourceManager mediaSourceManager, IMediaSourceManager mediaSourceManager,
IServerConfigurationManager serverConfigurationManager, IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
TranscodingJobHelper transcodingJobHelper, ITranscodeManager transcodeManager,
IHttpClientFactory httpClientFactory, IHttpClientFactory httpClientFactory,
IHttpContextAccessor httpContextAccessor, IHttpContextAccessor httpContextAccessor,
EncodingHelper encodingHelper) EncodingHelper encodingHelper)
@ -59,7 +59,7 @@ public class AudioHelper
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager; _serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_transcodingJobHelper = transcodingJobHelper; _transcodeManager = transcodeManager;
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
_encodingHelper = encodingHelper; _encodingHelper = encodingHelper;
@ -94,7 +94,7 @@ public class AudioHelper
_serverConfigurationManager, _serverConfigurationManager,
_mediaEncoder, _mediaEncoder,
_encodingHelper, _encodingHelper,
_transcodingJobHelper, _transcodeManager,
transcodingJobType, transcodingJobType,
cancellationTokenSource.Token) cancellationTokenSource.Token)
.ConfigureAwait(false); .ConfigureAwait(false);
@ -133,7 +133,7 @@ public class AudioHelper
if (state.MediaSource.IsInfiniteStream) 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); return new FileStreamResult(stream, contentType);
} }
@ -149,7 +149,7 @@ public class AudioHelper
state, state,
isHeadRequest, isHeadRequest,
_httpContextAccessor.HttpContext, _httpContextAccessor.HttpContext,
_transcodingJobHelper, _transcodeManager,
ffmpegCommandLineArguments, ffmpegCommandLineArguments,
transcodingJobType, transcodingJobType,
cancellationTokenSource).ConfigureAwait(false); cancellationTokenSource).ConfigureAwait(false);

View File

@ -8,7 +8,6 @@ using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Models.StreamingDtos;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
@ -18,6 +17,7 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.Controller.Trickplay; using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@ -39,7 +39,7 @@ public class DynamicHlsHelper
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly TranscodingJobHelper _transcodingJobHelper; private readonly ITranscodeManager _transcodeManager;
private readonly INetworkManager _networkManager; private readonly INetworkManager _networkManager;
private readonly ILogger<DynamicHlsHelper> _logger; private readonly ILogger<DynamicHlsHelper> _logger;
private readonly IHttpContextAccessor _httpContextAccessor; private readonly IHttpContextAccessor _httpContextAccessor;
@ -54,7 +54,7 @@ public class DynamicHlsHelper
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param> /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> 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="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="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> 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> /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
@ -66,7 +66,7 @@ public class DynamicHlsHelper
IMediaSourceManager mediaSourceManager, IMediaSourceManager mediaSourceManager,
IServerConfigurationManager serverConfigurationManager, IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
TranscodingJobHelper transcodingJobHelper, ITranscodeManager transcodeManager,
INetworkManager networkManager, INetworkManager networkManager,
ILogger<DynamicHlsHelper> logger, ILogger<DynamicHlsHelper> logger,
IHttpContextAccessor httpContextAccessor, IHttpContextAccessor httpContextAccessor,
@ -78,7 +78,7 @@ public class DynamicHlsHelper
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;
_serverConfigurationManager = serverConfigurationManager; _serverConfigurationManager = serverConfigurationManager;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_transcodingJobHelper = transcodingJobHelper; _transcodeManager = transcodeManager;
_networkManager = networkManager; _networkManager = networkManager;
_logger = logger; _logger = logger;
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
@ -130,7 +130,7 @@ public class DynamicHlsHelper
_serverConfigurationManager, _serverConfigurationManager,
_mediaEncoder, _mediaEncoder,
_encodingHelper, _encodingHelper,
_transcodingJobHelper, _transcodeManager,
transcodingJobType, transcodingJobType,
cancellationTokenSource.Token) cancellationTokenSource.Token)
.ConfigureAwait(false); .ConfigureAwait(false);

View File

@ -4,9 +4,9 @@ using System.Net.Http;
using System.Net.Mime; using System.Net.Mime;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Models.PlaybackDtos; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
@ -65,7 +65,7 @@ public static class FileStreamResponseHelpers
/// <param name="state">The current <see cref="StreamState"/>.</param> /// <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="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="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="ffmpegCommandLineArguments">The command line arguments to start ffmpeg.</param>
/// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param> /// <param name="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
/// <param name="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param> /// <param name="cancellationTokenSource">The <see cref="CancellationTokenSource"/>.</param>
@ -74,7 +74,7 @@ public static class FileStreamResponseHelpers
StreamState state, StreamState state,
bool isHeadRequest, bool isHeadRequest,
HttpContext httpContext, HttpContext httpContext,
TranscodingJobHelper transcodingJobHelper, ITranscodeManager transcodeManager,
string ffmpegCommandLineArguments, string ffmpegCommandLineArguments,
TranscodingJobType transcodingJobType, TranscodingJobType transcodingJobType,
CancellationTokenSource cancellationTokenSource) CancellationTokenSource cancellationTokenSource)
@ -93,22 +93,28 @@ public static class FileStreamResponseHelpers
return new OkResult(); return new OkResult();
} }
var transcodingLock = transcodingJobHelper.GetTranscodingLock(outputPath); var transcodingLock = transcodeManager.GetTranscodingLock(outputPath);
await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false); await transcodingLock.WaitAsync(cancellationTokenSource.Token).ConfigureAwait(false);
try try
{ {
TranscodingJobDto? job; TranscodingJob? job;
if (!File.Exists(outputPath)) 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 else
{ {
job = transcodingJobHelper.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive); job = transcodeManager.OnTranscodeBeginRequest(outputPath, TranscodingJobType.Progressive);
state.Dispose(); state.Dispose();
} }
var stream = new ProgressiveFileStream(outputPath, job, transcodingJobHelper); var stream = new ProgressiveFileStream(outputPath, job, transcodeManager);
return new FileStreamResult(stream, contentType); return new FileStreamResult(stream, contentType);
} }
finally finally

View File

@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Api.Models.StreamingDtos;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;

View File

@ -6,7 +6,6 @@ using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Models.StreamingDtos;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
@ -14,6 +13,7 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Streaming;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; 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="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param> /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
/// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</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="transcodingJobType">The <see cref="TranscodingJobType"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
/// <returns>A <see cref="Task"/> containing the current <see cref="StreamState"/>.</returns> /// <returns>A <see cref="Task"/> containing the current <see cref="StreamState"/>.</returns>
@ -51,7 +51,7 @@ public static class StreamingHelpers
IServerConfigurationManager serverConfigurationManager, IServerConfigurationManager serverConfigurationManager,
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
EncodingHelper encodingHelper, EncodingHelper encodingHelper,
TranscodingJobHelper transcodingJobHelper, ITranscodeManager transcodeManager,
TranscodingJobType transcodingJobType, TranscodingJobType transcodingJobType,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
@ -74,7 +74,7 @@ public static class StreamingHelpers
streamingRequest.AudioCodec = encodingHelper.InferAudioCodec(url); streamingRequest.AudioCodec = encodingHelper.InferAudioCodec(url);
} }
var state = new StreamState(mediaSourceManager, transcodingJobType, transcodingJobHelper) var state = new StreamState(mediaSourceManager, transcodingJobType, transcodeManager)
{ {
Request = streamingRequest, Request = streamingRequest,
RequestedUrl = url, RequestedUrl = url,
@ -115,7 +115,7 @@ public static class StreamingHelpers
if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId)) if (string.IsNullOrWhiteSpace(streamingRequest.LiveStreamId))
{ {
var currentJob = !string.IsNullOrWhiteSpace(streamingRequest.PlaySessionId) var currentJob = !string.IsNullOrWhiteSpace(streamingRequest.PlaySessionId)
? transcodingJobHelper.GetTranscodingJob(streamingRequest.PlaySessionId) ? transcodeManager.GetTranscodingJob(streamingRequest.PlaySessionId)
: null; : null;
if (currentJob is not null) if (currentJob is not null)

View File

@ -41,6 +41,8 @@ public class IPBasedAccessValidationMiddleware
if (!networkManager.HasRemoteAccess(remoteIP)) if (!networkManager.HasRemoteAccess(remoteIP))
{ {
// No access from network, respond with 503 instead of 200.
httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
return; return;
} }

View File

@ -1,3 +1,4 @@
using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
@ -40,6 +41,8 @@ public class LanFilteringMiddleware
var host = httpContext.GetNormalizedRemoteIP(); var host = httpContext.GetNormalizedRemoteIP();
if (!networkManager.IsInLocalNetwork(host)) if (!networkManager.IsInLocalNetwork(host))
{ {
// No access from network, respond with 503 instead of 200.
httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
return; return;
} }

View File

@ -30,26 +30,11 @@ public class ClientCapabilitiesDto
/// </summary> /// </summary>
public bool SupportsMediaControl { get; set; } 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> /// <summary>
/// Gets or sets a value indicating whether session supports a persistent identifier. /// Gets or sets a value indicating whether session supports a persistent identifier.
/// </summary> /// </summary>
public bool SupportsPersistentIdentifier { get; set; } public bool SupportsPersistentIdentifier { get; set; }
/// <summary>
/// Gets or sets a value indicating whether session supports sync.
/// </summary>
public bool SupportsSync { get; set; }
/// <summary> /// <summary>
/// Gets or sets the device profile. /// Gets or sets the device profile.
/// </summary> /// </summary>
@ -76,10 +61,7 @@ public class ClientCapabilitiesDto
PlayableMediaTypes = PlayableMediaTypes, PlayableMediaTypes = PlayableMediaTypes,
SupportedCommands = SupportedCommands, SupportedCommands = SupportedCommands,
SupportsMediaControl = SupportsMediaControl, SupportsMediaControl = SupportsMediaControl,
SupportsContentUploading = SupportsContentUploading,
MessageCallbackUrl = MessageCallbackUrl,
SupportsPersistentIdentifier = SupportsPersistentIdentifier, SupportsPersistentIdentifier = SupportsPersistentIdentifier,
SupportsSync = SupportsSync,
DeviceProfile = DeviceProfile, DeviceProfile = DeviceProfile,
AppStoreUrl = AppStoreUrl, AppStoreUrl = AppStoreUrl,
IconUrl = IconUrl IconUrl = IconUrl

View File

@ -1,4 +1,6 @@
namespace Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Controller.Streaming;
namespace Jellyfin.Api.Models.StreamingDtos;
/// <summary> /// <summary>
/// The hls video request dto. /// The hls video request dto.

View File

@ -1,4 +1,6 @@
namespace Jellyfin.Api.Models.StreamingDtos; using MediaBrowser.Controller.Streaming;
namespace Jellyfin.Api.Models.StreamingDtos;
/// <summary> /// <summary>
/// The hls video request dto. /// The hls video request dto.

View File

@ -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,
}

View File

@ -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; using Jellyfin.Data.Attributes;
namespace Jellyfin.Data.Enums; namespace Jellyfin.Data.Enums;
@ -10,155 +11,155 @@ public enum CollectionType
/// <summary> /// <summary>
/// Unknown collection. /// Unknown collection.
/// </summary> /// </summary>
Unknown = 0, unknown = 0,
/// <summary> /// <summary>
/// Movies collection. /// Movies collection.
/// </summary> /// </summary>
Movies = 1, movies = 1,
/// <summary> /// <summary>
/// Tv shows collection. /// Tv shows collection.
/// </summary> /// </summary>
TvShows = 2, tvshows = 2,
/// <summary> /// <summary>
/// Music collection. /// Music collection.
/// </summary> /// </summary>
Music = 3, music = 3,
/// <summary> /// <summary>
/// Music videos collection. /// Music videos collection.
/// </summary> /// </summary>
MusicVideos = 4, musicvideos = 4,
/// <summary> /// <summary>
/// Trailers collection. /// Trailers collection.
/// </summary> /// </summary>
Trailers = 5, trailers = 5,
/// <summary> /// <summary>
/// Home videos collection. /// Home videos collection.
/// </summary> /// </summary>
HomeVideos = 6, homevideos = 6,
/// <summary> /// <summary>
/// Box sets collection. /// Box sets collection.
/// </summary> /// </summary>
BoxSets = 7, boxsets = 7,
/// <summary> /// <summary>
/// Books collection. /// Books collection.
/// </summary> /// </summary>
Books = 8, books = 8,
/// <summary> /// <summary>
/// Photos collection. /// Photos collection.
/// </summary> /// </summary>
Photos = 9, photos = 9,
/// <summary> /// <summary>
/// Live tv collection. /// Live tv collection.
/// </summary> /// </summary>
LiveTv = 10, livetv = 10,
/// <summary> /// <summary>
/// Playlists collection. /// Playlists collection.
/// </summary> /// </summary>
Playlists = 11, playlists = 11,
/// <summary> /// <summary>
/// Folders collection. /// Folders collection.
/// </summary> /// </summary>
Folders = 12, folders = 12,
/// <summary> /// <summary>
/// Tv show series collection. /// Tv show series collection.
/// </summary> /// </summary>
[OpenApiIgnoreEnum] [OpenApiIgnoreEnum]
TvShowSeries = 101, tvshowseries = 101,
/// <summary> /// <summary>
/// Tv genres collection. /// Tv genres collection.
/// </summary> /// </summary>
[OpenApiIgnoreEnum] [OpenApiIgnoreEnum]
TvGenres = 102, tvgenres = 102,
/// <summary> /// <summary>
/// Tv genre collection. /// Tv genre collection.
/// </summary> /// </summary>
[OpenApiIgnoreEnum] [OpenApiIgnoreEnum]
TvGenre = 103, tvgenre = 103,
/// <summary> /// <summary>
/// Tv latest collection. /// Tv latest collection.
/// </summary> /// </summary>
[OpenApiIgnoreEnum] [OpenApiIgnoreEnum]
TvLatest = 104, tvlatest = 104,
/// <summary> /// <summary>
/// Tv next up collection. /// Tv next up collection.
/// </summary> /// </summary>
[OpenApiIgnoreEnum] [OpenApiIgnoreEnum]
TvNextUp = 105, tvnextup = 105,
/// <summary> /// <summary>
/// Tv resume collection. /// Tv resume collection.
/// </summary> /// </summary>
[OpenApiIgnoreEnum] [OpenApiIgnoreEnum]
TvResume = 106, tvresume = 106,
/// <summary> /// <summary>
/// Tv favorite series collection. /// Tv favorite series collection.
/// </summary> /// </summary>
[OpenApiIgnoreEnum] [OpenApiIgnoreEnum]
TvFavoriteSeries = 107, tvfavoriteseries = 107,
/// <summary> /// <summary>
/// Tv favorite episodes collection. /// Tv favorite episodes collection.
/// </summary> /// </summary>
[OpenApiIgnoreEnum] [OpenApiIgnoreEnum]
TvFavoriteEpisodes = 108, tvfavoriteepisodes = 108,
/// <summary> /// <summary>
/// Latest movies collection. /// Latest movies collection.
/// </summary> /// </summary>
[OpenApiIgnoreEnum] [OpenApiIgnoreEnum]
MovieLatest = 109, movielatest = 109,
/// <summary> /// <summary>
/// Movies to resume collection. /// Movies to resume collection.
/// </summary> /// </summary>
[OpenApiIgnoreEnum] [OpenApiIgnoreEnum]
MovieResume = 110, movieresume = 110,
/// <summary> /// <summary>
/// Movie movie collection. /// Movie movie collection.
/// </summary> /// </summary>
[OpenApiIgnoreEnum] [OpenApiIgnoreEnum]
MovieMovies = 111, moviemovies = 111,
/// <summary> /// <summary>
/// Movie collections collection. /// Movie collections collection.
/// </summary> /// </summary>
[OpenApiIgnoreEnum] [OpenApiIgnoreEnum]
MovieCollections = 112, moviecollection = 112,
/// <summary> /// <summary>
/// Movie favorites collection. /// Movie favorites collection.
/// </summary> /// </summary>
[OpenApiIgnoreEnum] [OpenApiIgnoreEnum]
MovieFavorites = 113, moviefavorites = 113,
/// <summary> /// <summary>
/// Movie genres collection. /// Movie genres collection.
/// </summary> /// </summary>
[OpenApiIgnoreEnum] [OpenApiIgnoreEnum]
MovieGenres = 114, moviegenres = 114,
/// <summary> /// <summary>
/// Movie genre collection. /// Movie genre collection.
/// </summary> /// </summary>
[OpenApiIgnoreEnum] [OpenApiIgnoreEnum]
MovieGenre = 115 moviegenre = 115
} }

View File

@ -110,21 +110,21 @@ namespace Jellyfin.Server.Implementations.Devices
/// <inheritdoc /> /// <inheritdoc />
public async Task<DeviceInfo?> GetDevice(string id) public async Task<DeviceInfo?> GetDevice(string id)
{ {
Device? device;
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false)) await using (dbContext.ConfigureAwait(false))
{ {
device = await dbContext.Devices var device = await dbContext.Devices
.Where(d => d.DeviceId == id) .Where(d => d.DeviceId == id)
.OrderByDescending(d => d.DateLastActivity) .OrderByDescending(d => d.DateLastActivity)
.Include(d => d.User) .Include(d => d.User)
.SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o })
.FirstOrDefaultAsync() .FirstOrDefaultAsync()
.ConfigureAwait(false); .ConfigureAwait(false);
var deviceInfo = device is null ? null : ToDeviceInfo(device.Device, device.Options);
return deviceInfo;
} }
var deviceInfo = device is null ? null : ToDeviceInfo(device);
return deviceInfo;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -167,22 +167,18 @@ namespace Jellyfin.Server.Implementations.Devices
} }
/// <inheritdoc /> /// <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); var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false)) await using (dbContext.ConfigureAwait(false))
{ {
IAsyncEnumerable<Device> sessions = dbContext.Devices var sessions = dbContext.Devices
.Include(d => d.User) .Include(d => d.User)
.OrderByDescending(d => d.DateLastActivity) .OrderByDescending(d => d.DateLastActivity)
.ThenBy(d => d.DeviceId) .ThenBy(d => d.DeviceId)
.SelectMany(d => dbContext.DeviceOptions.Where(o => o.DeviceId == d.DeviceId).DefaultIfEmpty(), (d, o) => new { Device = d, Options = o })
.AsAsyncEnumerable(); .AsAsyncEnumerable();
if (supportsSync.HasValue)
{
sessions = sessions.Where(i => GetCapabilities(i.DeviceId).SupportsSync == supportsSync.Value);
}
if (userId.HasValue) if (userId.HasValue)
{ {
var user = _userManager.GetUserById(userId.Value); var user = _userManager.GetUserById(userId.Value);
@ -191,10 +187,10 @@ namespace Jellyfin.Server.Implementations.Devices
throw new ResourceNotFoundException(); 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); return new QueryResult<DeviceInfo>(array);
} }
@ -226,7 +222,7 @@ namespace Jellyfin.Server.Implementations.Devices
|| !GetCapabilities(deviceId).SupportsPersistentIdentifier; || !GetCapabilities(deviceId).SupportsPersistentIdentifier;
} }
private DeviceInfo ToDeviceInfo(Device authInfo) private DeviceInfo ToDeviceInfo(Device authInfo, DeviceOptions? options = null)
{ {
var caps = GetCapabilities(authInfo.DeviceId); var caps = GetCapabilities(authInfo.DeviceId);
@ -239,7 +235,8 @@ namespace Jellyfin.Server.Implementations.Devices
LastUserName = authInfo.User.Username, LastUserName = authInfo.User.Username,
Name = authInfo.DeviceName, Name = authInfo.DeviceName,
DateLastActivity = authInfo.DateLastActivity, DateLastActivity = authInfo.DateLastActivity,
IconUrl = caps.IconUrl IconUrl = caps.IconUrl,
CustomName = options?.CustomName,
}; };
} }
} }

View File

@ -1,7 +1,7 @@
#pragma warning disable CA1307 #pragma warning disable CA1307
#pragma warning disable CA1309 // Use ordinal string comparison - EF can't translate this
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
@ -46,8 +46,6 @@ namespace Jellyfin.Server.Implementations.Users
private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider; private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
private readonly IServerConfigurationManager _serverConfigurationManager; private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IDictionary<Guid, User> _users;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="UserManager"/> class. /// Initializes a new instance of the <see cref="UserManager"/> class.
/// </summary> /// </summary>
@ -58,6 +56,8 @@ namespace Jellyfin.Server.Implementations.Users
/// <param name="imageProcessor">The image processor.</param> /// <param name="imageProcessor">The image processor.</param>
/// <param name="logger">The logger.</param> /// <param name="logger">The logger.</param>
/// <param name="serverConfigurationManager">The system config manager.</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( public UserManager(
IDbContextFactory<JellyfinDbContext> dbProvider, IDbContextFactory<JellyfinDbContext> dbProvider,
IEventManager eventManager, IEventManager eventManager,
@ -65,7 +65,9 @@ namespace Jellyfin.Server.Implementations.Users
IApplicationHost appHost, IApplicationHost appHost,
IImageProcessor imageProcessor, IImageProcessor imageProcessor,
ILogger<UserManager> logger, ILogger<UserManager> logger,
IServerConfigurationManager serverConfigurationManager) IServerConfigurationManager serverConfigurationManager,
IEnumerable<IPasswordResetProvider> passwordResetProviders,
IEnumerable<IAuthenticationProvider> authenticationProviders)
{ {
_dbProvider = dbProvider; _dbProvider = dbProvider;
_eventManager = eventManager; _eventManager = eventManager;
@ -75,35 +77,36 @@ namespace Jellyfin.Server.Implementations.Users
_logger = logger; _logger = logger;
_serverConfigurationManager = serverConfigurationManager; _serverConfigurationManager = serverConfigurationManager;
_passwordResetProviders = appHost.GetExports<IPasswordResetProvider>(); _passwordResetProviders = passwordResetProviders.ToList();
_authenticationProviders = appHost.GetExports<IAuthenticationProvider>(); _authenticationProviders = authenticationProviders.ToList();
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First(); _invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First(); _defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
_defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().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/> /// <inheritdoc/>
public event EventHandler<GenericEventArgs<User>>? OnUserUpdated; public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;
/// <inheritdoc/> /// <inheritdoc/>
public IEnumerable<User> Users => _users.Values; public IEnumerable<User> Users
{
get
{
using var dbContext = _dbProvider.CreateDbContext();
return GetUsersInternal(dbContext).ToList();
}
}
/// <inheritdoc/> /// <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 @ // 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 // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
@ -119,8 +122,8 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Guid can't be empty", nameof(id)); throw new ArgumentException("Guid can't be empty", nameof(id));
} }
_users.TryGetValue(id, out var user); using var dbContext = _dbProvider.CreateDbContext();
return user; return GetUsersInternal(dbContext).FirstOrDefault(u => u.Id.Equals(id));
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -131,7 +134,9 @@ namespace Jellyfin.Server.Implementations.Users
throw new ArgumentException("Invalid username", nameof(name)); 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/> /// <inheritdoc/>
@ -196,8 +201,6 @@ namespace Jellyfin.Server.Implementations.Users
user.AddDefaultPermissions(); user.AddDefaultPermissions();
user.AddDefaultPreferences(); user.AddDefaultPreferences();
_users.Add(user.Id, user);
return user; return user;
} }
@ -232,40 +235,46 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc/> /// <inheritdoc/>
public async Task DeleteUserAsync(Guid userId) public async Task DeleteUserAsync(Guid userId)
{ {
if (!_users.TryGetValue(userId, out var user))
{
throw new ResourceNotFoundException(nameof(userId));
}
if (_users.Count == 1)
{
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));
}
if (user.HasPermission(PermissionKind.IsAdministrator)
&& Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 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); var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false)) await using (dbContext.ConfigureAwait(false))
{ {
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 (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 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));
}
dbContext.Users.Remove(user); dbContext.Users.Remove(user);
await dbContext.SaveChangesAsync().ConfigureAwait(false); await dbContext.SaveChangesAsync().ConfigureAwait(false);
await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
} }
_users.Remove(userId);
await _eventManager.PublishAsync(new UserDeletedEventArgs(user)).ConfigureAwait(false);
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -532,23 +541,23 @@ namespace Jellyfin.Server.Implementations.Users
/// <inheritdoc /> /// <inheritdoc />
public async Task InitializeAsync() public async Task InitializeAsync()
{ {
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
if (_users.Any())
{
return;
}
var defaultName = Environment.UserName;
if (string.IsNullOrWhiteSpace(defaultName) || !ValidUsernameRegex().IsMatch(defaultName))
{
defaultName = "MyJellyfinUser";
}
_logger.LogWarning("No users, creating one with username {UserName}", defaultName);
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false)) await using (dbContext.ConfigureAwait(false))
{ {
// 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";
}
_logger.LogWarning("No users, creating one with username {UserName}", defaultName);
var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false); var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
newUser.SetPermission(PermissionKind.IsAdministrator, true); newUser.SetPermission(PermissionKind.IsAdministrator, true);
newUser.SetPermission(PermissionKind.EnableContentDeletion, true); newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
@ -595,12 +604,9 @@ namespace Jellyfin.Server.Implementations.Users
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false)) await using (dbContext.ConfigureAwait(false))
{ {
var user = dbContext.Users var user = await GetUsersInternal(dbContext)
.Include(u => u.Permissions) .FirstOrDefaultAsync(u => u.Id.Equals(userId))
.Include(u => u.Preferences) .ConfigureAwait(false)
.Include(u => u.AccessSchedules)
.Include(u => u.ProfileImage)
.FirstOrDefault(u => u.Id.Equals(userId))
?? throw new ArgumentException("No user exists with given Id!"); ?? throw new ArgumentException("No user exists with given Id!");
user.SubtitleMode = config.SubtitleMode; user.SubtitleMode = config.SubtitleMode;
@ -628,7 +634,6 @@ namespace Jellyfin.Server.Implementations.Users
user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes); user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
dbContext.Update(user); dbContext.Update(user);
_users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false); await dbContext.SaveChangesAsync().ConfigureAwait(false);
} }
} }
@ -639,12 +644,9 @@ namespace Jellyfin.Server.Implementations.Users
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false); var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false)) await using (dbContext.ConfigureAwait(false))
{ {
var user = dbContext.Users var user = await GetUsersInternal(dbContext)
.Include(u => u.Permissions) .FirstOrDefaultAsync(u => u.Id.Equals(userId))
.Include(u => u.Preferences) .ConfigureAwait(false)
.Include(u => u.AccessSchedules)
.Include(u => u.ProfileImage)
.FirstOrDefault(u => u.Id.Equals(userId))
?? throw new ArgumentException("No user exists with given Id!"); ?? 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" // The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
@ -704,7 +706,6 @@ namespace Jellyfin.Server.Implementations.Users
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders); user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
dbContext.Update(user); dbContext.Update(user);
_users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false); await dbContext.SaveChangesAsync().ConfigureAwait(false);
} }
} }
@ -725,7 +726,6 @@ namespace Jellyfin.Server.Implementations.Users
} }
user.ProfileImage = null; user.ProfileImage = null;
_users[user.Id] = user;
} }
internal static void ThrowIfInvalidUsername(string name) internal static void ThrowIfInvalidUsername(string name)
@ -872,8 +872,15 @@ namespace Jellyfin.Server.Implementations.Users
private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user) private async Task UpdateUserInternalAsync(JellyfinDbContext dbContext, User user)
{ {
dbContext.Users.Update(user); dbContext.Users.Update(user);
_users[user.Id] = user;
await dbContext.SaveChangesAsync().ConfigureAwait(false); 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);
} }
} }

View File

@ -14,6 +14,7 @@ using Jellyfin.Server.Implementations.Security;
using Jellyfin.Server.Implementations.Trickplay; using Jellyfin.Server.Implementations.Trickplay;
using Jellyfin.Server.Implementations.Users; using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.BaseItemManager; using MediaBrowser.Controller.BaseItemManager;
using MediaBrowser.Controller.Devices; using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
@ -78,6 +79,9 @@ namespace Jellyfin.Server
serviceCollection.AddSingleton<IActivityManager, ActivityManager>(); serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
serviceCollection.AddSingleton<IUserManager, UserManager>(); serviceCollection.AddSingleton<IUserManager, UserManager>();
serviceCollection.AddSingleton<IAuthenticationProvider, DefaultAuthenticationProvider>();
serviceCollection.AddSingleton<IAuthenticationProvider, InvalidAuthProvider>();
serviceCollection.AddSingleton<IPasswordResetProvider, DefaultPasswordResetProvider>();
serviceCollection.AddScoped<IDisplayPreferencesManager, DisplayPreferencesManager>(); serviceCollection.AddScoped<IDisplayPreferencesManager, DisplayPreferencesManager>();
serviceCollection.AddSingleton<IDeviceManager, DeviceManager>(); serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
serviceCollection.AddSingleton<ITrickplayManager, TrickplayManager>(); serviceCollection.AddSingleton<ITrickplayManager, TrickplayManager>();

View File

@ -6,6 +6,7 @@ using System.Net.Mime;
using System.Text; using System.Text;
using Jellyfin.Api.Middleware; using Jellyfin.Api.Middleware;
using Jellyfin.MediaEncoding.Hls.Extensions; using Jellyfin.MediaEncoding.Hls.Extensions;
using Jellyfin.Networking;
using Jellyfin.Networking.HappyEyeballs; using Jellyfin.Networking.HappyEyeballs;
using Jellyfin.Server.Extensions; using Jellyfin.Server.Extensions;
using Jellyfin.Server.HealthChecks; using Jellyfin.Server.HealthChecks;
@ -13,7 +14,6 @@ using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Extensions; using Jellyfin.Server.Implementations.Extensions;
using Jellyfin.Server.Infrastructure; using Jellyfin.Server.Infrastructure;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Extensions;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
@ -121,6 +121,8 @@ namespace Jellyfin.Server
.AddCheck<DbContextFactoryHealthCheck<JellyfinDbContext>>(nameof(JellyfinDbContext)); .AddCheck<DbContextFactoryHealthCheck<JellyfinDbContext>>(nameof(JellyfinDbContext));
services.AddHlsPlaylistGenerator(); services.AddHlsPlaylistGenerator();
services.AddHostedService<AutoDiscoveryHost>();
} }
/// <summary> /// <summary>

View File

@ -59,9 +59,8 @@ namespace MediaBrowser.Controller.Devices
/// Gets the devices. /// Gets the devices.
/// </summary> /// </summary>
/// <param name="userId">The user's id, or <c>null</c>.</param> /// <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&lt;DeviceInfo&gt;.</returns> /// <returns>IEnumerable&lt;DeviceInfo&gt;.</returns>
Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId, bool? supportsSync); Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId);
Task DeleteDevice(Device device); Task DeleteDevice(Device device);

View File

@ -724,7 +724,7 @@ namespace MediaBrowser.Controller.Entities
if (this is IHasCollectionType view) if (this is IHasCollectionType view)
{ {
if (view.CollectionType == CollectionType.LiveTv) if (view.CollectionType == CollectionType.livetv)
{ {
return true; return true;
} }
@ -773,8 +773,6 @@ namespace MediaBrowser.Controller.Entities
/// <value>The remote trailers.</value> /// <value>The remote trailers.</value>
public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; } public IReadOnlyList<MediaUrl> RemoteTrailers { get; set; }
public virtual bool SupportsExternalTransfer => false;
public virtual double GetDefaultPrimaryImageAspectRatio() public virtual double GetDefaultPrimaryImageAspectRatio()
{ {
return 0; return 0;

View File

@ -19,19 +19,19 @@ namespace MediaBrowser.Controller.Entities
{ {
private static readonly CollectionType?[] _viewTypesEligibleForGrouping = private static readonly CollectionType?[] _viewTypesEligibleForGrouping =
{ {
Jellyfin.Data.Enums.CollectionType.Movies, Jellyfin.Data.Enums.CollectionType.movies,
Jellyfin.Data.Enums.CollectionType.TvShows, Jellyfin.Data.Enums.CollectionType.tvshows,
null null
}; };
private static readonly CollectionType?[] _originalFolderViewTypes = private static readonly CollectionType?[] _originalFolderViewTypes =
{ {
Jellyfin.Data.Enums.CollectionType.Books, Jellyfin.Data.Enums.CollectionType.books,
Jellyfin.Data.Enums.CollectionType.MusicVideos, Jellyfin.Data.Enums.CollectionType.musicvideos,
Jellyfin.Data.Enums.CollectionType.HomeVideos, Jellyfin.Data.Enums.CollectionType.homevideos,
Jellyfin.Data.Enums.CollectionType.Photos, Jellyfin.Data.Enums.CollectionType.photos,
Jellyfin.Data.Enums.CollectionType.Music, Jellyfin.Data.Enums.CollectionType.music,
Jellyfin.Data.Enums.CollectionType.BoxSets Jellyfin.Data.Enums.CollectionType.boxsets
}; };
public static ITVSeriesManager TVSeriesManager { get; set; } public static ITVSeriesManager TVSeriesManager { get; set; }
@ -161,7 +161,7 @@ namespace MediaBrowser.Controller.Entities
return true; return true;
} }
return collectionFolder.CollectionType == Jellyfin.Data.Enums.CollectionType.Playlists; return collectionFolder.CollectionType == Jellyfin.Data.Enums.CollectionType.playlists;
} }
public static bool IsEligibleForGrouping(Folder folder) public static bool IsEligibleForGrouping(Folder folder)

View File

@ -58,58 +58,58 @@ namespace MediaBrowser.Controller.Entities
switch (viewType) switch (viewType)
{ {
case CollectionType.Folders: case CollectionType.folders:
return GetResult(_libraryManager.GetUserRootFolder().GetChildren(user, true), query); return GetResult(_libraryManager.GetUserRootFolder().GetChildren(user, true), query);
case CollectionType.TvShows: case CollectionType.tvshows:
return GetTvView(queryParent, user, query); return GetTvView(queryParent, user, query);
case CollectionType.Movies: case CollectionType.movies:
return GetMovieFolders(queryParent, user, query); return GetMovieFolders(queryParent, user, query);
case CollectionType.TvShowSeries: case CollectionType.tvshowseries:
return GetTvSeries(queryParent, user, query); return GetTvSeries(queryParent, user, query);
case CollectionType.TvGenres: case CollectionType.tvgenres:
return GetTvGenres(queryParent, user, query); return GetTvGenres(queryParent, user, query);
case CollectionType.TvGenre: case CollectionType.tvgenre:
return GetTvGenreItems(queryParent, displayParent, user, query); return GetTvGenreItems(queryParent, displayParent, user, query);
case CollectionType.TvResume: case CollectionType.tvresume:
return GetTvResume(queryParent, user, query); return GetTvResume(queryParent, user, query);
case CollectionType.TvNextUp: case CollectionType.tvnextup:
return GetTvNextUp(queryParent, query); return GetTvNextUp(queryParent, query);
case CollectionType.TvLatest: case CollectionType.tvlatest:
return GetTvLatest(queryParent, user, query); return GetTvLatest(queryParent, user, query);
case CollectionType.MovieFavorites: case CollectionType.moviefavorites:
return GetFavoriteMovies(queryParent, user, query); return GetFavoriteMovies(queryParent, user, query);
case CollectionType.MovieLatest: case CollectionType.movielatest:
return GetMovieLatest(queryParent, user, query); return GetMovieLatest(queryParent, user, query);
case CollectionType.MovieGenres: case CollectionType.moviegenres:
return GetMovieGenres(queryParent, user, query); return GetMovieGenres(queryParent, user, query);
case CollectionType.MovieGenre: case CollectionType.moviegenre:
return GetMovieGenreItems(queryParent, displayParent, user, query); return GetMovieGenreItems(queryParent, displayParent, user, query);
case CollectionType.MovieResume: case CollectionType.movieresume:
return GetMovieResume(queryParent, user, query); return GetMovieResume(queryParent, user, query);
case CollectionType.MovieMovies: case CollectionType.moviemovies:
return GetMovieMovies(queryParent, user, query); return GetMovieMovies(queryParent, user, query);
case CollectionType.MovieCollections: case CollectionType.moviecollection:
return GetMovieCollections(user, query); return GetMovieCollections(user, query);
case CollectionType.TvFavoriteEpisodes: case CollectionType.tvfavoriteepisodes:
return GetFavoriteEpisodes(queryParent, user, query); return GetFavoriteEpisodes(queryParent, user, query);
case CollectionType.TvFavoriteSeries: case CollectionType.tvfavoriteseries:
return GetFavoriteSeries(queryParent, user, query); return GetFavoriteSeries(queryParent, user, query);
default: default:
@ -146,12 +146,12 @@ namespace MediaBrowser.Controller.Entities
var list = new List<BaseItem> var list = new List<BaseItem>
{ {
GetUserView(CollectionType.MovieResume, "HeaderContinueWatching", "0", parent), GetUserView(CollectionType.movieresume, "HeaderContinueWatching", "0", parent),
GetUserView(CollectionType.MovieLatest, "Latest", "1", parent), GetUserView(CollectionType.movielatest, "Latest", "1", parent),
GetUserView(CollectionType.MovieMovies, "Movies", "2", parent), GetUserView(CollectionType.moviemovies, "Movies", "2", parent),
GetUserView(CollectionType.MovieCollections, "Collections", "3", parent), GetUserView(CollectionType.moviecollection, "Collections", "3", parent),
GetUserView(CollectionType.MovieFavorites, "Favorites", "4", parent), GetUserView(CollectionType.moviefavorites, "Favorites", "4", parent),
GetUserView(CollectionType.MovieGenres, "Genres", "5", parent) GetUserView(CollectionType.moviegenres, "Genres", "5", parent)
}; };
return GetResult(list, query); return GetResult(list, query);
@ -264,7 +264,7 @@ namespace MediaBrowser.Controller.Entities
} }
}) })
.Where(i => i is not null) .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); return GetResult(genres, query);
} }
@ -303,13 +303,13 @@ namespace MediaBrowser.Controller.Entities
var list = new List<BaseItem> var list = new List<BaseItem>
{ {
GetUserView(CollectionType.TvResume, "HeaderContinueWatching", "0", parent), GetUserView(CollectionType.tvresume, "HeaderContinueWatching", "0", parent),
GetUserView(CollectionType.TvNextUp, "HeaderNextUp", "1", parent), GetUserView(CollectionType.tvnextup, "HeaderNextUp", "1", parent),
GetUserView(CollectionType.TvLatest, "Latest", "2", parent), GetUserView(CollectionType.tvlatest, "Latest", "2", parent),
GetUserView(CollectionType.TvShowSeries, "Shows", "3", parent), GetUserView(CollectionType.tvshowseries, "Shows", "3", parent),
GetUserView(CollectionType.TvFavoriteSeries, "HeaderFavoriteShows", "4", parent), GetUserView(CollectionType.tvfavoriteseries, "HeaderFavoriteShows", "4", parent),
GetUserView(CollectionType.TvFavoriteEpisodes, "HeaderFavoriteEpisodes", "5", parent), GetUserView(CollectionType.tvfavoriteepisodes, "HeaderFavoriteEpisodes", "5", parent),
GetUserView(CollectionType.TvGenres, "Genres", "6", parent) GetUserView(CollectionType.tvgenres, "Genres", "6", parent)
}; };
return GetResult(list, query); return GetResult(list, query);
@ -330,7 +330,7 @@ namespace MediaBrowser.Controller.Entities
private QueryResult<BaseItem> GetTvNextUp(Folder parent, InternalItemsQuery query) 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( var result = _tvSeriesManager.GetNextUp(
new NextUpQuery new NextUpQuery
@ -392,7 +392,7 @@ namespace MediaBrowser.Controller.Entities
} }
}) })
.Where(i => i is not null) .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); return GetResult(genres, query);
} }

View File

@ -2,6 +2,7 @@
#pragma warning disable CA1711, CS1591 #pragma warning disable CA1711, CS1591
using System;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -9,7 +10,7 @@ using MediaBrowser.Model.Dto;
namespace MediaBrowser.Controller.Library namespace MediaBrowser.Controller.Library
{ {
public interface ILiveStream public interface ILiveStream : IDisposable
{ {
int ConsumerCount { get; set; } int ConsumerCount { get; set; }

View File

@ -35,6 +35,15 @@ namespace MediaBrowser.Controller.Library
void SaveUserData(User user, BaseItem item, UserItemData userData, UserDataSaveReason reason, CancellationToken cancellationToken); 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(User user, BaseItem item);
UserItemData GetUserData(Guid userId, BaseItem item); UserItemData GetUserData(Guid userId, BaseItem item);

View File

@ -36,7 +36,7 @@ namespace MediaBrowser.Controller.LiveTv
/// <value>The services.</value> /// <value>The services.</value>
IReadOnlyList<ILiveTvService> Services { get; } IReadOnlyList<ILiveTvService> Services { get; }
IListingsProvider[] ListingProviders { get; } IReadOnlyList<IListingsProvider> ListingProviders { get; }
/// <summary> /// <summary>
/// Gets the new timer defaults asynchronous. /// Gets the new timer defaults asynchronous.

View File

@ -50,7 +50,7 @@ namespace MediaBrowser.Controller.LiveTv
/// <param name="currentLiveStreams">The current live streams.</param> /// <param name="currentLiveStreams">The current live streams.</param>
/// <param name="cancellationToken">The cancellation token to cancel operation.</param> /// <param name="cancellationToken">The cancellation token to cancel operation.</param>
/// <returns>Live stream wrapped in a task.</returns> /// <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> /// <summary>
/// Gets the channel stream media sources. /// Gets the channel stream media sources.

View File

@ -1068,7 +1068,7 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
// hw transpose filters should be added manually. // hw transpose filters should be added manually.
args.Append(" -autorotate 0"); args.Append(" -noautorotate");
return args.ToString().Trim(); return args.ToString().Trim();
} }
@ -1159,7 +1159,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var isSwDecoder = string.IsNullOrEmpty(GetHardwareVideoDecoder(state, options)); var isSwDecoder = string.IsNullOrEmpty(GetHardwareVideoDecoder(state, options));
if (!isSwDecoder && _mediaEncoder.EncoderVersion >= new Version(4, 4)) if (!isSwDecoder && _mediaEncoder.EncoderVersion >= new Version(4, 4))
{ {
arg.Append(" -autoscale 0"); arg.Append(" -noautoscale");
} }
return arg.ToString(); return arg.ToString();
@ -3343,7 +3343,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// [0:s]scale=s=1280x720 // [0:s]scale=s=1280x720
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter); 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); return (mainFilters, subFilters, overlayFilters);
@ -3520,7 +3520,7 @@ namespace MediaBrowser.Controller.MediaEncoding
} }
subFilters.Add("hwupload=derive_device=cuda"); 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 else
@ -3529,7 +3529,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter); 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"); 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("hwmap=derive_device=d3d11va:reverse=1");
overlayFilters.Add("format=d3d11"); overlayFilters.Add("format=d3d11");
} }
@ -3729,7 +3729,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter); 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; : string.Empty;
var overlayQsvFilter = string.Format( var overlayQsvFilter = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"overlay_qsv=eof_action=endall:shortest=1:repeatlast=0{0}", "overlay_qsv=eof_action=pass:repeatlast=0{0}",
overlaySize); overlaySize);
overlayFilters.Add(overlayQsvFilter); overlayFilters.Add(overlayQsvFilter);
} }
@ -3975,7 +3975,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter); 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; : string.Empty;
var overlayQsvFilter = string.Format( var overlayQsvFilter = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"overlay_qsv=eof_action=endall:shortest=1:repeatlast=0{0}", "overlay_qsv=eof_action=pass:repeatlast=0{0}",
overlaySize); overlaySize);
overlayFilters.Add(overlayQsvFilter); overlayFilters.Add(overlayQsvFilter);
} }
@ -4191,7 +4191,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter); 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; : string.Empty;
var overlayVaapiFilter = string.Format( var overlayVaapiFilter = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
"overlay_vaapi=eof_action=endall:shortest=1:repeatlast=0{0}", "overlay_vaapi=eof_action=pass:repeatlast=0{0}",
overlaySize); overlaySize);
overlayFilters.Add(overlayVaapiFilter); overlayFilters.Add(overlayVaapiFilter);
} }
@ -4456,7 +4456,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter); subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0"); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
if (isVaapiEncoder) if (isVaapiEncoder)
{ {
@ -4616,7 +4616,7 @@ namespace MediaBrowser.Controller.MediaEncoding
subFilters.Add("hwupload=derive_device=vulkan"); subFilters.Add("hwupload=derive_device=vulkan");
subFilters.Add("format=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) if (isSwEncoder)
{ {
@ -4817,7 +4817,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{ {
var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH); var subSwScaleFilter = GetCustomSwScaleFilter(inW, inH, reqW, reqH, reqMaxW, reqMaxH);
subFilters.Add(subSwScaleFilter); subFilters.Add(subSwScaleFilter);
overlayFilters.Add("overlay=eof_action=pass:shortest=1:repeatlast=0"); overlayFilters.Add("overlay=eof_action=pass:repeatlast=0");
if (isVaapiEncoder) if (isVaapiEncoder)
{ {

View File

@ -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);
}

Some files were not shown because too many files have changed in this diff Show More