mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-08-06 22:27:07 +02:00
Merge branch 'jellyfin:master' into cast-with-localhost-server
This commit is contained in:
commit
c83432f9e6
@ -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"
|
||||||
]
|
]
|
||||||
|
28
.devcontainer/devcontainer.json
Normal file
28
.devcontainer/devcontainer.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
6
.github/workflows/ci-codeql-analysis.yml
vendored
6
.github/workflows/ci-codeql-analysis.yml
vendored
@ -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
|
||||||
|
8
.github/workflows/ci-openapi.yml
vendored
8
.github/workflows/ci-openapi.yml
vendored
@ -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
|
||||||
|
12
.github/workflows/ci-tests.yml
vendored
12
.github/workflows/ci-tests.yml
vendored
@ -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"
|
|
||||||
|
10
.github/workflows/project-automation.yml
vendored
10
.github/workflows/project-automation.yml
vendored
@ -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:
|
||||||
|
10
.vscode/extensions.json
vendored
10
.vscode/extensions.json
vendored
@ -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": [
|
||||||
|
|
||||||
]
|
]
|
||||||
|
@ -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)
|
||||||
|
@ -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>
|
||||||
|
@ -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>();
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
|
@ -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);
|
||||||
|
@ -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()
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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 ||
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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))
|
||||||
{
|
{
|
||||||
|
@ -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))
|
||||||
{
|
{
|
||||||
|
@ -23,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||||||
private CollectionType?[] _musicPlaylistCollectionTypes =
|
private CollectionType?[] _musicPlaylistCollectionTypes =
|
||||||
{
|
{
|
||||||
null,
|
null,
|
||||||
CollectionType.Music
|
CollectionType.music
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
@ -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()
|
||||||
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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())
|
||||||
{
|
{
|
||||||
|
@ -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
|
||||||
{
|
{
|
||||||
|
@ -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.
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
1
Emby.Server.Implementations/Localization/Core/ab.json
Normal file
1
Emby.Server.Implementations/Localization/Core/ab.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
@ -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."
|
||||||
}
|
}
|
||||||
|
@ -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}",
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -124,5 +124,7 @@
|
|||||||
"TaskKeyframeExtractorDescription": "فریم های کلیدی را از فایل های ویدئویی استخراج می کند تا لیست های پخش HLS دقیق تری ایجاد کند. این کار ممکن است برای مدت طولانی اجرا شود.",
|
"TaskKeyframeExtractorDescription": "فریم های کلیدی را از فایل های ویدئویی استخراج می کند تا لیست های پخش HLS دقیق تری ایجاد کند. این کار ممکن است برای مدت طولانی اجرا شود.",
|
||||||
"TaskKeyframeExtractor": "استخراج کننده فریم کلیدی",
|
"TaskKeyframeExtractor": "استخراج کننده فریم کلیدی",
|
||||||
"External": "خارجی",
|
"External": "خارجی",
|
||||||
"HearingImpaired": "مشکل شنوایی"
|
"HearingImpaired": "مشکل شنوایی",
|
||||||
|
"TaskRefreshTrickplayImages": "تولید تصاویر Trickplay",
|
||||||
|
"TaskRefreshTrickplayImagesDescription": "تولید پیشنمایش های trickplay برای ویدیو های فعال شده در کتابخانه."
|
||||||
}
|
}
|
||||||
|
@ -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."
|
||||||
}
|
}
|
||||||
|
39
Emby.Server.Implementations/Localization/Core/hy.json
Normal file
39
Emby.Server.Implementations/Localization/Core/hy.json
Normal 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}-ն միացված է"
|
||||||
|
}
|
@ -123,5 +123,7 @@
|
|||||||
"TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული დამატებების განახლებების გადმოწერა და დაყენება.",
|
"TaskUpdatePluginsDescription": "ავტომატურად განახლებადად მონიშნული დამატებების განახლებების გადმოწერა და დაყენება.",
|
||||||
"TaskCleanTranscodeDescription": "ერთ დღეზე უფრო ძველი ტრანსკოდირების ფაილების წაშლა.",
|
"TaskCleanTranscodeDescription": "ერთ დღეზე უფრო ძველი ტრანსკოდირების ფაილების წაშლა.",
|
||||||
"TaskDownloadMissingSubtitlesDescription": "მეტამონაცემებზე დაყრდნობით ინტერნეტში ნაკლული სუბტიტრების ძებნა.",
|
"TaskDownloadMissingSubtitlesDescription": "მეტამონაცემებზე დაყრდნობით ინტერნეტში ნაკლული სუბტიტრების ძებნა.",
|
||||||
"TaskOptimizeDatabaseDescription": "ბაზს შეკუშვა და ადგილის გათავისუფლება. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს."
|
"TaskOptimizeDatabaseDescription": "ბაზს შეკუშვა და ადგილის გათავისუფლება. ამ ამოცანის ბიბლიოთეკის სკანირების ან ნებისმიერი ცვლილების, რომელიც ბაზაში რამეს აკეთებს, გაშვებას შეუძლია ბაზის წარმადობა გაზარდოს.",
|
||||||
|
"TaskRefreshTrickplayImagesDescription": "ქმნის trickplay წინასწარ ხედებს ვიდეოებისთვის ჩართულ ბიბლიოთეკებში.",
|
||||||
|
"TaskRefreshTrickplayImages": "Trickplay სურათების გენერირება"
|
||||||
}
|
}
|
||||||
|
@ -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."
|
||||||
}
|
}
|
||||||
|
@ -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."
|
||||||
}
|
}
|
||||||
|
@ -117,5 +117,6 @@
|
|||||||
"TaskCleanActivityLog": "Slett aktivitetslogg",
|
"TaskCleanActivityLog": "Slett aktivitetslogg",
|
||||||
"Undefined": "Udefinert",
|
"Undefined": "Udefinert",
|
||||||
"Forced": "Tvungen",
|
"Forced": "Tvungen",
|
||||||
"Default": "Standard"
|
"Default": "Standard",
|
||||||
|
"External": "Ekstern"
|
||||||
}
|
}
|
||||||
|
@ -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": "అప్లికేషన్ అప్డేట్ అందుబాటులో ఉంది"
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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} незавершено, збій",
|
||||||
|
@ -696,7 +696,7 @@
|
|||||||
"TwoLetterISORegionName": "SI"
|
"TwoLetterISORegionName": "SI"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"DisplayName": "Soomaaliya",
|
"DisplayName": "Somalia",
|
||||||
"Name": "SO",
|
"Name": "SO",
|
||||||
"ThreeLetterISORegionName": "SOM",
|
"ThreeLetterISORegionName": "SOM",
|
||||||
"TwoLetterISORegionName": "SO"
|
"TwoLetterISORegionName": "SO"
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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>
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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)
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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" }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
22
Jellyfin.Data/Enums/AudioSpatialFormat.cs
Normal file
22
Jellyfin.Data/Enums/AudioSpatialFormat.cs
Normal 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,
|
||||||
|
}
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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>();
|
||||||
|
@ -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>
|
||||||
|
@ -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<DeviceInfo>.</returns>
|
/// <returns>IEnumerable<DeviceInfo>.</returns>
|
||||||
Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId, bool? supportsSync);
|
Task<QueryResult<DeviceInfo>> GetDevicesForUser(Guid? userId);
|
||||||
|
|
||||||
Task DeleteDevice(Device device);
|
Task DeleteDevice(Device device);
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
@ -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)
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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; }
|
||||||
|
|
||||||
|
@ -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);
|
||||||
|
@ -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.
|
||||||
|
@ -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.
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
104
MediaBrowser.Controller/MediaEncoding/ITranscodeManager.cs
Normal file
104
MediaBrowser.Controller/MediaEncoding/ITranscodeManager.cs
Normal 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
Loading…
Reference in New Issue
Block a user