mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-06-23 15:30:56 -04:00
Merge branch 'master' into flac-hls-fixes
# Conflicts: # Jellyfin.Api/Controllers/DynamicHlsController.cs
This commit is contained in:
commit
fd022ee685
@ -168,6 +168,7 @@ jobs:
|
|||||||
- job: CollectArtifacts
|
- job: CollectArtifacts
|
||||||
timeoutInMinutes: 20
|
timeoutInMinutes: 20
|
||||||
displayName: 'Collect Artifacts'
|
displayName: 'Collect Artifacts'
|
||||||
|
condition: succeededOrFailed()
|
||||||
continueOnError: true
|
continueOnError: true
|
||||||
dependsOn:
|
dependsOn:
|
||||||
- BuildPackage
|
- BuildPackage
|
||||||
|
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@ -20,18 +20,18 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
|
||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: '7.0.x'
|
dotnet-version: '7.0.x'
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
|
uses: github/codeql-action/init@04daf014b50eaf774287bf3f0f1869d4b4c4b913 # v2.21.7
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: +security-extended
|
queries: +security-extended
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
|
uses: github/codeql-action/autobuild@04daf014b50eaf774287bf3f0f1869d4b4c4b913 # v2.21.7
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@0ba4244466797eb048eb91a6cd43d5c03ca8bd05 # v2.21.2
|
uses: github/codeql-action/analyze@04daf014b50eaf774287bf3f0f1869d4b4c4b913 # v2.21.7
|
||||||
|
4
.github/workflows/commands.yml
vendored
4
.github/workflows/commands.yml
vendored
@ -24,7 +24,7 @@ jobs:
|
|||||||
reactions: '+1'
|
reactions: '+1'
|
||||||
|
|
||||||
- name: Checkout the latest code
|
- name: Checkout the latest code
|
||||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
@ -51,7 +51,7 @@ jobs:
|
|||||||
reactions: eyes
|
reactions: eyes
|
||||||
|
|
||||||
- name: Checkout the latest code
|
- name: Checkout the latest code
|
||||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
8
.github/workflows/openapi.yml
vendored
8
.github/workflows/openapi.yml
vendored
@ -14,7 +14,7 @@ jobs:
|
|||||||
permissions: read-all
|
permissions: read-all
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
@ -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@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||||
with:
|
with:
|
||||||
name: openapi-head
|
name: openapi-head
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
@ -39,7 +39,7 @@ jobs:
|
|||||||
permissions: read-all
|
permissions: read-all
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.sha }}
|
ref: ${{ github.event.pull_request.head.sha }}
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||||
@ -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@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3
|
||||||
with:
|
with:
|
||||||
name: openapi-base
|
name: openapi-base
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
|
82
.github/workflows/repo-bump-version.yaml
vendored
Normal file
82
.github/workflows/repo-bump-version.yaml
vendored
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
name: '🆙 Auto bump_version'
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types:
|
||||||
|
- published
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
TAG_BRANCH:
|
||||||
|
required: true
|
||||||
|
description: release-x.y.z
|
||||||
|
NEXT_VERSION:
|
||||||
|
required: true
|
||||||
|
description: x.y.z
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
auto_bump_version:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event_name == 'release' && !contains(github.event.release.tag_name, 'rc') }}
|
||||||
|
env:
|
||||||
|
TAG_BRANCH: ${{ github.event.release.target_commitish }}
|
||||||
|
steps:
|
||||||
|
- name: Wait for deploy checks to finish
|
||||||
|
uses: jitterbit/await-check-suites@292a541bb7618078395b2ce711a0d89cfb8a568a # v1
|
||||||
|
with:
|
||||||
|
ref: ${{ env.TAG_BRANCH }}
|
||||||
|
intervalSeconds: 60
|
||||||
|
timeoutSeconds: 3600
|
||||||
|
|
||||||
|
- name: Setup YQ
|
||||||
|
uses: chrisdickinson/setup-yq@latest
|
||||||
|
with:
|
||||||
|
yq-version: v4.9.8
|
||||||
|
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
|
with:
|
||||||
|
ref: ${{ env.TAG_BRANCH }}
|
||||||
|
|
||||||
|
- name: Setup EnvVars
|
||||||
|
run: |-
|
||||||
|
CURRENT_VERSION=$(yq e '.version' build.yaml)
|
||||||
|
CURRENT_MAJOR_MINOR=${CURRENT_VERSION%.*}
|
||||||
|
CURRENT_PATCH=${CURRENT_VERSION##*.}
|
||||||
|
echo "CURRENT_VERSION=${CURRENT_VERSION}" >> $GITHUB_ENV
|
||||||
|
echo "CURRENT_MAJOR_MINOR=${CURRENT_MAJOR_MINOR}" >> $GITHUB_ENV
|
||||||
|
echo "CURRENT_PATCH=${CURRENT_PATCH}" >> $GITHUB_ENV
|
||||||
|
echo "NEXT_VERSION=${CURRENT_MAJOR_MINOR}.$(($CURRENT_PATCH + 1))" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Run bump_version
|
||||||
|
run: ./bump_version ${{ env.NEXT_VERSION }}
|
||||||
|
|
||||||
|
- name: Commit Changes
|
||||||
|
run: |-
|
||||||
|
git config user.name "jellyfin-bot"
|
||||||
|
git config user.email "team@jellyfin.org"
|
||||||
|
git checkout ${{ env.TAG_BRANCH }}
|
||||||
|
git commit -am "Bump version to ${{ env.NEXT_VERSION }}"
|
||||||
|
git push origin ${{ env.TAG_BRANCH }}
|
||||||
|
|
||||||
|
manual_bump_version:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||||
|
env:
|
||||||
|
TAG_BRANCH: ${{ github.event.inputs.TAG_BRANCH }}
|
||||||
|
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
|
||||||
|
with:
|
||||||
|
ref: ${{ env.TAG_BRANCH }}
|
||||||
|
|
||||||
|
- name: Run bump_version
|
||||||
|
run: ./bump_version ${{ env.NEXT_VERSION }}
|
||||||
|
|
||||||
|
- name: Commit Changes
|
||||||
|
run: |-
|
||||||
|
git config user.name "jellyfin-bot"
|
||||||
|
git config user.email "team@jellyfin.org"
|
||||||
|
git checkout ${{ env.TAG_BRANCH }}
|
||||||
|
git commit -am "Bump version to ${{ env.NEXT_VERSION }}"
|
||||||
|
git push origin ${{ env.TAG_BRANCH }}
|
@ -166,6 +166,8 @@
|
|||||||
- [RealGreenDragon](https://github.com/RealGreenDragon)
|
- [RealGreenDragon](https://github.com/RealGreenDragon)
|
||||||
- [ipitio](https://github.com/ipitio)
|
- [ipitio](https://github.com/ipitio)
|
||||||
- [TheTyrius](https://github.com/TheTyrius)
|
- [TheTyrius](https://github.com/TheTyrius)
|
||||||
|
- [tallbl0nde](https://github.com/tallbl0nde)
|
||||||
|
- [sleepycatcoding](https://github.com/sleepycatcoding)
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
|
@ -23,14 +23,15 @@
|
|||||||
<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.0" />
|
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
|
||||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.9" />
|
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.11" />
|
||||||
<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="7.0.9" />
|
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.11" />
|
||||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.9" />
|
<PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.11" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.9" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.11" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.9" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.11" />
|
||||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.9" />
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.11" />
|
||||||
|
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.11" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="7.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
||||||
@ -39,14 +40,14 @@
|
|||||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="7.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.9" />
|
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.11" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.9" />
|
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.11" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
|
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" />
|
<PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" />
|
||||||
<PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" />
|
<PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" />
|
||||||
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.3" />
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.7.2" />
|
||||||
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
|
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
|
||||||
<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" />
|
||||||
@ -59,23 +60,21 @@
|
|||||||
<PackageVersion Include="prometheus-net" Version="8.0.1" />
|
<PackageVersion Include="prometheus-net" Version="8.0.1" />
|
||||||
<PackageVersion Include="Serilog.AspNetCore" Version="7.0.0" />
|
<PackageVersion Include="Serilog.AspNetCore" Version="7.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="7.0.0" />
|
<PackageVersion Include="Serilog.Settings.Configuration" Version="7.0.1" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
|
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
|
<PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
|
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||||
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.2" />
|
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.2" />
|
||||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||||
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
|
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
|
||||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.3" />
|
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" />
|
||||||
<PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" />
|
<PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" />
|
||||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.3" />
|
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.5" />
|
||||||
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.3" />
|
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="2.8.2.5" />
|
||||||
<PackageVersion Include="SkiaSharp" Version="2.88.3" />
|
<PackageVersion Include="SkiaSharp" Version="2.88.5" />
|
||||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||||
<PackageVersion Include="SQLitePCL.pretty.netstandard" Version="3.1.0" />
|
|
||||||
<PackageVersion Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.5" />
|
|
||||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" />
|
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" />
|
||||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.4.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" />
|
||||||
@ -88,6 +87,6 @@
|
|||||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.0" />
|
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.0" />
|
||||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
|
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
|
||||||
<PackageVersion Include="xunit" Version="2.4.2" />
|
<PackageVersion Include="xunit" Version="2.5.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@ -45,8 +43,8 @@ namespace Emby.Dlna.Didl
|
|||||||
private readonly DeviceProfile _profile;
|
private readonly DeviceProfile _profile;
|
||||||
private readonly IImageProcessor _imageProcessor;
|
private readonly IImageProcessor _imageProcessor;
|
||||||
private readonly string _serverAddress;
|
private readonly string _serverAddress;
|
||||||
private readonly string _accessToken;
|
private readonly string? _accessToken;
|
||||||
private readonly User _user;
|
private readonly User? _user;
|
||||||
private readonly IUserDataManager _userDataManager;
|
private readonly IUserDataManager _userDataManager;
|
||||||
private readonly ILocalizationManager _localization;
|
private readonly ILocalizationManager _localization;
|
||||||
private readonly IMediaSourceManager _mediaSourceManager;
|
private readonly IMediaSourceManager _mediaSourceManager;
|
||||||
@ -56,10 +54,10 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
public DidlBuilder(
|
public DidlBuilder(
|
||||||
DeviceProfile profile,
|
DeviceProfile profile,
|
||||||
User user,
|
User? user,
|
||||||
IImageProcessor imageProcessor,
|
IImageProcessor imageProcessor,
|
||||||
string serverAddress,
|
string serverAddress,
|
||||||
string accessToken,
|
string? accessToken,
|
||||||
IUserDataManager userDataManager,
|
IUserDataManager userDataManager,
|
||||||
ILocalizationManager localization,
|
ILocalizationManager localization,
|
||||||
IMediaSourceManager mediaSourceManager,
|
IMediaSourceManager mediaSourceManager,
|
||||||
@ -85,7 +83,7 @@ namespace Emby.Dlna.Didl
|
|||||||
return url + "&dlnaheaders=true";
|
return url + "&dlnaheaders=true";
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetItemDidl(BaseItem item, User user, BaseItem context, string deviceId, Filter filter, StreamInfo streamInfo)
|
public string GetItemDidl(BaseItem item, User? user, BaseItem? context, string deviceId, Filter filter, StreamInfo streamInfo)
|
||||||
{
|
{
|
||||||
var settings = new XmlWriterSettings
|
var settings = new XmlWriterSettings
|
||||||
{
|
{
|
||||||
@ -140,12 +138,12 @@ namespace Emby.Dlna.Didl
|
|||||||
public void WriteItemElement(
|
public void WriteItemElement(
|
||||||
XmlWriter writer,
|
XmlWriter writer,
|
||||||
BaseItem item,
|
BaseItem item,
|
||||||
User user,
|
User? user,
|
||||||
BaseItem context,
|
BaseItem? context,
|
||||||
StubType? contextStubType,
|
StubType? contextStubType,
|
||||||
string deviceId,
|
string deviceId,
|
||||||
Filter filter,
|
Filter filter,
|
||||||
StreamInfo streamInfo = null)
|
StreamInfo? streamInfo = null)
|
||||||
{
|
{
|
||||||
var clientId = GetClientId(item, null);
|
var clientId = GetClientId(item, null);
|
||||||
|
|
||||||
@ -190,7 +188,7 @@ namespace Emby.Dlna.Didl
|
|||||||
writer.WriteFullEndElement();
|
writer.WriteFullEndElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo streamInfo = null)
|
private void AddVideoResource(XmlWriter writer, BaseItem video, string deviceId, Filter filter, StreamInfo? streamInfo = null)
|
||||||
{
|
{
|
||||||
if (streamInfo is null)
|
if (streamInfo is null)
|
||||||
{
|
{
|
||||||
@ -203,7 +201,7 @@ namespace Emby.Dlna.Didl
|
|||||||
Profile = _profile,
|
Profile = _profile,
|
||||||
DeviceId = deviceId,
|
DeviceId = deviceId,
|
||||||
MaxBitrate = _profile.MaxStreamingBitrate
|
MaxBitrate = _profile.MaxStreamingBitrate
|
||||||
});
|
}) ?? throw new InvalidOperationException("No optimal video stream found");
|
||||||
}
|
}
|
||||||
|
|
||||||
var targetWidth = streamInfo.TargetWidth;
|
var targetWidth = streamInfo.TargetWidth;
|
||||||
@ -315,7 +313,7 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
var mediaSource = streamInfo.MediaSource;
|
var mediaSource = streamInfo.MediaSource;
|
||||||
|
|
||||||
if (mediaSource.RunTimeTicks.HasValue)
|
if (mediaSource?.RunTimeTicks.HasValue == true)
|
||||||
{
|
{
|
||||||
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
|
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
@ -410,7 +408,7 @@ namespace Emby.Dlna.Didl
|
|||||||
writer.WriteFullEndElement();
|
writer.WriteFullEndElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem context)
|
private string GetDisplayName(BaseItem item, StubType? itemStubType, BaseItem? context)
|
||||||
{
|
{
|
||||||
if (itemStubType.HasValue)
|
if (itemStubType.HasValue)
|
||||||
{
|
{
|
||||||
@ -452,7 +450,7 @@ namespace Emby.Dlna.Didl
|
|||||||
/// <param name="episode">The episode.</param>
|
/// <param name="episode">The episode.</param>
|
||||||
/// <param name="context">Current context.</param>
|
/// <param name="context">Current context.</param>
|
||||||
/// <returns>Formatted name of the episode.</returns>
|
/// <returns>Formatted name of the episode.</returns>
|
||||||
private string GetEpisodeDisplayName(Episode episode, BaseItem context)
|
private string GetEpisodeDisplayName(Episode episode, BaseItem? context)
|
||||||
{
|
{
|
||||||
string[] components;
|
string[] components;
|
||||||
|
|
||||||
@ -530,7 +528,7 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
private bool NotNullOrWhiteSpace(string s) => !string.IsNullOrWhiteSpace(s);
|
private bool NotNullOrWhiteSpace(string s) => !string.IsNullOrWhiteSpace(s);
|
||||||
|
|
||||||
private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo streamInfo = null)
|
private void AddAudioResource(XmlWriter writer, BaseItem audio, string deviceId, Filter filter, StreamInfo? streamInfo = null)
|
||||||
{
|
{
|
||||||
writer.WriteStartElement(string.Empty, "res", NsDidl);
|
writer.WriteStartElement(string.Empty, "res", NsDidl);
|
||||||
|
|
||||||
@ -544,14 +542,14 @@ namespace Emby.Dlna.Didl
|
|||||||
MediaSources = sources.ToArray(),
|
MediaSources = sources.ToArray(),
|
||||||
Profile = _profile,
|
Profile = _profile,
|
||||||
DeviceId = deviceId
|
DeviceId = deviceId
|
||||||
});
|
}) ?? throw new InvalidOperationException("No optimal audio stream found");
|
||||||
}
|
}
|
||||||
|
|
||||||
var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken));
|
var url = NormalizeDlnaMediaUrl(streamInfo.ToUrl(_serverAddress, _accessToken));
|
||||||
|
|
||||||
var mediaSource = streamInfo.MediaSource;
|
var mediaSource = streamInfo.MediaSource;
|
||||||
|
|
||||||
if (mediaSource.RunTimeTicks.HasValue)
|
if (mediaSource?.RunTimeTicks is not null)
|
||||||
{
|
{
|
||||||
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
|
writer.WriteAttributeString("duration", TimeSpan.FromTicks(mediaSource.RunTimeTicks.Value).ToString("c", CultureInfo.InvariantCulture));
|
||||||
}
|
}
|
||||||
@ -634,7 +632,7 @@ namespace Emby.Dlna.Didl
|
|||||||
// Samsung sometimes uses 1 as root
|
// Samsung sometimes uses 1 as root
|
||||||
|| string.Equals(id, "1", StringComparison.OrdinalIgnoreCase);
|
|| string.Equals(id, "1", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string requestedId = null)
|
public void WriteFolderElement(XmlWriter writer, BaseItem folder, StubType? stubType, BaseItem context, int childCount, Filter filter, string? requestedId = null)
|
||||||
{
|
{
|
||||||
writer.WriteStartElement(string.Empty, "container", NsDidl);
|
writer.WriteStartElement(string.Empty, "container", NsDidl);
|
||||||
|
|
||||||
@ -678,14 +676,14 @@ namespace Emby.Dlna.Didl
|
|||||||
writer.WriteFullEndElement();
|
writer.WriteFullEndElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddSamsungBookmarkInfo(BaseItem item, User user, XmlWriter writer, StreamInfo streamInfo)
|
private void AddSamsungBookmarkInfo(BaseItem item, User? user, XmlWriter writer, StreamInfo? streamInfo)
|
||||||
{
|
{
|
||||||
if (!item.SupportsPositionTicksResume || item is Folder)
|
if (!item.SupportsPositionTicksResume || item is Folder)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
XmlAttribute secAttribute = null;
|
XmlAttribute? secAttribute = null;
|
||||||
foreach (var attribute in _profile.XmlRootAttributes)
|
foreach (var attribute in _profile.XmlRootAttributes)
|
||||||
{
|
{
|
||||||
if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase))
|
||||||
@ -695,8 +693,8 @@ namespace Emby.Dlna.Didl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not a samsung device
|
// Not a samsung device or no user data
|
||||||
if (secAttribute is null)
|
if (secAttribute is null || user is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -717,7 +715,7 @@ namespace Emby.Dlna.Didl
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds fields used by both items and folders.
|
/// Adds fields used by both items and folders.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter)
|
private void AddCommonFields(BaseItem item, StubType? itemStubType, BaseItem? context, XmlWriter writer, Filter filter)
|
||||||
{
|
{
|
||||||
// Don't filter on dc:title because not all devices will include it in the filter
|
// Don't filter on dc:title because not all devices will include it in the filter
|
||||||
// MediaMonkey for example won't display content without a title
|
// MediaMonkey for example won't display content without a title
|
||||||
@ -795,7 +793,7 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
if (item.IsDisplayedAsFolder || stubType.HasValue)
|
if (item.IsDisplayedAsFolder || stubType.HasValue)
|
||||||
{
|
{
|
||||||
string classType = null;
|
string? classType = null;
|
||||||
|
|
||||||
if (!_profile.RequiresPlainFolders)
|
if (!_profile.RequiresPlainFolders)
|
||||||
{
|
{
|
||||||
@ -899,7 +897,7 @@ namespace Emby.Dlna.Didl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem context, XmlWriter writer, Filter filter)
|
private void AddGeneralProperties(BaseItem item, StubType? itemStubType, BaseItem? context, XmlWriter writer, Filter filter)
|
||||||
{
|
{
|
||||||
AddCommonFields(item, itemStubType, context, writer, filter);
|
AddCommonFields(item, itemStubType, context, writer, filter);
|
||||||
|
|
||||||
@ -975,7 +973,7 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
private void AddCover(BaseItem item, StubType? stubType, XmlWriter writer)
|
private void AddCover(BaseItem item, StubType? stubType, XmlWriter writer)
|
||||||
{
|
{
|
||||||
ImageDownloadInfo imageInfo = GetImageInfo(item);
|
ImageDownloadInfo? imageInfo = GetImageInfo(item);
|
||||||
|
|
||||||
if (imageInfo is null)
|
if (imageInfo is null)
|
||||||
{
|
{
|
||||||
@ -1073,7 +1071,7 @@ namespace Emby.Dlna.Didl
|
|||||||
writer.WriteFullEndElement();
|
writer.WriteFullEndElement();
|
||||||
}
|
}
|
||||||
|
|
||||||
private ImageDownloadInfo GetImageInfo(BaseItem item)
|
private ImageDownloadInfo? GetImageInfo(BaseItem item)
|
||||||
{
|
{
|
||||||
if (item.HasImage(ImageType.Primary))
|
if (item.HasImage(ImageType.Primary))
|
||||||
{
|
{
|
||||||
@ -1118,7 +1116,7 @@ namespace Emby.Dlna.Didl
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private BaseItem GetFirstParentWithImageBelowUserRoot(BaseItem item)
|
private BaseItem? GetFirstParentWithImageBelowUserRoot(BaseItem item)
|
||||||
{
|
{
|
||||||
if (item is null)
|
if (item is null)
|
||||||
{
|
{
|
||||||
@ -1148,7 +1146,7 @@ namespace Emby.Dlna.Didl
|
|||||||
private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type)
|
private ImageDownloadInfo GetImageInfo(BaseItem item, ImageType type)
|
||||||
{
|
{
|
||||||
var imageInfo = item.GetImageInfo(type, 0);
|
var imageInfo = item.GetImageInfo(type, 0);
|
||||||
string tag = null;
|
string? tag = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -1250,7 +1248,7 @@ namespace Emby.Dlna.Didl
|
|||||||
{
|
{
|
||||||
internal Guid ItemId { get; set; }
|
internal Guid ItemId { get; set; }
|
||||||
|
|
||||||
internal string ImageTag { get; set; }
|
internal string? ImageTag { get; set; }
|
||||||
|
|
||||||
internal ImageType Type { get; set; }
|
internal ImageType Type { get; set; }
|
||||||
|
|
||||||
@ -1260,9 +1258,9 @@ namespace Emby.Dlna.Didl
|
|||||||
|
|
||||||
internal bool IsDirectStream { get; set; }
|
internal bool IsDirectStream { get; set; }
|
||||||
|
|
||||||
internal string Format { get; set; }
|
internal required string Format { get; set; }
|
||||||
|
|
||||||
internal ItemImageInfo ItemImageInfo { get; set; }
|
internal required ItemImageInfo ItemImageInfo { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@ -25,7 +23,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
private readonly object _timerLock = new object();
|
private readonly object _timerLock = new object();
|
||||||
private Timer _timer;
|
private Timer? _timer;
|
||||||
private int _muteVol;
|
private int _muteVol;
|
||||||
private int _volume;
|
private int _volume;
|
||||||
private DateTime _lastVolumeRefresh;
|
private DateTime _lastVolumeRefresh;
|
||||||
@ -40,13 +38,13 @@ namespace Emby.Dlna.PlayTo
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public event EventHandler<PlaybackStartEventArgs> PlaybackStart;
|
public event EventHandler<PlaybackStartEventArgs>? PlaybackStart;
|
||||||
|
|
||||||
public event EventHandler<PlaybackProgressEventArgs> PlaybackProgress;
|
public event EventHandler<PlaybackProgressEventArgs>? PlaybackProgress;
|
||||||
|
|
||||||
public event EventHandler<PlaybackStoppedEventArgs> PlaybackStopped;
|
public event EventHandler<PlaybackStoppedEventArgs>? PlaybackStopped;
|
||||||
|
|
||||||
public event EventHandler<MediaChangedEventArgs> MediaChanged;
|
public event EventHandler<MediaChangedEventArgs>? MediaChanged;
|
||||||
|
|
||||||
public DeviceInfo Properties { get; set; }
|
public DeviceInfo Properties { get; set; }
|
||||||
|
|
||||||
@ -75,13 +73,13 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
public bool IsStopped => TransportState == TransportState.STOPPED;
|
public bool IsStopped => TransportState == TransportState.STOPPED;
|
||||||
|
|
||||||
public Action OnDeviceUnavailable { get; set; }
|
public Action? OnDeviceUnavailable { get; set; }
|
||||||
|
|
||||||
private TransportCommands AvCommands { get; set; }
|
private TransportCommands? AvCommands { get; set; }
|
||||||
|
|
||||||
private TransportCommands RendererCommands { get; set; }
|
private TransportCommands? RendererCommands { get; set; }
|
||||||
|
|
||||||
public UBaseObject CurrentMediaInfo { get; private set; }
|
public UBaseObject? CurrentMediaInfo { get; private set; }
|
||||||
|
|
||||||
public void Start()
|
public void Start()
|
||||||
{
|
{
|
||||||
@ -131,7 +129,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
_volumeRefreshActive = true;
|
_volumeRefreshActive = true;
|
||||||
|
|
||||||
var time = immediate ? 100 : 10000;
|
var time = immediate ? 100 : 10000;
|
||||||
_timer.Change(time, Timeout.Infinite);
|
_timer?.Change(time, Timeout.Infinite);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,7 +147,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
_volumeRefreshActive = false;
|
_volumeRefreshActive = false;
|
||||||
|
|
||||||
_timer.Change(Timeout.Infinite, Timeout.Infinite);
|
_timer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -199,7 +197,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private DeviceService GetServiceRenderingControl()
|
private DeviceService? GetServiceRenderingControl()
|
||||||
{
|
{
|
||||||
var services = Properties.Services;
|
var services = Properties.Services;
|
||||||
|
|
||||||
@ -207,7 +205,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
services.FirstOrDefault(s => (s.ServiceType ?? string.Empty).StartsWith("urn:schemas-upnp-org:service:RenderingControl", StringComparison.OrdinalIgnoreCase));
|
services.FirstOrDefault(s => (s.ServiceType ?? string.Empty).StartsWith("urn:schemas-upnp-org:service:RenderingControl", StringComparison.OrdinalIgnoreCase));
|
||||||
}
|
}
|
||||||
|
|
||||||
private DeviceService GetAvTransportService()
|
private DeviceService? GetAvTransportService()
|
||||||
{
|
{
|
||||||
var services = Properties.Services;
|
var services = Properties.Services;
|
||||||
|
|
||||||
@ -240,7 +238,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
Properties.BaseUrl,
|
Properties.BaseUrl,
|
||||||
service,
|
service,
|
||||||
command.Name,
|
command.Name,
|
||||||
rendererCommands.BuildPost(command, service.ServiceType, value),
|
rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above
|
||||||
cancellationToken: cancellationToken)
|
cancellationToken: cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
@ -265,12 +263,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var service = GetServiceRenderingControl();
|
var service = GetServiceRenderingControl() ?? throw new InvalidOperationException("Unable to find service");
|
||||||
|
|
||||||
if (service is null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Unable to find service");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set it early and assume it will succeed
|
// Set it early and assume it will succeed
|
||||||
// Remote control will perform better
|
// Remote control will perform better
|
||||||
@ -281,7 +274,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
Properties.BaseUrl,
|
Properties.BaseUrl,
|
||||||
service,
|
service,
|
||||||
command.Name,
|
command.Name,
|
||||||
rendererCommands.BuildPost(command, service.ServiceType, value),
|
rendererCommands!.BuildPost(command, service.ServiceType, value), // null checked above
|
||||||
cancellationToken: cancellationToken)
|
cancellationToken: cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
@ -296,26 +289,20 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var service = GetAvTransportService();
|
var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
|
||||||
|
|
||||||
if (service is null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Unable to find service");
|
|
||||||
}
|
|
||||||
|
|
||||||
await new DlnaHttpClient(_logger, _httpClientFactory)
|
await new DlnaHttpClient(_logger, _httpClientFactory)
|
||||||
.SendCommandAsync(
|
.SendCommandAsync(
|
||||||
Properties.BaseUrl,
|
Properties.BaseUrl,
|
||||||
service,
|
service,
|
||||||
command.Name,
|
command.Name,
|
||||||
avCommands.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"),
|
avCommands!.BuildPost(command, service.ServiceType, string.Format(CultureInfo.InvariantCulture, "{0:hh}:{0:mm}:{0:ss}", value), "REL_TIME"), // null checked above
|
||||||
cancellationToken: cancellationToken)
|
cancellationToken: cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
RestartTimer(true);
|
RestartTimer(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SetAvTransport(string url, string header, string metaData, CancellationToken cancellationToken)
|
public async Task SetAvTransport(string url, string? header, string metaData, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
@ -335,14 +322,8 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{ "CurrentURIMetaData", CreateDidlMeta(metaData) }
|
{ "CurrentURIMetaData", CreateDidlMeta(metaData) }
|
||||||
};
|
};
|
||||||
|
|
||||||
var service = GetAvTransportService();
|
var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
|
||||||
|
var post = avCommands!.BuildPost(command, service.ServiceType, url, dictionary); // null checked above
|
||||||
if (service is null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Unable to find service");
|
|
||||||
}
|
|
||||||
|
|
||||||
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
|
|
||||||
await new DlnaHttpClient(_logger, _httpClientFactory)
|
await new DlnaHttpClient(_logger, _httpClientFactory)
|
||||||
.SendCommandAsync(
|
.SendCommandAsync(
|
||||||
Properties.BaseUrl,
|
Properties.BaseUrl,
|
||||||
@ -372,7 +353,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
* SetNextAvTransport is used to specify to the DLNA device what is the next track to play.
|
* SetNextAvTransport is used to specify to the DLNA device what is the next track to play.
|
||||||
* Without that information, the next track command on the device does not work.
|
* Without that information, the next track command on the device does not work.
|
||||||
*/
|
*/
|
||||||
public async Task SetNextAvTransport(string url, string header, string metaData, CancellationToken cancellationToken = default)
|
public async Task SetNextAvTransport(string url, string? header, string metaData, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
var avCommands = await GetAVProtocolAsync(cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
@ -380,7 +361,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
_logger.LogDebug("{PropertyName} - SetNextAvTransport Uri: {Url} DlnaHeaders: {Header}", Properties.Name, url, header);
|
_logger.LogDebug("{PropertyName} - SetNextAvTransport Uri: {Url} DlnaHeaders: {Header}", Properties.Name, url, header);
|
||||||
|
|
||||||
var command = avCommands.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase));
|
var command = avCommands?.ServiceActions.FirstOrDefault(c => string.Equals(c.Name, "SetNextAVTransportURI", StringComparison.OrdinalIgnoreCase));
|
||||||
if (command is null)
|
if (command is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@ -392,14 +373,8 @@ namespace Emby.Dlna.PlayTo
|
|||||||
{ "NextURIMetaData", CreateDidlMeta(metaData) }
|
{ "NextURIMetaData", CreateDidlMeta(metaData) }
|
||||||
};
|
};
|
||||||
|
|
||||||
var service = GetAvTransportService();
|
var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
|
||||||
|
var post = avCommands!.BuildPost(command, service.ServiceType, url, dictionary); // null checked above
|
||||||
if (service is null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Unable to find service");
|
|
||||||
}
|
|
||||||
|
|
||||||
var post = avCommands.BuildPost(command, service.ServiceType, url, dictionary);
|
|
||||||
await new DlnaHttpClient(_logger, _httpClientFactory)
|
await new DlnaHttpClient(_logger, _httpClientFactory)
|
||||||
.SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header, cancellationToken)
|
.SendCommandAsync(Properties.BaseUrl, service, command.Name, post, header, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
@ -423,12 +398,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
var service = GetAvTransportService();
|
var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
|
||||||
if (service is null)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException("Unable to find service");
|
|
||||||
}
|
|
||||||
|
|
||||||
return new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
|
return new DlnaHttpClient(_logger, _httpClientFactory).SendCommandAsync(
|
||||||
Properties.BaseUrl,
|
Properties.BaseUrl,
|
||||||
service,
|
service,
|
||||||
@ -460,14 +430,13 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var service = GetAvTransportService();
|
var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
|
||||||
|
|
||||||
await new DlnaHttpClient(_logger, _httpClientFactory)
|
await new DlnaHttpClient(_logger, _httpClientFactory)
|
||||||
.SendCommandAsync(
|
.SendCommandAsync(
|
||||||
Properties.BaseUrl,
|
Properties.BaseUrl,
|
||||||
service,
|
service,
|
||||||
command.Name,
|
command.Name,
|
||||||
avCommands.BuildPost(command, service.ServiceType, 1),
|
avCommands!.BuildPost(command, service.ServiceType, 1), // null checked above
|
||||||
cancellationToken: cancellationToken)
|
cancellationToken: cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
@ -484,14 +453,13 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var service = GetAvTransportService();
|
var service = GetAvTransportService() ?? throw new InvalidOperationException("Unable to find service");
|
||||||
|
|
||||||
await new DlnaHttpClient(_logger, _httpClientFactory)
|
await new DlnaHttpClient(_logger, _httpClientFactory)
|
||||||
.SendCommandAsync(
|
.SendCommandAsync(
|
||||||
Properties.BaseUrl,
|
Properties.BaseUrl,
|
||||||
service,
|
service,
|
||||||
command.Name,
|
command.Name,
|
||||||
avCommands.BuildPost(command, service.ServiceType, 1),
|
avCommands!.BuildPost(command, service.ServiceType, 1), // null checked above
|
||||||
cancellationToken: cancellationToken)
|
cancellationToken: cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
@ -500,7 +468,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
RestartTimer(true);
|
RestartTimer(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void TimerCallback(object sender)
|
private async void TimerCallback(object? sender)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
{
|
{
|
||||||
@ -623,7 +591,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
Properties.BaseUrl,
|
Properties.BaseUrl,
|
||||||
service,
|
service,
|
||||||
command.Name,
|
command.Name,
|
||||||
rendererCommands.BuildPost(command, service.ServiceType),
|
rendererCommands!.BuildPost(command, service.ServiceType), // null checked above
|
||||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (result is null || result.Document is null)
|
if (result is null || result.Document is null)
|
||||||
@ -673,7 +641,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
Properties.BaseUrl,
|
Properties.BaseUrl,
|
||||||
service,
|
service,
|
||||||
command.Name,
|
command.Name,
|
||||||
rendererCommands.BuildPost(command, service.ServiceType),
|
rendererCommands!.BuildPost(command, service.ServiceType), // null checked above
|
||||||
cancellationToken: cancellationToken).ConfigureAwait(false);
|
cancellationToken: cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
if (result is null || result.Document is null)
|
if (result is null || result.Document is null)
|
||||||
@ -728,7 +696,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<UBaseObject> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
private async Task<UBaseObject?> GetMediaInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo");
|
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetMediaInfo");
|
||||||
if (command is null)
|
if (command is null)
|
||||||
@ -798,7 +766,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(bool Success, UBaseObject Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
private async Task<(bool Success, UBaseObject? Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo");
|
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo");
|
||||||
if (command is null)
|
if (command is null)
|
||||||
@ -871,7 +839,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return (true, null);
|
return (true, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
XElement uPnpResponse = null;
|
XElement? uPnpResponse = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -895,7 +863,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return (true, uTrack);
|
return (true, uTrack);
|
||||||
}
|
}
|
||||||
|
|
||||||
private XElement ParseResponse(string xml)
|
private XElement? ParseResponse(string xml)
|
||||||
{
|
{
|
||||||
// Handle different variations sent back by devices.
|
// Handle different variations sent back by devices.
|
||||||
try
|
try
|
||||||
@ -929,7 +897,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static UBaseObject CreateUBaseObject(XElement container, string trackUri)
|
private static UBaseObject CreateUBaseObject(XElement? container, string? trackUri)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(container);
|
ArgumentNullException.ThrowIfNull(container);
|
||||||
|
|
||||||
@ -972,7 +940,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return new string[4];
|
return new string[4];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<TransportCommands> GetAVProtocolAsync(CancellationToken cancellationToken)
|
private async Task<TransportCommands?> GetAVProtocolAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (AvCommands is not null)
|
if (AvCommands is not null)
|
||||||
{
|
{
|
||||||
@ -1004,7 +972,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return AvCommands;
|
return AvCommands;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<TransportCommands> GetRenderingProtocolAsync(CancellationToken cancellationToken)
|
private async Task<TransportCommands?> GetRenderingProtocolAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (RendererCommands is not null)
|
if (RendererCommands is not null)
|
||||||
{
|
{
|
||||||
@ -1054,7 +1022,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
return baseUrl + url;
|
return baseUrl + url;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<Device> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken)
|
public static async Task<Device?> CreateuPnpDeviceAsync(Uri url, IHttpClientFactory httpClientFactory, ILogger logger, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var ssdpHttpClient = new DlnaHttpClient(logger, httpClientFactory);
|
var ssdpHttpClient = new DlnaHttpClient(logger, httpClientFactory);
|
||||||
|
|
||||||
@ -1287,7 +1255,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
}
|
}
|
||||||
|
|
||||||
_timer = null;
|
_timer = null;
|
||||||
Properties = null;
|
Properties = null!;
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
private readonly IDeviceDiscovery _deviceDiscovery;
|
private readonly IDeviceDiscovery _deviceDiscovery;
|
||||||
private readonly string _serverAddress;
|
private readonly string _serverAddress;
|
||||||
private readonly string _accessToken;
|
private readonly string? _accessToken;
|
||||||
|
|
||||||
private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>();
|
private readonly List<PlaylistItem> _playlist = new List<PlaylistItem>();
|
||||||
private Device _device;
|
private Device _device;
|
||||||
@ -59,7 +59,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
IUserManager userManager,
|
IUserManager userManager,
|
||||||
IImageProcessor imageProcessor,
|
IImageProcessor imageProcessor,
|
||||||
string serverAddress,
|
string serverAddress,
|
||||||
string accessToken,
|
string? accessToken,
|
||||||
IDeviceDiscovery deviceDiscovery,
|
IDeviceDiscovery deviceDiscovery,
|
||||||
IUserDataManager userDataManager,
|
IUserDataManager userDataManager,
|
||||||
ILocalizationManager localization,
|
ILocalizationManager localization,
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@ -67,7 +65,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
_deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
|
_deviceDiscovery.DeviceDiscovered += OnDeviceDiscoveryDeviceDiscovered;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnDeviceDiscoveryDeviceDiscovered(object sender, GenericEventArgs<UpnpDeviceInfo> e)
|
private async void OnDeviceDiscoveryDeviceDiscovered(object? sender, GenericEventArgs<UpnpDeviceInfo> e)
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
{
|
{
|
||||||
@ -76,12 +74,12 @@ namespace Emby.Dlna.PlayTo
|
|||||||
|
|
||||||
var info = e.Argument;
|
var info = e.Argument;
|
||||||
|
|
||||||
if (!info.Headers.TryGetValue("USN", out string usn))
|
if (!info.Headers.TryGetValue("USN", out string? usn))
|
||||||
{
|
{
|
||||||
usn = string.Empty;
|
usn = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!info.Headers.TryGetValue("NT", out string nt))
|
if (!info.Headers.TryGetValue("NT", out string? nt))
|
||||||
{
|
{
|
||||||
nt = string.Empty;
|
nt = string.Empty;
|
||||||
}
|
}
|
||||||
@ -161,7 +159,7 @@ namespace Emby.Dlna.PlayTo
|
|||||||
var uri = info.Location;
|
var uri = info.Location;
|
||||||
_logger.LogDebug("Attempting to create PlayToController from location {0}", uri);
|
_logger.LogDebug("Attempting to create PlayToController from location {0}", uri);
|
||||||
|
|
||||||
if (info.Headers.TryGetValue("USN", out string uuid))
|
if (info.Headers.TryGetValue("USN", out string? uuid))
|
||||||
{
|
{
|
||||||
uuid = GetUuid(uuid);
|
uuid = GetUuid(uuid);
|
||||||
}
|
}
|
||||||
|
@ -318,22 +318,24 @@ namespace Emby.Naming.Common
|
|||||||
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
|
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
|
||||||
// <!-- foo.E01., foo.e01. -->
|
// <!-- foo.E01., foo.e01. -->
|
||||||
new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
|
new EpisodeExpression(@"[^\\/]*?()\.?[Ee]([0-9]+)\.([^\\/]*)$"),
|
||||||
new EpisodeExpression("(?<year>[0-9]{4})[\\.-](?<month>[0-9]{2})[\\.-](?<day>[0-9]{2})", true)
|
new EpisodeExpression(@"(?<year>[0-9]{4})[._ -](?<month>[0-9]{2})[._ -](?<day>[0-9]{2})", true)
|
||||||
{
|
{
|
||||||
DateTimeFormats = new[]
|
DateTimeFormats = new[]
|
||||||
{
|
{
|
||||||
"yyyy.MM.dd",
|
"yyyy.MM.dd",
|
||||||
"yyyy-MM-dd",
|
"yyyy-MM-dd",
|
||||||
"yyyy_MM_dd"
|
"yyyy_MM_dd",
|
||||||
|
"yyyy MM dd"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
new EpisodeExpression(@"(?<day>[0-9]{2})[.-](?<month>[0-9]{2})[.-](?<year>[0-9]{4})", true)
|
new EpisodeExpression(@"(?<day>[0-9]{2})[._ -](?<month>[0-9]{2})[._ -](?<year>[0-9]{4})", true)
|
||||||
{
|
{
|
||||||
DateTimeFormats = new[]
|
DateTimeFormats = new[]
|
||||||
{
|
{
|
||||||
"dd.MM.yyyy",
|
"dd.MM.yyyy",
|
||||||
"dd-MM-yyyy",
|
"dd-MM-yyyy",
|
||||||
"dd_MM_yyyy"
|
"dd_MM_yyyy",
|
||||||
|
"dd MM yyyy"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -1006,7 +1006,7 @@ namespace Emby.Server.Implementations
|
|||||||
if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest)
|
if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest)
|
||||||
{
|
{
|
||||||
int? requestPort = request.Host.Port;
|
int? requestPort = request.Host.Port;
|
||||||
if (requestPort == null
|
if (requestPort is null
|
||||||
|| (requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase))
|
|| (requestPort == 80 && string.Equals(request.Scheme, "http", StringComparison.OrdinalIgnoreCase))
|
||||||
|| (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
|
|| (requestPort == 443 && string.Equals(request.Scheme, "https", StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
@ -1190,7 +1190,7 @@ namespace Emby.Server.Implementations
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_sessionManager != null)
|
if (_sessionManager is not null)
|
||||||
{
|
{
|
||||||
// used for closing websockets
|
// used for closing websockets
|
||||||
foreach (var session in _sessionManager.Sessions)
|
foreach (var session in _sessionManager.Sessions)
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Jellyfin.Extensions;
|
using Jellyfin.Extensions;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SQLitePCL.pretty;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Data
|
namespace Emby.Server.Implementations.Data
|
||||||
{
|
{
|
||||||
@ -45,24 +45,6 @@ namespace Emby.Server.Implementations.Data
|
|||||||
/// <value>The logger.</value>
|
/// <value>The logger.</value>
|
||||||
protected ILogger<BaseSqliteRepository> Logger { get; }
|
protected ILogger<BaseSqliteRepository> Logger { get; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the default connection flags.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The default connection flags.</value>
|
|
||||||
protected virtual ConnectionFlags DefaultConnectionFlags => ConnectionFlags.NoMutex;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the transaction mode.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The transaction mode.</value>>
|
|
||||||
protected TransactionMode TransactionMode => TransactionMode.Deferred;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the transaction mode for read-only operations.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The transaction mode.</value>
|
|
||||||
protected TransactionMode ReadTransactionMode => TransactionMode.Deferred;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the cache size.
|
/// Gets the cache size.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -107,23 +89,8 @@ namespace Emby.Server.Implementations.Data
|
|||||||
/// <see cref="SynchronousMode"/>
|
/// <see cref="SynchronousMode"/>
|
||||||
protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal;
|
protected virtual SynchronousMode? Synchronous => SynchronousMode.Normal;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the write lock.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The write lock.</value>
|
|
||||||
protected ConnectionPool WriteConnections { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets the write connection.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The write connection.</value>
|
|
||||||
protected ConnectionPool ReadConnections { get; set; }
|
|
||||||
|
|
||||||
public virtual void Initialize()
|
public virtual void Initialize()
|
||||||
{
|
{
|
||||||
WriteConnections = new ConnectionPool(WriteConnectionsCount, CreateWriteConnection);
|
|
||||||
ReadConnections = new ConnectionPool(ReadConnectionsCount, CreateReadConnection);
|
|
||||||
|
|
||||||
// Configuration and pragmas can affect VACUUM so it needs to be last.
|
// Configuration and pragmas can affect VACUUM so it needs to be last.
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection())
|
||||||
{
|
{
|
||||||
@ -131,57 +98,10 @@ namespace Emby.Server.Implementations.Data
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ManagedConnection GetConnection(bool readOnly = false)
|
protected SqliteConnection GetConnection()
|
||||||
=> readOnly ? ReadConnections.GetConnection() : WriteConnections.GetConnection();
|
|
||||||
|
|
||||||
protected SQLiteDatabaseConnection CreateWriteConnection()
|
|
||||||
{
|
{
|
||||||
var writeConnection = SQLite3.Open(
|
var connection = new SqliteConnection($"Filename={DbFilePath}");
|
||||||
DbFilePath,
|
connection.Open();
|
||||||
DefaultConnectionFlags | ConnectionFlags.Create | ConnectionFlags.ReadWrite,
|
|
||||||
null);
|
|
||||||
|
|
||||||
if (CacheSize.HasValue)
|
|
||||||
{
|
|
||||||
writeConnection.Execute("PRAGMA cache_size=" + CacheSize.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(LockingMode))
|
|
||||||
{
|
|
||||||
writeConnection.Execute("PRAGMA locking_mode=" + LockingMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(JournalMode))
|
|
||||||
{
|
|
||||||
writeConnection.Execute("PRAGMA journal_mode=" + JournalMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (JournalSizeLimit.HasValue)
|
|
||||||
{
|
|
||||||
writeConnection.Execute("PRAGMA journal_size_limit=" + JournalSizeLimit.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Synchronous.HasValue)
|
|
||||||
{
|
|
||||||
writeConnection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (PageSize.HasValue)
|
|
||||||
{
|
|
||||||
writeConnection.Execute("PRAGMA page_size=" + PageSize.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
writeConnection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
|
||||||
|
|
||||||
return writeConnection;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected SQLiteDatabaseConnection CreateReadConnection()
|
|
||||||
{
|
|
||||||
var connection = SQLite3.Open(
|
|
||||||
DbFilePath,
|
|
||||||
DefaultConnectionFlags | ConnectionFlags.ReadOnly,
|
|
||||||
null);
|
|
||||||
|
|
||||||
if (CacheSize.HasValue)
|
if (CacheSize.HasValue)
|
||||||
{
|
{
|
||||||
@ -208,24 +128,26 @@ namespace Emby.Server.Implementations.Data
|
|||||||
connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
|
connection.Execute("PRAGMA synchronous=" + (int)Synchronous.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (PageSize.HasValue)
|
||||||
|
{
|
||||||
|
connection.Execute("PRAGMA page_size=" + PageSize.Value);
|
||||||
|
}
|
||||||
|
|
||||||
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
connection.Execute("PRAGMA temp_store=" + (int)TempStore);
|
||||||
|
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IStatement PrepareStatement(ManagedConnection connection, string sql)
|
public SqliteCommand PrepareStatement(SqliteConnection connection, string sql)
|
||||||
=> connection.PrepareStatement(sql);
|
{
|
||||||
|
var command = connection.CreateCommand();
|
||||||
|
command.CommandText = sql;
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
public IStatement PrepareStatement(IDatabaseConnection connection, string sql)
|
protected bool TableExists(SqliteConnection connection, string name)
|
||||||
=> connection.PrepareStatement(sql);
|
|
||||||
|
|
||||||
protected bool TableExists(ManagedConnection connection, string name)
|
|
||||||
{
|
|
||||||
return connection.RunInTransaction(
|
|
||||||
db =>
|
|
||||||
{
|
|
||||||
using (var statement = PrepareStatement(db, "select DISTINCT tbl_name from sqlite_master"))
|
|
||||||
{
|
{
|
||||||
|
using var statement = PrepareStatement(connection, "select DISTINCT tbl_name from sqlite_master");
|
||||||
foreach (var row in statement.ExecuteQuery())
|
foreach (var row in statement.ExecuteQuery())
|
||||||
{
|
{
|
||||||
if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(name, row.GetString(0), StringComparison.OrdinalIgnoreCase))
|
||||||
@ -233,14 +155,11 @@ namespace Emby.Server.Implementations.Data
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
},
|
|
||||||
ReadTransactionMode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected List<string> GetColumnNames(IDatabaseConnection connection, string table)
|
protected List<string> GetColumnNames(SqliteConnection connection, string table)
|
||||||
{
|
{
|
||||||
var columnNames = new List<string>();
|
var columnNames = new List<string>();
|
||||||
|
|
||||||
@ -255,7 +174,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
return columnNames;
|
return columnNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void AddColumn(IDatabaseConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
|
protected void AddColumn(SqliteConnection connection, string table, string columnName, string type, List<string> existingColumnNames)
|
||||||
{
|
{
|
||||||
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
|
if (existingColumnNames.Contains(columnName, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@ -291,12 +210,6 @@ namespace Emby.Server.Implementations.Data
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (dispose)
|
|
||||||
{
|
|
||||||
WriteConnections.Dispose();
|
|
||||||
ReadConnections.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,79 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using SQLitePCL.pretty;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Data;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A pool of SQLite Database connections.
|
|
||||||
/// </summary>
|
|
||||||
public sealed class ConnectionPool : IDisposable
|
|
||||||
{
|
|
||||||
private readonly BlockingCollection<SQLiteDatabaseConnection> _connections = new();
|
|
||||||
private bool _disposed;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Initializes a new instance of the <see cref="ConnectionPool" /> class.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="count">The number of database connection to create.</param>
|
|
||||||
/// <param name="factory">Factory function to create the database connections.</param>
|
|
||||||
public ConnectionPool(int count, Func<SQLiteDatabaseConnection> factory)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < count; i++)
|
|
||||||
{
|
|
||||||
_connections.Add(factory.Invoke());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets a database connection from the pool if one is available, otherwise blocks.
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>A database connection.</returns>
|
|
||||||
public ManagedConnection GetConnection()
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
ThrowObjectDisposedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ManagedConnection(_connections.Take(), this);
|
|
||||||
|
|
||||||
static void ThrowObjectDisposedException()
|
|
||||||
{
|
|
||||||
throw new ObjectDisposedException(nameof(ConnectionPool));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Return a database connection to the pool.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="connection">The database connection to return.</param>
|
|
||||||
public void Return(SQLiteDatabaseConnection connection)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
connection.Dispose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_connections.Add(connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var connection in _connections)
|
|
||||||
{
|
|
||||||
connection.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
_connections.Dispose();
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,81 +0,0 @@
|
|||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using SQLitePCL.pretty;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Data
|
|
||||||
{
|
|
||||||
public sealed class ManagedConnection : IDisposable
|
|
||||||
{
|
|
||||||
private readonly ConnectionPool _pool;
|
|
||||||
|
|
||||||
private SQLiteDatabaseConnection _db;
|
|
||||||
|
|
||||||
private bool _disposed = false;
|
|
||||||
|
|
||||||
public ManagedConnection(SQLiteDatabaseConnection db, ConnectionPool pool)
|
|
||||||
{
|
|
||||||
_db = db;
|
|
||||||
_pool = pool;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IStatement PrepareStatement(string sql)
|
|
||||||
{
|
|
||||||
return _db.PrepareStatement(sql);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<IStatement> PrepareAll(string sql)
|
|
||||||
{
|
|
||||||
return _db.PrepareAll(sql);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ExecuteAll(string sql)
|
|
||||||
{
|
|
||||||
_db.ExecuteAll(sql);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Execute(string sql, params object[] values)
|
|
||||||
{
|
|
||||||
_db.Execute(sql, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RunQueries(string[] sql)
|
|
||||||
{
|
|
||||||
_db.RunQueries(sql);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RunInTransaction(Action<IDatabaseConnection> action, TransactionMode mode)
|
|
||||||
{
|
|
||||||
_db.RunInTransaction(action, mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
public T RunInTransaction<T>(Func<IDatabaseConnection, T> action, TransactionMode mode)
|
|
||||||
{
|
|
||||||
return _db.RunInTransaction(action, mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql)
|
|
||||||
{
|
|
||||||
return _db.Query(sql);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<IReadOnlyList<ResultSetValue>> Query(string sql, params object[] values)
|
|
||||||
{
|
|
||||||
return _db.Query(sql, values);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_pool.Return(_db);
|
|
||||||
|
|
||||||
_db = null!; // Don't dispose it
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +1,10 @@
|
|||||||
#nullable disable
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Data;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using SQLitePCL.pretty;
|
using Microsoft.Data.Sqlite;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Data
|
namespace Emby.Server.Implementations.Data
|
||||||
{
|
{
|
||||||
@ -52,19 +51,29 @@ namespace Emby.Server.Implementations.Data
|
|||||||
"yy-MM-dd"
|
"yy-MM-dd"
|
||||||
};
|
};
|
||||||
|
|
||||||
public static void RunQueries(this SQLiteDatabaseConnection connection, string[] queries)
|
public static IEnumerable<SqliteDataReader> Query(this SqliteConnection sqliteConnection, string commandText)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(queries);
|
if (sqliteConnection.State != ConnectionState.Open)
|
||||||
|
|
||||||
connection.RunInTransaction(conn =>
|
|
||||||
{
|
{
|
||||||
conn.ExecuteAll(string.Join(';', queries));
|
sqliteConnection.Open();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Guid ReadGuidFromBlob(this ResultSetValue result)
|
using var command = sqliteConnection.CreateCommand();
|
||||||
|
command.CommandText = commandText;
|
||||||
|
using (var reader = command.ExecuteReader())
|
||||||
{
|
{
|
||||||
return new Guid(result.ToBlob());
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
yield return reader;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Execute(this SqliteConnection sqliteConnection, string commandText)
|
||||||
|
{
|
||||||
|
using var command = sqliteConnection.CreateCommand();
|
||||||
|
command.CommandText = commandText;
|
||||||
|
command.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string ToDateTimeParamValue(this DateTime dateValue)
|
public static string ToDateTimeParamValue(this DateTime dateValue)
|
||||||
@ -83,27 +92,15 @@ namespace Emby.Server.Implementations.Data
|
|||||||
private static string GetDateTimeKindFormat(DateTimeKind kind)
|
private static string GetDateTimeKindFormat(DateTimeKind kind)
|
||||||
=> (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal;
|
=> (kind == DateTimeKind.Utc) ? DatetimeFormatUtc : DatetimeFormatLocal;
|
||||||
|
|
||||||
public static DateTime ReadDateTime(this ResultSetValue result)
|
public static bool TryReadDateTime(this SqliteDataReader reader, int index, out DateTime result)
|
||||||
{
|
{
|
||||||
var dateText = result.ToString();
|
if (reader.IsDBNull(index))
|
||||||
|
|
||||||
return DateTime.ParseExact(
|
|
||||||
dateText,
|
|
||||||
_datetimeFormats,
|
|
||||||
DateTimeFormatInfo.InvariantInfo,
|
|
||||||
DateTimeStyles.AdjustToUniversal);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TryReadDateTime(this IReadOnlyList<ResultSetValue> reader, int index, out DateTime result)
|
|
||||||
{
|
|
||||||
var item = reader[index];
|
|
||||||
if (item.IsDbNull())
|
|
||||||
{
|
{
|
||||||
result = default;
|
result = default;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var dateText = item.ToString();
|
var dateText = reader.GetString(index);
|
||||||
|
|
||||||
if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult))
|
if (DateTime.TryParseExact(dateText, _datetimeFormats, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out var dateTimeResult))
|
||||||
{
|
{
|
||||||
@ -115,335 +112,145 @@ namespace Emby.Server.Implementations.Data
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryGetGuid(this IReadOnlyList<ResultSetValue> reader, int index, out Guid result)
|
public static bool TryGetGuid(this SqliteDataReader reader, int index, out Guid result)
|
||||||
{
|
{
|
||||||
var item = reader[index];
|
if (reader.IsDBNull(index))
|
||||||
if (item.IsDbNull())
|
|
||||||
{
|
{
|
||||||
result = default;
|
result = default;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
result = item.ReadGuidFromBlob();
|
result = reader.GetGuid(index);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool IsDbNull(this ResultSetValue result)
|
public static bool TryGetString(this SqliteDataReader reader, int index, out string result)
|
||||||
{
|
{
|
||||||
return result.SQLiteType == SQLiteType.Null;
|
result = string.Empty;
|
||||||
}
|
|
||||||
|
|
||||||
public static string GetString(this IReadOnlyList<ResultSetValue> result, int index)
|
if (reader.IsDBNull(index))
|
||||||
{
|
|
||||||
return result[index].ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TryGetString(this IReadOnlyList<ResultSetValue> reader, int index, out string result)
|
|
||||||
{
|
|
||||||
result = null;
|
|
||||||
var item = reader[index];
|
|
||||||
if (item.IsDbNull())
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
result = item.ToString();
|
result = reader.GetString(index);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool GetBoolean(this IReadOnlyList<ResultSetValue> result, int index)
|
public static bool TryGetBoolean(this SqliteDataReader reader, int index, out bool result)
|
||||||
{
|
{
|
||||||
return result[index].ToBool();
|
if (reader.IsDBNull(index))
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TryGetBoolean(this IReadOnlyList<ResultSetValue> reader, int index, out bool result)
|
|
||||||
{
|
|
||||||
var item = reader[index];
|
|
||||||
if (item.IsDbNull())
|
|
||||||
{
|
{
|
||||||
result = default;
|
result = default;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
result = item.ToBool();
|
result = reader.GetBoolean(index);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryGetInt32(this IReadOnlyList<ResultSetValue> reader, int index, out int result)
|
public static bool TryGetInt32(this SqliteDataReader reader, int index, out int result)
|
||||||
{
|
{
|
||||||
var item = reader[index];
|
if (reader.IsDBNull(index))
|
||||||
if (item.IsDbNull())
|
|
||||||
{
|
{
|
||||||
result = default;
|
result = default;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
result = item.ToInt();
|
result = reader.GetInt32(index);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static long GetInt64(this IReadOnlyList<ResultSetValue> result, int index)
|
public static bool TryGetInt64(this SqliteDataReader reader, int index, out long result)
|
||||||
{
|
{
|
||||||
return result[index].ToInt64();
|
if (reader.IsDBNull(index))
|
||||||
}
|
|
||||||
|
|
||||||
public static bool TryGetInt64(this IReadOnlyList<ResultSetValue> reader, int index, out long result)
|
|
||||||
{
|
|
||||||
var item = reader[index];
|
|
||||||
if (item.IsDbNull())
|
|
||||||
{
|
{
|
||||||
result = default;
|
result = default;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
result = item.ToInt64();
|
result = reader.GetInt64(index);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryGetSingle(this IReadOnlyList<ResultSetValue> reader, int index, out float result)
|
public static bool TryGetSingle(this SqliteDataReader reader, int index, out float result)
|
||||||
{
|
{
|
||||||
var item = reader[index];
|
if (reader.IsDBNull(index))
|
||||||
if (item.IsDbNull())
|
|
||||||
{
|
{
|
||||||
result = default;
|
result = default;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
result = item.ToFloat();
|
result = reader.GetFloat(index);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryGetDouble(this IReadOnlyList<ResultSetValue> reader, int index, out double result)
|
public static bool TryGetDouble(this SqliteDataReader reader, int index, out double result)
|
||||||
{
|
{
|
||||||
var item = reader[index];
|
if (reader.IsDBNull(index))
|
||||||
if (item.IsDbNull())
|
|
||||||
{
|
{
|
||||||
result = default;
|
result = default;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
result = item.ToDouble();
|
result = reader.GetDouble(index);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Guid GetGuid(this IReadOnlyList<ResultSetValue> result, int index)
|
public static void TryBind(this SqliteCommand statement, string name, Guid value)
|
||||||
{
|
{
|
||||||
return result[index].ReadGuidFromBlob();
|
statement.TryBind(name, value, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Conditional("DEBUG")]
|
public static void TryBind(this SqliteCommand statement, string name, object? value, bool isBlob = false)
|
||||||
private static void CheckName(string name)
|
|
||||||
{
|
{
|
||||||
throw new ArgumentException("Invalid param name: " + name, nameof(name));
|
var preparedValue = value ?? DBNull.Value;
|
||||||
}
|
if (statement.Parameters.Contains(name))
|
||||||
|
|
||||||
public static void TryBind(this IStatement statement, string name, double value)
|
|
||||||
{
|
{
|
||||||
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
|
statement.Parameters[name].Value = preparedValue;
|
||||||
{
|
|
||||||
bindParam.Bind(value);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
CheckName(name);
|
// Blobs aren't always detected automatically
|
||||||
}
|
if (isBlob)
|
||||||
}
|
|
||||||
|
|
||||||
public static void TryBind(this IStatement statement, string name, string value)
|
|
||||||
{
|
{
|
||||||
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
|
statement.Parameters.Add(new SqliteParameter(name, SqliteType.Blob) { Value = value });
|
||||||
{
|
|
||||||
if (value is null)
|
|
||||||
{
|
|
||||||
bindParam.BindNull();
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
bindParam.Bind(value);
|
statement.Parameters.AddWithValue(name, preparedValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
CheckName(name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void TryBind(this IStatement statement, string name, bool value)
|
public static void TryBindNull(this SqliteCommand statement, string name)
|
||||||
{
|
{
|
||||||
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
|
statement.TryBind(name, DBNull.Value);
|
||||||
{
|
|
||||||
bindParam.Bind(value);
|
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
public static IEnumerable<SqliteDataReader> ExecuteQuery(this SqliteCommand command)
|
||||||
{
|
{
|
||||||
CheckName(name);
|
using (var reader = command.ExecuteReader())
|
||||||
|
{
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
yield return reader;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void TryBind(this IStatement statement, string name, float value)
|
public static int SelectScalarInt(this SqliteCommand command)
|
||||||
{
|
{
|
||||||
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
|
var result = command.ExecuteScalar();
|
||||||
{
|
// Can't be null since the method is used to retrieve Count
|
||||||
bindParam.Bind(value);
|
return Convert.ToInt32(result!, CultureInfo.InvariantCulture);
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CheckName(name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void TryBind(this IStatement statement, string name, int value)
|
public static SqliteCommand PrepareStatement(this SqliteConnection sqliteConnection, string sql)
|
||||||
{
|
{
|
||||||
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
|
var command = sqliteConnection.CreateCommand();
|
||||||
{
|
command.CommandText = sql;
|
||||||
bindParam.Bind(value);
|
return command;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CheckName(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void TryBind(this IStatement statement, string name, Guid value)
|
|
||||||
{
|
|
||||||
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
|
|
||||||
{
|
|
||||||
Span<byte> byteValue = stackalloc byte[16];
|
|
||||||
value.TryWriteBytes(byteValue);
|
|
||||||
bindParam.Bind(byteValue);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CheckName(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void TryBind(this IStatement statement, string name, DateTime value)
|
|
||||||
{
|
|
||||||
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
|
|
||||||
{
|
|
||||||
bindParam.Bind(value.ToDateTimeParamValue());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CheckName(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void TryBind(this IStatement statement, string name, long value)
|
|
||||||
{
|
|
||||||
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
|
|
||||||
{
|
|
||||||
bindParam.Bind(value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CheckName(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void TryBind(this IStatement statement, string name, ReadOnlySpan<byte> value)
|
|
||||||
{
|
|
||||||
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
|
|
||||||
{
|
|
||||||
bindParam.Bind(value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CheckName(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void TryBindNull(this IStatement statement, string name)
|
|
||||||
{
|
|
||||||
if (statement.BindParameters.TryGetValue(name, out IBindParameter bindParam))
|
|
||||||
{
|
|
||||||
bindParam.BindNull();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CheckName(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void TryBind(this IStatement statement, string name, DateTime? value)
|
|
||||||
{
|
|
||||||
if (value.HasValue)
|
|
||||||
{
|
|
||||||
TryBind(statement, name, value.Value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
TryBindNull(statement, name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void TryBind(this IStatement statement, string name, Guid? value)
|
|
||||||
{
|
|
||||||
if (value.HasValue)
|
|
||||||
{
|
|
||||||
TryBind(statement, name, value.Value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
TryBindNull(statement, name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void TryBind(this IStatement statement, string name, double? value)
|
|
||||||
{
|
|
||||||
if (value.HasValue)
|
|
||||||
{
|
|
||||||
TryBind(statement, name, value.Value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
TryBindNull(statement, name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void TryBind(this IStatement statement, string name, int? value)
|
|
||||||
{
|
|
||||||
if (value.HasValue)
|
|
||||||
{
|
|
||||||
TryBind(statement, name, value.Value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
TryBindNull(statement, name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void TryBind(this IStatement statement, string name, float? value)
|
|
||||||
{
|
|
||||||
if (value.HasValue)
|
|
||||||
{
|
|
||||||
TryBind(statement, name, value.Value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
TryBindNull(statement, name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void TryBind(this IStatement statement, string name, bool? value)
|
|
||||||
{
|
|
||||||
if (value.HasValue)
|
|
||||||
{
|
|
||||||
TryBind(statement, name, value.Value);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
TryBindNull(statement, name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IEnumerable<IReadOnlyList<ResultSetValue>> ExecuteQuery(this IStatement statement)
|
|
||||||
{
|
|
||||||
while (statement.MoveNext())
|
|
||||||
{
|
|
||||||
yield return statement.Current;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -11,8 +11,8 @@ using MediaBrowser.Controller.Configuration;
|
|||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Persistence;
|
using MediaBrowser.Controller.Persistence;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SQLitePCL.pretty;
|
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.Data
|
namespace Emby.Server.Implementations.Data
|
||||||
{
|
{
|
||||||
@ -44,14 +44,10 @@ namespace Emby.Server.Implementations.Data
|
|||||||
var userDataTableExists = TableExists(connection, "userdata");
|
var userDataTableExists = TableExists(connection, "userdata");
|
||||||
|
|
||||||
var users = userDatasTableExists ? null : _userManager.Users;
|
var users = userDatasTableExists ? null : _userManager.Users;
|
||||||
|
using var transaction = connection.BeginTransaction();
|
||||||
connection.RunInTransaction(
|
connection.Execute(string.Join(
|
||||||
db =>
|
';',
|
||||||
{
|
|
||||||
db.ExecuteAll(string.Join(';', new[]
|
|
||||||
{
|
|
||||||
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
|
"create table if not exists UserDatas (key nvarchar not null, userId INT not null, rating float null, played bit not null, playCount int not null, isFavorite bit not null, playbackPositionTicks bigint not null, lastPlayedDate datetime null, AudioStreamIndex INT, SubtitleStreamIndex INT)",
|
||||||
|
|
||||||
"drop index if exists idx_userdata",
|
"drop index if exists idx_userdata",
|
||||||
"drop index if exists idx_userdata1",
|
"drop index if exists idx_userdata1",
|
||||||
"drop index if exists idx_userdata2",
|
"drop index if exists idx_userdata2",
|
||||||
@ -62,30 +58,34 @@ namespace Emby.Server.Implementations.Data
|
|||||||
"create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
|
"create unique index if not exists UserDatasIndex1 on UserDatas (key, userId)",
|
||||||
"create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
|
"create index if not exists UserDatasIndex2 on UserDatas (key, userId, played)",
|
||||||
"create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
|
"create index if not exists UserDatasIndex3 on UserDatas (key, userId, playbackPositionTicks)",
|
||||||
"create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)"
|
"create index if not exists UserDatasIndex4 on UserDatas (key, userId, isFavorite)"));
|
||||||
}));
|
|
||||||
|
|
||||||
if (userDataTableExists)
|
if (!userDataTableExists)
|
||||||
{
|
{
|
||||||
var existingColumnNames = GetColumnNames(db, "userdata");
|
transaction.Commit();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
AddColumn(db, "userdata", "InternalUserId", "int", existingColumnNames);
|
var existingColumnNames = GetColumnNames(connection, "userdata");
|
||||||
AddColumn(db, "userdata", "AudioStreamIndex", "int", existingColumnNames);
|
|
||||||
AddColumn(db, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
|
|
||||||
|
|
||||||
if (!userDatasTableExists)
|
AddColumn(connection, "userdata", "InternalUserId", "int", existingColumnNames);
|
||||||
|
AddColumn(connection, "userdata", "AudioStreamIndex", "int", existingColumnNames);
|
||||||
|
AddColumn(connection, "userdata", "SubtitleStreamIndex", "int", existingColumnNames);
|
||||||
|
|
||||||
|
if (userDatasTableExists)
|
||||||
{
|
{
|
||||||
ImportUserIds(db, users);
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
db.ExecuteAll("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
|
ImportUserIds(connection, users);
|
||||||
}
|
|
||||||
}
|
connection.Execute("INSERT INTO UserDatas (key, userId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex) SELECT key, InternalUserId, rating, played, playCount, isFavorite, playbackPositionTicks, lastPlayedDate, AudioStreamIndex, SubtitleStreamIndex from userdata where InternalUserId not null");
|
||||||
},
|
|
||||||
TransactionMode);
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ImportUserIds(IDatabaseConnection db, IEnumerable<User> users)
|
private void ImportUserIds(SqliteConnection db, IEnumerable<User> users)
|
||||||
{
|
{
|
||||||
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
|
var userIdsWithUserData = GetAllUserIdsWithUserData(db);
|
||||||
|
|
||||||
@ -101,13 +101,12 @@ namespace Emby.Server.Implementations.Data
|
|||||||
statement.TryBind("@UserId", user.Id);
|
statement.TryBind("@UserId", user.Id);
|
||||||
statement.TryBind("@InternalUserId", user.InternalId);
|
statement.TryBind("@InternalUserId", user.InternalId);
|
||||||
|
|
||||||
statement.MoveNext();
|
statement.ExecuteNonQuery();
|
||||||
statement.Reset();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Guid> GetAllUserIdsWithUserData(IDatabaseConnection db)
|
private List<Guid> GetAllUserIdsWithUserData(SqliteConnection db)
|
||||||
{
|
{
|
||||||
var list = new List<Guid>();
|
var list = new List<Guid>();
|
||||||
|
|
||||||
@ -117,7 +116,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
list.Add(row[0].ReadGuidFromBlob());
|
list.Add(row.GetGuid(0));
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -169,17 +168,14 @@ namespace Emby.Server.Implementations.Data
|
|||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection())
|
||||||
|
using (var transaction = connection.BeginTransaction())
|
||||||
{
|
{
|
||||||
connection.RunInTransaction(
|
SaveUserData(connection, internalUserId, key, userData);
|
||||||
db =>
|
transaction.Commit();
|
||||||
{
|
|
||||||
SaveUserData(db, internalUserId, key, userData);
|
|
||||||
},
|
|
||||||
TransactionMode);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SaveUserData(IDatabaseConnection db, long internalUserId, string key, UserItemData userData)
|
private static void SaveUserData(SqliteConnection db, long internalUserId, string key, UserItemData userData)
|
||||||
{
|
{
|
||||||
using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
|
using (var statement = db.PrepareStatement("replace into UserDatas (key, userId, rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex) values (@key, @userId, @rating,@played,@playCount,@isFavorite,@playbackPositionTicks,@lastPlayedDate,@AudioStreamIndex,@SubtitleStreamIndex)"))
|
||||||
{
|
{
|
||||||
@ -227,7 +223,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
statement.TryBindNull("@SubtitleStreamIndex");
|
statement.TryBindNull("@SubtitleStreamIndex");
|
||||||
}
|
}
|
||||||
|
|
||||||
statement.MoveNext();
|
statement.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -239,16 +235,14 @@ namespace Emby.Server.Implementations.Data
|
|||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
using (var connection = GetConnection())
|
using (var connection = GetConnection())
|
||||||
{
|
using (var transaction = connection.BeginTransaction())
|
||||||
connection.RunInTransaction(
|
|
||||||
db =>
|
|
||||||
{
|
{
|
||||||
foreach (var userItemData in userDataList)
|
foreach (var userItemData in userDataList)
|
||||||
{
|
{
|
||||||
SaveUserData(db, internalUserId, userItemData.Key, userItemData);
|
SaveUserData(connection, internalUserId, userItemData.Key, userItemData);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
TransactionMode);
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -272,7 +266,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
|
|
||||||
ArgumentException.ThrowIfNullOrEmpty(key);
|
ArgumentException.ThrowIfNullOrEmpty(key);
|
||||||
|
|
||||||
using (var connection = GetConnection(true))
|
using (var connection = GetConnection())
|
||||||
{
|
{
|
||||||
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
|
using (var statement = connection.PrepareStatement("select key,userid,rating,played,playCount,isFavorite,playbackPositionTicks,lastPlayedDate,AudioStreamIndex,SubtitleStreamIndex from UserDatas where key =@Key and userId=@UserId"))
|
||||||
{
|
{
|
||||||
@ -336,7 +330,7 @@ namespace Emby.Server.Implementations.Data
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="reader">The list of result set values.</param>
|
/// <param name="reader">The list of result set values.</param>
|
||||||
/// <returns>The user item data.</returns>
|
/// <returns>The user item data.</returns>
|
||||||
private UserItemData ReadRow(IReadOnlyList<ResultSetValue> reader)
|
private UserItemData ReadRow(SqliteDataReader reader)
|
||||||
{
|
{
|
||||||
var userData = new UserItemData();
|
var userData = new UserItemData();
|
||||||
|
|
||||||
@ -348,10 +342,10 @@ namespace Emby.Server.Implementations.Data
|
|||||||
userData.Rating = rating;
|
userData.Rating = rating;
|
||||||
}
|
}
|
||||||
|
|
||||||
userData.Played = reader[3].ToBool();
|
userData.Played = reader.GetBoolean(3);
|
||||||
userData.PlayCount = reader[4].ToInt();
|
userData.PlayCount = reader.GetInt32(4);
|
||||||
userData.IsFavorite = reader[5].ToBool();
|
userData.IsFavorite = reader.GetBoolean(5);
|
||||||
userData.PlaybackPositionTicks = reader[6].ToInt64();
|
userData.PlaybackPositionTicks = reader.GetInt64(6);
|
||||||
|
|
||||||
if (reader.TryReadDateTime(7, out var lastPlayedDate))
|
if (reader.TryReadDateTime(7, out var lastPlayedDate))
|
||||||
{
|
{
|
||||||
|
@ -903,10 +903,11 @@ namespace Emby.Server.Implementations.Dto
|
|||||||
dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder;
|
dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dto.LUFS = item.LUFS;
|
||||||
|
|
||||||
// Add audio info
|
// Add audio info
|
||||||
if (item is Audio audio)
|
if (item is Audio audio)
|
||||||
{
|
{
|
||||||
dto.LUFS = audio.LUFS;
|
|
||||||
dto.Album = audio.Album;
|
dto.Album = audio.Album;
|
||||||
if (audio.ExtraType.HasValue)
|
if (audio.ExtraType.HasValue)
|
||||||
{
|
{
|
||||||
|
@ -24,6 +24,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DiscUtils.Udf" />
|
<PackageReference Include="DiscUtils.Udf" />
|
||||||
<PackageReference Include="Jellyfin.XmlTv" />
|
<PackageReference Include="Jellyfin.XmlTv" />
|
||||||
|
<PackageReference Include="Microsoft.Data.Sqlite" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" />
|
||||||
@ -31,7 +32,6 @@
|
|||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" />
|
||||||
<PackageReference Include="Mono.Nat" />
|
<PackageReference Include="Mono.Nat" />
|
||||||
<PackageReference Include="prometheus-net.DotNetRuntime" />
|
<PackageReference Include="prometheus-net.DotNetRuntime" />
|
||||||
<PackageReference Include="SQLitePCL.pretty.netstandard" />
|
|
||||||
<PackageReference Include="DotNet.Glob" />
|
<PackageReference Include="DotNet.Glob" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
@ -12,6 +12,7 @@ using MediaBrowser.Controller.Net;
|
|||||||
using MediaBrowser.Controller.Net.WebSocketMessages;
|
using MediaBrowser.Controller.Net.WebSocketMessages;
|
||||||
using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
|
using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
|
||||||
using MediaBrowser.Model.Session;
|
using MediaBrowser.Model.Session;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Emby.Server.Implementations.HttpServer
|
namespace Emby.Server.Implementations.HttpServer
|
||||||
@ -43,14 +44,17 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="logger">The logger.</param>
|
/// <param name="logger">The logger.</param>
|
||||||
/// <param name="socket">The socket.</param>
|
/// <param name="socket">The socket.</param>
|
||||||
|
/// <param name="authorizationInfo">The authorization information.</param>
|
||||||
/// <param name="remoteEndPoint">The remote end point.</param>
|
/// <param name="remoteEndPoint">The remote end point.</param>
|
||||||
public WebSocketConnection(
|
public WebSocketConnection(
|
||||||
ILogger<WebSocketConnection> logger,
|
ILogger<WebSocketConnection> logger,
|
||||||
WebSocket socket,
|
WebSocket socket,
|
||||||
|
AuthorizationInfo authorizationInfo,
|
||||||
IPAddress? remoteEndPoint)
|
IPAddress? remoteEndPoint)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_socket = socket;
|
_socket = socket;
|
||||||
|
AuthorizationInfo = authorizationInfo;
|
||||||
RemoteEndPoint = remoteEndPoint;
|
RemoteEndPoint = remoteEndPoint;
|
||||||
|
|
||||||
_jsonOptions = JsonDefaults.Options;
|
_jsonOptions = JsonDefaults.Options;
|
||||||
@ -60,30 +64,22 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event EventHandler<EventArgs>? Closed;
|
public event EventHandler<EventArgs>? Closed;
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Gets the remote end point.
|
public AuthorizationInfo AuthorizationInfo { get; }
|
||||||
/// </summary>
|
|
||||||
|
/// <inheritdoc />
|
||||||
public IPAddress? RemoteEndPoint { get; }
|
public IPAddress? RemoteEndPoint { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Gets or sets the receive action.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The receive action.</value>
|
|
||||||
public Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
|
public Func<WebSocketMessageInfo, Task>? OnReceive { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Gets the last activity date.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The last activity date.</value>
|
|
||||||
public DateTime LastActivityDate { get; private set; }
|
public DateTime LastActivityDate { get; private set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public DateTime LastKeepAliveDate { get; set; }
|
public DateTime LastKeepAliveDate { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Gets the state.
|
|
||||||
/// </summary>
|
|
||||||
/// <value>The state.</value>
|
|
||||||
public WebSocketState State => _socket.State;
|
public WebSocketState State => _socket.State;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -101,7 +97,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task ProcessAsync(CancellationToken cancellationToken = default)
|
public async Task ReceiveAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var pipe = new Pipe();
|
var pipe = new Pipe();
|
||||||
var writer = pipe.Writer;
|
var writer = pipe.Writer;
|
||||||
|
@ -51,6 +51,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
using var connection = new WebSocketConnection(
|
using var connection = new WebSocketConnection(
|
||||||
_loggerFactory.CreateLogger<WebSocketConnection>(),
|
_loggerFactory.CreateLogger<WebSocketConnection>(),
|
||||||
webSocket,
|
webSocket,
|
||||||
|
authorizationInfo,
|
||||||
context.GetNormalizedRemoteIP())
|
context.GetNormalizedRemoteIP())
|
||||||
{
|
{
|
||||||
OnReceive = ProcessWebSocketMessageReceived
|
OnReceive = ProcessWebSocketMessageReceived
|
||||||
@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.HttpServer
|
|||||||
|
|
||||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||||
|
|
||||||
await connection.ProcessAsync().ConfigureAwait(false);
|
await connection.ReceiveAsync().ConfigureAwait(false);
|
||||||
_logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
|
_logger.LogInformation("WS {IP} closed", context.Connection.RemoteIpAddress);
|
||||||
}
|
}
|
||||||
catch (Exception ex) // Otherwise ASP.Net will ignore the exception
|
catch (Exception ex) // Otherwise ASP.Net will ignore the exception
|
||||||
|
@ -31,6 +31,7 @@ namespace Emby.Server.Implementations.Images
|
|||||||
return _libraryManager.GetItemList(new InternalItemsQuery
|
return _libraryManager.GetItemList(new InternalItemsQuery
|
||||||
{
|
{
|
||||||
Parent = item,
|
Parent = item,
|
||||||
|
Recursive = true,
|
||||||
DtoOptions = new DtoOptions(true),
|
DtoOptions = new DtoOptions(true),
|
||||||
ImageTypes = new ImageType[] { ImageType.Primary },
|
ImageTypes = new ImageType[] { ImageType.Primary },
|
||||||
OrderBy = new (string, SortOrder)[]
|
OrderBy = new (string, SortOrder)[]
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@ -63,7 +64,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
private const string ShortcutFileExtension = ".mblink";
|
private const string ShortcutFileExtension = ".mblink";
|
||||||
|
|
||||||
private readonly ILogger<LibraryManager> _logger;
|
private readonly ILogger<LibraryManager> _logger;
|
||||||
private readonly IMemoryCache _memoryCache;
|
private readonly ConcurrentDictionary<Guid, BaseItem> _cache;
|
||||||
private readonly ITaskManager _taskManager;
|
private readonly ITaskManager _taskManager;
|
||||||
private readonly IUserManager _userManager;
|
private readonly IUserManager _userManager;
|
||||||
private readonly IUserDataManager _userDataRepository;
|
private readonly IUserDataManager _userDataRepository;
|
||||||
@ -111,7 +112,6 @@ namespace Emby.Server.Implementations.Library
|
|||||||
/// <param name="mediaEncoder">The media encoder.</param>
|
/// <param name="mediaEncoder">The media encoder.</param>
|
||||||
/// <param name="itemRepository">The item repository.</param>
|
/// <param name="itemRepository">The item repository.</param>
|
||||||
/// <param name="imageProcessor">The image processor.</param>
|
/// <param name="imageProcessor">The image processor.</param>
|
||||||
/// <param name="memoryCache">The memory cache.</param>
|
|
||||||
/// <param name="namingOptions">The naming options.</param>
|
/// <param name="namingOptions">The naming options.</param>
|
||||||
/// <param name="directoryService">The directory service.</param>
|
/// <param name="directoryService">The directory service.</param>
|
||||||
public LibraryManager(
|
public LibraryManager(
|
||||||
@ -128,7 +128,6 @@ namespace Emby.Server.Implementations.Library
|
|||||||
IMediaEncoder mediaEncoder,
|
IMediaEncoder mediaEncoder,
|
||||||
IItemRepository itemRepository,
|
IItemRepository itemRepository,
|
||||||
IImageProcessor imageProcessor,
|
IImageProcessor imageProcessor,
|
||||||
IMemoryCache memoryCache,
|
|
||||||
NamingOptions namingOptions,
|
NamingOptions namingOptions,
|
||||||
IDirectoryService directoryService)
|
IDirectoryService directoryService)
|
||||||
{
|
{
|
||||||
@ -145,7 +144,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
_mediaEncoder = mediaEncoder;
|
_mediaEncoder = mediaEncoder;
|
||||||
_itemRepository = itemRepository;
|
_itemRepository = itemRepository;
|
||||||
_imageProcessor = imageProcessor;
|
_imageProcessor = imageProcessor;
|
||||||
_memoryCache = memoryCache;
|
_cache = new ConcurrentDictionary<Guid, BaseItem>();
|
||||||
_namingOptions = namingOptions;
|
_namingOptions = namingOptions;
|
||||||
|
|
||||||
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
|
_extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions, directoryService);
|
||||||
@ -300,7 +299,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_memoryCache.Set(item.Id, item);
|
_cache[item.Id] = item;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DeleteItem(BaseItem item, DeleteOptions options)
|
public void DeleteItem(BaseItem item, DeleteOptions options)
|
||||||
@ -359,7 +358,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
|
|
||||||
var children = item.IsFolder
|
var children = item.IsFolder
|
||||||
? ((Folder)item).GetRecursiveChildren(false)
|
? ((Folder)item).GetRecursiveChildren(false)
|
||||||
: Enumerable.Empty<BaseItem>();
|
: Array.Empty<BaseItem>();
|
||||||
|
|
||||||
foreach (var metadataPath in GetMetadataPaths(item, children))
|
foreach (var metadataPath in GetMetadataPaths(item, children))
|
||||||
{
|
{
|
||||||
@ -441,7 +440,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
_itemRepository.DeleteItem(child.Id);
|
_itemRepository.DeleteItem(child.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
_memoryCache.Remove(item.Id);
|
_cache.TryRemove(item.Id, out _);
|
||||||
|
|
||||||
ReportItemRemoved(item, parent);
|
ReportItemRemoved(item, parent);
|
||||||
}
|
}
|
||||||
@ -1233,7 +1232,7 @@ namespace Emby.Server.Implementations.Library
|
|||||||
throw new ArgumentException("Guid can't be empty", nameof(id));
|
throw new ArgumentException("Guid can't be empty", nameof(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_memoryCache.TryGetValue(id, out BaseItem item))
|
if (_cache.TryGetValue(id, out BaseItem item))
|
||||||
{
|
{
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -23,14 +24,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||||||
{
|
{
|
||||||
public abstract class BaseTunerHost
|
public abstract class BaseTunerHost
|
||||||
{
|
{
|
||||||
private readonly IMemoryCache _memoryCache;
|
private readonly ConcurrentDictionary<string, List<ChannelInfo>> _cache;
|
||||||
|
|
||||||
protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem, IMemoryCache memoryCache)
|
protected BaseTunerHost(IServerConfigurationManager config, ILogger<BaseTunerHost> logger, IFileSystem fileSystem)
|
||||||
{
|
{
|
||||||
Config = config;
|
Config = config;
|
||||||
Logger = logger;
|
Logger = logger;
|
||||||
_memoryCache = memoryCache;
|
|
||||||
FileSystem = fileSystem;
|
FileSystem = fileSystem;
|
||||||
|
_cache = new ConcurrentDictionary<string, List<ChannelInfo>>();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected IServerConfigurationManager Config { get; }
|
protected IServerConfigurationManager Config { get; }
|
||||||
@ -51,7 +52,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||||||
{
|
{
|
||||||
var key = tuner.Id;
|
var key = tuner.Id;
|
||||||
|
|
||||||
if (enableCache && !string.IsNullOrEmpty(key) && _memoryCache.TryGetValue(key, out List<ChannelInfo> cache))
|
if (enableCache && !string.IsNullOrEmpty(key) && _cache.TryGetValue(key, out List<ChannelInfo> cache))
|
||||||
{
|
{
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
@ -61,7 +62,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||||||
|
|
||||||
if (!string.IsNullOrEmpty(key) && list.Count > 0)
|
if (!string.IsNullOrEmpty(key) && list.Count > 0)
|
||||||
{
|
{
|
||||||
_memoryCache.Set(key, list);
|
_cache[key] = list;
|
||||||
}
|
}
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
|
@ -50,9 +50,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
IServerApplicationHost appHost,
|
IServerApplicationHost appHost,
|
||||||
ISocketFactory socketFactory,
|
ISocketFactory socketFactory,
|
||||||
IStreamHelper streamHelper,
|
IStreamHelper streamHelper)
|
||||||
IMemoryCache memoryCache)
|
: base(config, logger, fileSystem)
|
||||||
: base(config, logger, fileSystem, memoryCache)
|
|
||||||
{
|
{
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
_appHost = appHost;
|
_appHost = appHost;
|
||||||
|
@ -54,9 +54,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
|||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
IServerApplicationHost appHost,
|
IServerApplicationHost appHost,
|
||||||
INetworkManager networkManager,
|
INetworkManager networkManager,
|
||||||
IStreamHelper streamHelper,
|
IStreamHelper streamHelper)
|
||||||
IMemoryCache memoryCache)
|
: base(config, logger, fileSystem)
|
||||||
: base(config, logger, fileSystem, memoryCache)
|
|
||||||
{
|
{
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
_appHost = appHost;
|
_appHost = appHost;
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
"Folders": "المجلدات",
|
"Folders": "المجلدات",
|
||||||
"Genres": "التصنيفات",
|
"Genres": "التصنيفات",
|
||||||
"HeaderAlbumArtists": "فناني الألبوم",
|
"HeaderAlbumArtists": "فناني الألبوم",
|
||||||
"HeaderContinueWatching": "أستئناف المشاهدة",
|
"HeaderContinueWatching": "استئناف المشاهدة",
|
||||||
"HeaderFavoriteAlbums": "الألبومات المفضلة",
|
"HeaderFavoriteAlbums": "الألبومات المفضلة",
|
||||||
"HeaderFavoriteArtists": "الفنانون المفضلون",
|
"HeaderFavoriteArtists": "الفنانون المفضلون",
|
||||||
"HeaderFavoriteEpisodes": "الحلقات المفضلة",
|
"HeaderFavoriteEpisodes": "الحلقات المفضلة",
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
"HeaderFavoriteEpisodes": "Oblíbené epizody",
|
"HeaderFavoriteEpisodes": "Oblíbené epizody",
|
||||||
"HeaderFavoriteShows": "Oblíbené seriály",
|
"HeaderFavoriteShows": "Oblíbené seriály",
|
||||||
"HeaderFavoriteSongs": "Oblíbená hudba",
|
"HeaderFavoriteSongs": "Oblíbená hudba",
|
||||||
"HeaderLiveTV": "Televize",
|
"HeaderLiveTV": "Živý přenos",
|
||||||
"HeaderNextUp": "Další díly",
|
"HeaderNextUp": "Další díly",
|
||||||
"HeaderRecordingGroups": "Skupiny nahrávek",
|
"HeaderRecordingGroups": "Skupiny nahrávek",
|
||||||
"HomeVideos": "Domácí videa",
|
"HomeVideos": "Domácí videa",
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"Albums": "Album-album",
|
"Albums": "Albums",
|
||||||
"AppDeviceValues": "Apl: {0}, Peranti: {1}",
|
"AppDeviceValues": "Apl: {0}, Peranti: {1}",
|
||||||
"Application": "Aplikasi",
|
"Application": "Aplikasi",
|
||||||
"Artists": "Artis-artis",
|
"Artists": "Artis-artis",
|
||||||
|
@ -24,5 +24,10 @@
|
|||||||
"TaskDownloadMissingSubtitlesDescription": "Scours the seven seas o' the internet for subtitles that be missin' based on the captain's map o' metadata.",
|
"TaskDownloadMissingSubtitlesDescription": "Scours the seven seas o' the internet for subtitles that be missin' based on the captain's map o' metadata.",
|
||||||
"HeaderAlbumArtists": "Buccaneers o' the musical arts",
|
"HeaderAlbumArtists": "Buccaneers o' the musical arts",
|
||||||
"HeaderFavoriteAlbums": "Beloved booty o' musical adventures",
|
"HeaderFavoriteAlbums": "Beloved booty o' musical adventures",
|
||||||
"HeaderFavoriteArtists": "Treasured scallywags o' the creative seas"
|
"HeaderFavoriteArtists": "Treasured scallywags o' the creative seas",
|
||||||
|
"Channels": "Channels",
|
||||||
|
"Forced": "Pressed",
|
||||||
|
"External": "Outboard",
|
||||||
|
"HeaderFavoriteEpisodes": "Treasured Tales",
|
||||||
|
"HeaderFavoriteShows": "Treasured Tales"
|
||||||
}
|
}
|
||||||
|
@ -31,13 +31,13 @@
|
|||||||
"ItemRemovedWithName": "{0} - изъято из медиатеки",
|
"ItemRemovedWithName": "{0} - изъято из медиатеки",
|
||||||
"LabelIpAddressValue": "IP-адрес: {0}",
|
"LabelIpAddressValue": "IP-адрес: {0}",
|
||||||
"LabelRunningTimeValue": "Длительность: {0}",
|
"LabelRunningTimeValue": "Длительность: {0}",
|
||||||
"Latest": "Новое",
|
"Latest": "Последние добавленные",
|
||||||
"MessageApplicationUpdated": "Jellyfin Server был обновлён",
|
"MessageApplicationUpdated": "Jellyfin Server был обновлён",
|
||||||
"MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}",
|
"MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}",
|
||||||
"MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена",
|
"MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена",
|
||||||
"MessageServerConfigurationUpdated": "Конфигурация сервера была обновлена",
|
"MessageServerConfigurationUpdated": "Конфигурация сервера была обновлена",
|
||||||
"MixedContent": "Смешанное содержание",
|
"MixedContent": "Смешанное содержание",
|
||||||
"Movies": "Кино",
|
"Movies": "Фильмы",
|
||||||
"Music": "Музыка",
|
"Music": "Музыка",
|
||||||
"MusicVideos": "Муз. видео",
|
"MusicVideos": "Муз. видео",
|
||||||
"NameInstallFailed": "Установка {0} неудачна",
|
"NameInstallFailed": "Установка {0} неудачна",
|
||||||
@ -77,7 +77,7 @@
|
|||||||
"SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}",
|
"SubtitleDownloadFailureFromForItem": "Субтитры к {1} не удалось загрузить с {0}",
|
||||||
"Sync": "Синхронизация",
|
"Sync": "Синхронизация",
|
||||||
"System": "Система",
|
"System": "Система",
|
||||||
"TvShows": "ТВ",
|
"TvShows": "Телесериалы",
|
||||||
"User": "Пользователь",
|
"User": "Пользователь",
|
||||||
"UserCreatedWithName": "Пользователь {0} был создан",
|
"UserCreatedWithName": "Пользователь {0} был создан",
|
||||||
"UserDeletedWithName": "Пользователь {0} был удалён",
|
"UserDeletedWithName": "Пользователь {0} был удалён",
|
||||||
|
@ -3,19 +3,19 @@
|
|||||||
"AppDeviceValues": "Uygulama: {0}, Aygıt: {1}",
|
"AppDeviceValues": "Uygulama: {0}, Aygıt: {1}",
|
||||||
"Application": "Uygulama",
|
"Application": "Uygulama",
|
||||||
"Artists": "Sanatçılar",
|
"Artists": "Sanatçılar",
|
||||||
"AuthenticationSucceededWithUserName": "{0} kimlik başarıyla doğrulandı",
|
"AuthenticationSucceededWithUserName": "{0} kimliği başarıyla doğrulandı",
|
||||||
"Books": "Kitaplar",
|
"Books": "Kitaplar",
|
||||||
"CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi",
|
"CameraImageUploadedFrom": "{0} 'den yeni bir kamera resmi yüklendi",
|
||||||
"Channels": "Kanallar",
|
"Channels": "Kanallar",
|
||||||
"ChapterNameValue": "Bölüm {0}",
|
"ChapterNameValue": "{0}. Bölüm",
|
||||||
"Collections": "Koleksiyonlar",
|
"Collections": "Koleksiyonlar",
|
||||||
"DeviceOfflineWithName": "{0} bağlantısı kesildi",
|
"DeviceOfflineWithName": "{0} bağlantısı kesildi",
|
||||||
"DeviceOnlineWithName": "{0} bağlı",
|
"DeviceOnlineWithName": "{0} bağlı",
|
||||||
"FailedLoginAttemptWithUserName": "{0} adresinden giriş denemesi başarısız oldu",
|
"FailedLoginAttemptWithUserName": "{0} kullanıcısının giriş denemesi başarısız oldu",
|
||||||
"Favorites": "Favoriler",
|
"Favorites": "Favoriler",
|
||||||
"Folders": "Klasörler",
|
"Folders": "Klasörler",
|
||||||
"Genres": "Türler",
|
"Genres": "Türler",
|
||||||
"HeaderAlbumArtists": "Albüm Sanatçıları",
|
"HeaderAlbumArtists": "Albüm sanatçıları",
|
||||||
"HeaderContinueWatching": "İzlemeye Devam Et",
|
"HeaderContinueWatching": "İzlemeye Devam Et",
|
||||||
"HeaderFavoriteAlbums": "Favori Albümler",
|
"HeaderFavoriteAlbums": "Favori Albümler",
|
||||||
"HeaderFavoriteArtists": "Favori Sanatçılar",
|
"HeaderFavoriteArtists": "Favori Sanatçılar",
|
||||||
@ -25,7 +25,7 @@
|
|||||||
"HeaderLiveTV": "Canlı TV",
|
"HeaderLiveTV": "Canlı TV",
|
||||||
"HeaderNextUp": "Gelecek Hafta",
|
"HeaderNextUp": "Gelecek Hafta",
|
||||||
"HeaderRecordingGroups": "Kayıt Grupları",
|
"HeaderRecordingGroups": "Kayıt Grupları",
|
||||||
"HomeVideos": "Ana sayfa videoları",
|
"HomeVideos": "Ana Sayfa Videoları",
|
||||||
"Inherit": "Devral",
|
"Inherit": "Devral",
|
||||||
"ItemAddedWithName": "{0} kütüphaneye eklendi",
|
"ItemAddedWithName": "{0} kütüphaneye eklendi",
|
||||||
"ItemRemovedWithName": "{0} kütüphaneden silindi",
|
"ItemRemovedWithName": "{0} kütüphaneden silindi",
|
||||||
@ -34,14 +34,14 @@
|
|||||||
"Latest": "En son",
|
"Latest": "En son",
|
||||||
"MessageApplicationUpdated": "Jellyfin Sunucusu güncellendi",
|
"MessageApplicationUpdated": "Jellyfin Sunucusu güncellendi",
|
||||||
"MessageApplicationUpdatedTo": "Jellyfin Sunucusu {0} sürümüne güncellendi",
|
"MessageApplicationUpdatedTo": "Jellyfin Sunucusu {0} sürümüne güncellendi",
|
||||||
"MessageNamedServerConfigurationUpdatedWithValue": "Sunucu ayar kısmı {0} güncellendi",
|
"MessageNamedServerConfigurationUpdatedWithValue": "Sunucu yapılandırma bölümü {0} güncellendi",
|
||||||
"MessageServerConfigurationUpdated": "Sunucu ayarları güncellendi",
|
"MessageServerConfigurationUpdated": "Sunucu yapılandırması güncellendi",
|
||||||
"MixedContent": "Karışık içerik",
|
"MixedContent": "Karışık içerik",
|
||||||
"Movies": "Filmler",
|
"Movies": "Filmler",
|
||||||
"Music": "Müzik",
|
"Music": "Müzik",
|
||||||
"MusicVideos": "Müzik videoları",
|
"MusicVideos": "Müzik Videoları",
|
||||||
"NameInstallFailed": "{0} kurulumu başarısız",
|
"NameInstallFailed": "{0} kurulumu başarısız",
|
||||||
"NameSeasonNumber": "Sezon {0}",
|
"NameSeasonNumber": "{0}. Sezon",
|
||||||
"NameSeasonUnknown": "Bilinmeyen Sezon",
|
"NameSeasonUnknown": "Bilinmeyen Sezon",
|
||||||
"NewVersionIsAvailable": "Jellyfin Sunucusunun yeni bir sürümü indirmek için hazır.",
|
"NewVersionIsAvailable": "Jellyfin Sunucusunun yeni bir sürümü indirmek için hazır.",
|
||||||
"NotificationOptionApplicationUpdateAvailable": "Uygulama güncellemesi mevcut",
|
"NotificationOptionApplicationUpdateAvailable": "Uygulama güncellemesi mevcut",
|
||||||
@ -55,9 +55,9 @@
|
|||||||
"NotificationOptionPluginInstalled": "Eklenti yüklendi",
|
"NotificationOptionPluginInstalled": "Eklenti yüklendi",
|
||||||
"NotificationOptionPluginUninstalled": "Eklenti kaldırıldı",
|
"NotificationOptionPluginUninstalled": "Eklenti kaldırıldı",
|
||||||
"NotificationOptionPluginUpdateInstalled": "Eklenti güncellemesi yüklendi",
|
"NotificationOptionPluginUpdateInstalled": "Eklenti güncellemesi yüklendi",
|
||||||
"NotificationOptionServerRestartRequired": "Sunucu yeniden başlatma gerekli",
|
"NotificationOptionServerRestartRequired": "Sunucunun yeniden başlatılması gerekiyor",
|
||||||
"NotificationOptionTaskFailed": "Zamanlanmış görev hatası",
|
"NotificationOptionTaskFailed": "Zamanlanmış görev hatası",
|
||||||
"NotificationOptionUserLockedOut": "Kullanıcı kitlendi",
|
"NotificationOptionUserLockedOut": "Kullanıcı kilitlendi",
|
||||||
"NotificationOptionVideoPlayback": "Video oynatma başladı",
|
"NotificationOptionVideoPlayback": "Video oynatma başladı",
|
||||||
"NotificationOptionVideoPlaybackStopped": "Video oynatma durduruldu",
|
"NotificationOptionVideoPlaybackStopped": "Video oynatma durduruldu",
|
||||||
"Photos": "Fotoğraflar",
|
"Photos": "Fotoğraflar",
|
||||||
@ -74,36 +74,36 @@
|
|||||||
"Songs": "Şarkılar",
|
"Songs": "Şarkılar",
|
||||||
"StartupEmbyServerIsLoading": "Jellyfin Sunucusu yükleniyor. Lütfen kısa süre sonra tekrar deneyin.",
|
"StartupEmbyServerIsLoading": "Jellyfin Sunucusu yükleniyor. Lütfen kısa süre sonra tekrar deneyin.",
|
||||||
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
"SubtitleDownloadFailureForItem": "Subtitles failed to download for {0}",
|
||||||
"SubtitleDownloadFailureFromForItem": "{1} için alt yazılar {0} 'dan indirilemedi",
|
"SubtitleDownloadFailureFromForItem": "{1} için alt yazılar {0} sağlayıcısından indirilemedi",
|
||||||
"Sync": "Eşzamanlama",
|
"Sync": "Eşzamanlama",
|
||||||
"System": "Sistem",
|
"System": "Sistem",
|
||||||
"TvShows": "Diziler",
|
"TvShows": "Diziler",
|
||||||
"User": "Kullanıcı",
|
"User": "Kullanıcı",
|
||||||
"UserCreatedWithName": "{0} kullanıcısı oluşturuldu",
|
"UserCreatedWithName": "{0} kullanıcısı oluşturuldu",
|
||||||
"UserDeletedWithName": "Kullanıcı {0} silindi",
|
"UserDeletedWithName": "{0} kullanıcısı silindi",
|
||||||
"UserDownloadingItemWithValues": "{0} indiriliyor {1}",
|
"UserDownloadingItemWithValues": "{0} {1} medyasını indiriyor",
|
||||||
"UserLockedOutWithName": "Kullanıcı {0} kitlendi",
|
"UserLockedOutWithName": "{0} adlı kullanıcı kilitlendi",
|
||||||
"UserOfflineFromDevice": "{0}, {1} ile bağlantısı kesildi",
|
"UserOfflineFromDevice": "{0} kullanıcısının {1} ile bağlantısı kesildi",
|
||||||
"UserOnlineFromDevice": "{0}, {1} çevrimiçi",
|
"UserOnlineFromDevice": "{0} kullanıcısı {1} ile çevrimiçi",
|
||||||
"UserPasswordChangedWithName": "{0} kullanıcısı için şifre değiştirildi",
|
"UserPasswordChangedWithName": "{0} kullanıcısının parolası değiştirildi",
|
||||||
"UserPolicyUpdatedWithName": "Kullanıcı politikası {0} için 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": "Medya kütüphanenize {0} eklendi",
|
||||||
"ValueSpecialEpisodeName": "Özel - {0}",
|
"ValueSpecialEpisodeName": "Özel - {0}",
|
||||||
"VersionNumber": "Sürüm {0}",
|
"VersionNumber": "Sürüm {0}",
|
||||||
"TaskCleanCache": "Geçici dosya klasörünü temizle",
|
"TaskCleanCache": "Geçici Dosya Klasörünü Temizle",
|
||||||
"TasksChannelsCategory": "İnternet kanalları",
|
"TasksChannelsCategory": "İnternet Kanalları",
|
||||||
"TasksApplicationCategory": "Uygulama",
|
"TasksApplicationCategory": "Uygulama",
|
||||||
"TasksLibraryCategory": "Kütüphane",
|
"TasksLibraryCategory": "Kütüphane",
|
||||||
"TasksMaintenanceCategory": "Bakım",
|
"TasksMaintenanceCategory": "Bakım",
|
||||||
"TaskRefreshPeopleDescription": "Medya kütüphanenizdeki videoların oyuncu ve yönetmen bilgilerini günceller.",
|
"TaskRefreshPeopleDescription": "Medya kütüphanenizdeki videoların oyuncu ve yönetmen bilgilerini günceller.",
|
||||||
"TaskDownloadMissingSubtitlesDescription": "Metadata ayarlarını baz alarak eksik altyazıları internette arar.",
|
"TaskDownloadMissingSubtitlesDescription": "Meta veri yapılandırmasına dayalı olarak eksik altyazılar için internette arama yapar.",
|
||||||
"TaskDownloadMissingSubtitles": "Eksik altyazıları indir",
|
"TaskDownloadMissingSubtitles": "Eksik altyazıları indir",
|
||||||
"TaskRefreshChannelsDescription": "Internet kanal bilgilerini yenile.",
|
"TaskRefreshChannelsDescription": "Internet kanal bilgilerini yenile.",
|
||||||
"TaskRefreshChannels": "Kanalları Yenile",
|
"TaskRefreshChannels": "Kanalları Yenile",
|
||||||
"TaskCleanTranscodeDescription": "Bir günden daha eski dönüştürme dosyalarını siler.",
|
"TaskCleanTranscodeDescription": "Bir günden daha eski kod dönüştürme dosyalarını siler.",
|
||||||
"TaskCleanTranscode": "Dönüşüm Dizinini Temizle",
|
"TaskCleanTranscode": "Kod Dönüştürme Dizinini Temizle",
|
||||||
"TaskUpdatePluginsDescription": "Otomatik güncellenmeye ayarlanmış eklentilerin güncellemelerini indirir ve kurar.",
|
"TaskUpdatePluginsDescription": "Otomatik güncellenmeye ayarlanmış eklentilerin güncellemelerini indirir ve kurar.",
|
||||||
"TaskUpdatePlugins": "Eklentileri Güncelle",
|
"TaskUpdatePlugins": "Eklentileri Güncelle",
|
||||||
"TaskRefreshPeople": "Kullanıcıları Yenile",
|
"TaskRefreshPeople": "Kullanıcıları Yenile",
|
||||||
|
@ -3,6 +3,7 @@ A/fig,0
|
|||||||
A/i,0
|
A/i,0
|
||||||
A/fig/i,0
|
A/fig/i,0
|
||||||
APTA,0
|
APTA,0
|
||||||
|
ERI,0
|
||||||
TP,0
|
TP,0
|
||||||
0+,0
|
0+,0
|
||||||
6+,6
|
6+,6
|
||||||
|
|
@ -1,5 +1,6 @@
|
|||||||
Public Averti,0
|
Public Averti,0
|
||||||
Tous Publics,0
|
Tous Publics,0
|
||||||
|
TP,0
|
||||||
U,0
|
U,0
|
||||||
0+,0
|
0+,0
|
||||||
6+,6
|
6+,6
|
||||||
|
|
6
Emby.Server.Implementations/Localization/Ratings/sk.csv
Normal file
6
Emby.Server.Implementations/Localization/Ratings/sk.csv
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
NR,0
|
||||||
|
U,0
|
||||||
|
7,7
|
||||||
|
12,12
|
||||||
|
15,15
|
||||||
|
18,18
|
|
@ -677,7 +677,7 @@ namespace Emby.Server.Implementations.Plugins
|
|||||||
}
|
}
|
||||||
catch (JsonException ex)
|
catch (JsonException ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error deserializing {Json}.", Encoding.UTF8.GetString(data!));
|
_logger.LogError(ex, "Error deserializing {Json}.", Encoding.UTF8.GetString(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (manifest is not null)
|
if (manifest is not null)
|
||||||
|
@ -24,6 +24,7 @@ using MediaBrowser.Controller.Drawing;
|
|||||||
using MediaBrowser.Controller.Dto;
|
using MediaBrowser.Controller.Dto;
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
using MediaBrowser.Controller.Events;
|
using MediaBrowser.Controller.Events;
|
||||||
|
using MediaBrowser.Controller.Events.Authentication;
|
||||||
using MediaBrowser.Controller.Events.Session;
|
using MediaBrowser.Controller.Events.Session;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
@ -1462,7 +1463,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
|
|
||||||
if (user is null)
|
if (user is null)
|
||||||
{
|
{
|
||||||
await _eventManager.PublishAsync(new GenericEventArgs<AuthenticationRequest>(request)).ConfigureAwait(false);
|
await _eventManager.PublishAsync(new AuthenticationRequestEventArgs(request)).ConfigureAwait(false);
|
||||||
throw new AuthenticationException("Invalid username or password entered.");
|
throw new AuthenticationException("Invalid username or password entered.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1498,7 +1499,7 @@ namespace Emby.Server.Implementations.Session
|
|||||||
ServerId = _appHost.SystemId
|
ServerId = _appHost.SystemId
|
||||||
};
|
};
|
||||||
|
|
||||||
await _eventManager.PublishAsync(new GenericEventArgs<AuthenticationResult>(returnResult)).ConfigureAwait(false);
|
await _eventManager.PublishAsync(new AuthenticationResultEventArgs(returnResult)).ConfigureAwait(false);
|
||||||
return returnResult;
|
return returnResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1508,36 +1509,21 @@ namespace Emby.Server.Implementations.Session
|
|||||||
new DeviceQuery
|
new DeviceQuery
|
||||||
{
|
{
|
||||||
DeviceId = deviceId,
|
DeviceId = deviceId,
|
||||||
UserId = user.Id,
|
UserId = user.Id
|
||||||
Limit = 1
|
|
||||||
}).ConfigureAwait(false)).Items.FirstOrDefault();
|
|
||||||
|
|
||||||
var allExistingForDevice = (await _deviceManager.GetDevices(
|
|
||||||
new DeviceQuery
|
|
||||||
{
|
|
||||||
DeviceId = deviceId
|
|
||||||
}).ConfigureAwait(false)).Items;
|
}).ConfigureAwait(false)).Items;
|
||||||
|
|
||||||
foreach (var auth in allExistingForDevice)
|
foreach (var auth in existing)
|
||||||
{
|
|
||||||
if (existing is null || !string.Equals(auth.AccessToken, existing.AccessToken, StringComparison.Ordinal))
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
// Logout any existing sessions for the user on this device
|
||||||
await Logout(auth).ConfigureAwait(false);
|
await Logout(auth).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error while logging out.");
|
_logger.LogError(ex, "Error while logging out existing session.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (existing is not null)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Reissuing access token: {Token}", existing.AccessToken);
|
|
||||||
return existing.AccessToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Creating new access token for user {0}", user.Id);
|
_logger.LogInformation("Creating new access token for user {0}", user.Id);
|
||||||
var device = await _deviceManager.CreateDevice(new Device(user.Id, app, appVersion, deviceName, deviceId)).ConfigureAwait(false);
|
var device = await _deviceManager.CreateDevice(new Device(user.Id, app, appVersion, deviceName, deviceId)).ConfigureAwait(false);
|
||||||
|
@ -135,12 +135,12 @@ namespace Emby.Server.Implementations.TV
|
|||||||
|
|
||||||
private IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IReadOnlyList<string> seriesKeys, DtoOptions dtoOptions)
|
private IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IReadOnlyList<string> seriesKeys, DtoOptions dtoOptions)
|
||||||
{
|
{
|
||||||
var allNextUp = seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, false));
|
var allNextUp = seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, request.EnableResumable, false));
|
||||||
|
|
||||||
if (request.EnableRewatching)
|
if (request.EnableRewatching)
|
||||||
{
|
{
|
||||||
allNextUp = allNextUp.Concat(
|
allNextUp = allNextUp
|
||||||
seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, true)))
|
.Concat(seriesKeys.Select(i => GetNextUp(i, user, dtoOptions, false, true)))
|
||||||
.OrderByDescending(i => i.LastWatchedDate);
|
.OrderByDescending(i => i.LastWatchedDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,7 +183,7 @@ namespace Emby.Server.Implementations.TV
|
|||||||
/// Gets the next up.
|
/// Gets the next up.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>Task{Episode}.</returns>
|
/// <returns>Task{Episode}.</returns>
|
||||||
private (DateTime LastWatchedDate, Func<Episode?> GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching)
|
private (DateTime LastWatchedDate, Func<Episode?> GetEpisodeFunction) GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool includeResumable, bool includePlayed)
|
||||||
{
|
{
|
||||||
var lastQuery = new InternalItemsQuery(user)
|
var lastQuery = new InternalItemsQuery(user)
|
||||||
{
|
{
|
||||||
@ -200,8 +200,8 @@ namespace Emby.Server.Implementations.TV
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// If rewatching is enabled, sort first by date played and then by season and episode numbers
|
// If including played results, sort first by date played and then by season and episode numbers
|
||||||
lastQuery.OrderBy = rewatching
|
lastQuery.OrderBy = includePlayed
|
||||||
? new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) }
|
? new[] { (ItemSortBy.DatePlayed, SortOrder.Descending), (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) }
|
||||||
: new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) };
|
: new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Descending), (ItemSortBy.IndexNumber, SortOrder.Descending) };
|
||||||
|
|
||||||
@ -216,7 +216,7 @@ namespace Emby.Server.Implementations.TV
|
|||||||
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
||||||
OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending) },
|
OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending) },
|
||||||
Limit = 1,
|
Limit = 1,
|
||||||
IsPlayed = rewatching,
|
IsPlayed = includePlayed,
|
||||||
IsVirtualItem = false,
|
IsVirtualItem = false,
|
||||||
ParentIndexNumberNotEquals = 0,
|
ParentIndexNumberNotEquals = 0,
|
||||||
DtoOptions = dtoOptions
|
DtoOptions = dtoOptions
|
||||||
@ -240,7 +240,7 @@ namespace Emby.Server.Implementations.TV
|
|||||||
SeriesPresentationUniqueKey = seriesKey,
|
SeriesPresentationUniqueKey = seriesKey,
|
||||||
ParentIndexNumber = 0,
|
ParentIndexNumber = 0,
|
||||||
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
||||||
IsPlayed = rewatching,
|
IsPlayed = includePlayed,
|
||||||
IsVirtualItem = false,
|
IsVirtualItem = false,
|
||||||
DtoOptions = dtoOptions
|
DtoOptions = dtoOptions
|
||||||
})
|
})
|
||||||
@ -269,7 +269,7 @@ namespace Emby.Server.Implementations.TV
|
|||||||
nextEpisode = sortedConsideredEpisodes.FirstOrDefault();
|
nextEpisode = sortedConsideredEpisodes.FirstOrDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nextEpisode is not null)
|
if (nextEpisode is not null && !includeResumable)
|
||||||
{
|
{
|
||||||
var userData = _userDataManager.GetUserData(user, nextEpisode);
|
var userData = _userDataManager.GetUserData(user, nextEpisode);
|
||||||
|
|
||||||
|
@ -1651,7 +1651,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
_encodingHelper.GetInputArgument(state, _encodingOptions, segmentContainer),
|
_encodingHelper.GetInputArgument(state, _encodingOptions, segmentContainer),
|
||||||
threads,
|
threads,
|
||||||
mapArgs,
|
mapArgs,
|
||||||
GetVideoArguments(state, startNumber, isEventPlaylist),
|
GetVideoArguments(state, startNumber, isEventPlaylist, segmentContainer),
|
||||||
GetAudioArguments(state),
|
GetAudioArguments(state),
|
||||||
maxMuxingQueueSize,
|
maxMuxingQueueSize,
|
||||||
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
|
state.SegmentLength.ToString(CultureInfo.InvariantCulture),
|
||||||
@ -1703,6 +1703,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
var audioCodec = _encodingHelper.GetAudioEncoder(state);
|
var audioCodec = _encodingHelper.GetAudioEncoder(state);
|
||||||
|
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
|
||||||
|
|
||||||
// dts, flac, opus and truehd are experimental in mp4 muxer
|
// dts, flac, opus and truehd are experimental in mp4 muxer
|
||||||
var strictArgs = string.Empty;
|
var strictArgs = string.Empty;
|
||||||
@ -1719,14 +1720,12 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
{
|
{
|
||||||
if (EncodingHelper.IsCopyCodec(audioCodec))
|
if (EncodingHelper.IsCopyCodec(audioCodec))
|
||||||
{
|
{
|
||||||
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
|
|
||||||
|
|
||||||
return "-acodec copy" + bitStreamArgs + strictArgs;
|
return "-acodec copy" + bitStreamArgs + strictArgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
var audioTranscodeParams = string.Empty;
|
var audioTranscodeParams = string.Empty;
|
||||||
|
|
||||||
audioTranscodeParams += "-acodec " + audioCodec + strictArgs;
|
audioTranscodeParams += "-acodec " + audioCodec + bitStreamArgs + strictArgs;
|
||||||
|
|
||||||
var audioBitrate = state.OutputAudioBitrate;
|
var audioBitrate = state.OutputAudioBitrate;
|
||||||
var audioChannels = state.OutputAudioChannels;
|
var audioChannels = state.OutputAudioChannels;
|
||||||
@ -1761,7 +1760,6 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
if (EncodingHelper.IsCopyCodec(audioCodec))
|
if (EncodingHelper.IsCopyCodec(audioCodec))
|
||||||
{
|
{
|
||||||
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
||||||
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
|
|
||||||
var copyArgs = "-codec:a:0 copy" + bitStreamArgs + strictArgs;
|
var copyArgs = "-codec:a:0 copy" + bitStreamArgs + strictArgs;
|
||||||
|
|
||||||
if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
|
if (EncodingHelper.IsCopyCodec(videoCodec) && state.EnableBreakOnNonKeyFrames(videoCodec))
|
||||||
@ -1772,7 +1770,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
return copyArgs;
|
return copyArgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
var args = "-codec:a:0 " + audioCodec + strictArgs;
|
var args = "-codec:a:0 " + audioCodec + bitStreamArgs + strictArgs;
|
||||||
|
|
||||||
var channels = state.OutputAudioChannels;
|
var channels = state.OutputAudioChannels;
|
||||||
|
|
||||||
@ -1816,8 +1814,9 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
/// <param name="state">The <see cref="StreamState"/>.</param>
|
/// <param name="state">The <see cref="StreamState"/>.</param>
|
||||||
/// <param name="startNumber">The first number in the hls sequence.</param>
|
/// <param name="startNumber">The first number in the hls sequence.</param>
|
||||||
/// <param name="isEventPlaylist">Whether the playlist is EVENT or VOD.</param>
|
/// <param name="isEventPlaylist">Whether the playlist is EVENT or VOD.</param>
|
||||||
|
/// <param name="segmentContainer">The segment container.</param>
|
||||||
/// <returns>The command line arguments for video transcoding.</returns>
|
/// <returns>The command line arguments for video transcoding.</returns>
|
||||||
private string GetVideoArguments(StreamState state, int startNumber, bool isEventPlaylist)
|
private string GetVideoArguments(StreamState state, int startNumber, bool isEventPlaylist, string segmentContainer)
|
||||||
{
|
{
|
||||||
if (state.VideoStream is null)
|
if (state.VideoStream is null)
|
||||||
{
|
{
|
||||||
@ -1909,7 +1908,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO why was this not enabled for VOD?
|
// TODO why was this not enabled for VOD?
|
||||||
if (isEventPlaylist)
|
if (isEventPlaylist && string.Equals(segmentContainer, "ts", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
args += " -flags -global_header";
|
args += " -flags -global_header";
|
||||||
}
|
}
|
||||||
|
@ -68,7 +68,8 @@ public class TvShowsController : BaseJellyfinApiController
|
|||||||
/// <param name="nextUpDateCutoff">Optional. Starting date of shows to show in Next Up section.</param>
|
/// <param name="nextUpDateCutoff">Optional. Starting date of shows to show in Next Up section.</param>
|
||||||
/// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
|
/// <param name="enableTotalRecordCount">Whether to enable the total records count. Defaults to true.</param>
|
||||||
/// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param>
|
/// <param name="disableFirstEpisode">Whether to disable sending the first episode in a series as next up.</param>
|
||||||
/// <param name="enableRewatching">Whether to include watched episode in next up results.</param>
|
/// <param name="enableResumable">Whether to include resumable episodes in next up results.</param>
|
||||||
|
/// <param name="enableRewatching">Whether to include watched episodes in next up results.</param>
|
||||||
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
|
/// <returns>A <see cref="QueryResult{BaseItemDto}"/> with the next up episodes.</returns>
|
||||||
[HttpGet("NextUp")]
|
[HttpGet("NextUp")]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
@ -86,6 +87,7 @@ public class TvShowsController : BaseJellyfinApiController
|
|||||||
[FromQuery] DateTime? nextUpDateCutoff,
|
[FromQuery] DateTime? nextUpDateCutoff,
|
||||||
[FromQuery] bool enableTotalRecordCount = true,
|
[FromQuery] bool enableTotalRecordCount = true,
|
||||||
[FromQuery] bool disableFirstEpisode = false,
|
[FromQuery] bool disableFirstEpisode = false,
|
||||||
|
[FromQuery] bool enableResumable = true,
|
||||||
[FromQuery] bool enableRewatching = false)
|
[FromQuery] bool enableRewatching = false)
|
||||||
{
|
{
|
||||||
userId = RequestHelpers.GetUserId(User, userId);
|
userId = RequestHelpers.GetUserId(User, userId);
|
||||||
@ -104,6 +106,7 @@ public class TvShowsController : BaseJellyfinApiController
|
|||||||
EnableTotalRecordCount = enableTotalRecordCount,
|
EnableTotalRecordCount = enableTotalRecordCount,
|
||||||
DisableFirstEpisode = disableFirstEpisode,
|
DisableFirstEpisode = disableFirstEpisode,
|
||||||
NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue,
|
NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue,
|
||||||
|
EnableResumable = enableResumable,
|
||||||
EnableRewatching = enableRewatching
|
EnableRewatching = enableRewatching
|
||||||
},
|
},
|
||||||
options);
|
options);
|
||||||
|
@ -494,7 +494,7 @@ public class UserController : BaseJellyfinApiController
|
|||||||
var isLocal = HttpContext.IsLocal()
|
var isLocal = HttpContext.IsLocal()
|
||||||
|| _networkManager.IsInLocalNetwork(ip);
|
|| _networkManager.IsInLocalNetwork(ip);
|
||||||
|
|
||||||
if (isLocal)
|
if (!isLocal)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Password reset process initiated from outside the local network with IP: {IP}", ip);
|
_logger.LogWarning("Password reset process initiated from outside the local network with IP: {IP}", ip);
|
||||||
}
|
}
|
||||||
|
@ -693,7 +693,7 @@ public class DynamicHlsHelper
|
|||||||
// Currently we only transcode to 8 bits AV1
|
// Currently we only transcode to 8 bits AV1
|
||||||
int bitDepth = 8;
|
int bitDepth = 8;
|
||||||
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
|
||||||
&& state.VideoStream != null
|
&& state.VideoStream is not null
|
||||||
&& state.VideoStream.BitDepth.HasValue)
|
&& state.VideoStream.BitDepth.HasValue)
|
||||||
{
|
{
|
||||||
bitDepth = state.VideoStream.BitDepth.Value;
|
bitDepth = state.VideoStream.BitDepth.Value;
|
||||||
|
@ -620,7 +620,7 @@ public class TranscodingJobHelper : IDisposable
|
|||||||
state.TranscodingJob = transcodingJob;
|
state.TranscodingJob = transcodingJob;
|
||||||
|
|
||||||
// Important - don't await the log task or we won't be able to kill FFmpeg when the user stops playback
|
// Important - don't await the log task or we won't be able to kill FFmpeg when the user stops playback
|
||||||
_ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError.BaseStream, logStream);
|
_ = new JobLogger(_logger).StartStreamingLog(state, process.StandardError, logStream);
|
||||||
|
|
||||||
// Wait for the file to exist before proceeding
|
// Wait for the file to exist before proceeding
|
||||||
var ffmpegTargetFile = state.WaitForPath ?? outputPath;
|
var ffmpegTargetFile = state.WaitForPath ?? outputPath;
|
||||||
|
@ -77,7 +77,7 @@ public class CommaDelimitedArrayModelBinder : IModelBinder
|
|||||||
var typedValueIndex = 0;
|
var typedValueIndex = 0;
|
||||||
for (var i = 0; i < parsedValues.Length; i++)
|
for (var i = 0; i < parsedValues.Length; i++)
|
||||||
{
|
{
|
||||||
if (parsedValues[i] != null)
|
if (parsedValues[i] is not null)
|
||||||
{
|
{
|
||||||
typedValues.SetValue(parsedValues[i], typedValueIndex);
|
typedValues.SetValue(parsedValues[i], typedValueIndex);
|
||||||
typedValueIndex++;
|
typedValueIndex++;
|
||||||
|
@ -77,7 +77,7 @@ public class PipeDelimitedArrayModelBinder : IModelBinder
|
|||||||
var typedValueIndex = 0;
|
var typedValueIndex = 0;
|
||||||
for (var i = 0; i < parsedValues.Length; i++)
|
for (var i = 0; i < parsedValues.Length; i++)
|
||||||
{
|
{
|
||||||
if (parsedValues[i] != null)
|
if (parsedValues[i] is not null)
|
||||||
{
|
{
|
||||||
typedValues.SetValue(parsedValues[i], typedValueIndex);
|
typedValues.SetValue(parsedValues[i], typedValueIndex);
|
||||||
typedValueIndex++;
|
typedValueIndex++;
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Data.Events;
|
using Jellyfin.Data.Events;
|
||||||
|
using MediaBrowser.Controller.Authentication;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Model.Activity;
|
using MediaBrowser.Model.Activity;
|
||||||
using MediaBrowser.Model.Session;
|
using MediaBrowser.Model.Session;
|
||||||
@ -9,7 +11,7 @@ using Microsoft.Extensions.Logging;
|
|||||||
namespace Jellyfin.Api.WebSocketListeners;
|
namespace Jellyfin.Api.WebSocketListeners;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Class SessionInfoWebSocketListener.
|
/// Class ActivityLogWebSocketListener.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<ActivityLogEntry[], WebSocketListenerState>
|
public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<ActivityLogEntry[], WebSocketListenerState>
|
||||||
{
|
{
|
||||||
@ -56,6 +58,20 @@ public class ActivityLogWebSocketListener : BasePeriodicWebSocketListener<Activi
|
|||||||
base.Dispose(dispose);
|
base.Dispose(dispose);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts sending messages over an activity log web socket.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The message.</param>
|
||||||
|
protected override void Start(WebSocketMessageInfo message)
|
||||||
|
{
|
||||||
|
if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
|
||||||
|
{
|
||||||
|
throw new AuthenticationException("Only admin users can retrieve the activity log.");
|
||||||
|
}
|
||||||
|
|
||||||
|
base.Start(message);
|
||||||
|
}
|
||||||
|
|
||||||
private async void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
|
private async void OnEntryCreated(object? sender, GenericEventArgs<ActivityLogEntry> e)
|
||||||
{
|
{
|
||||||
await SendData(true).ConfigureAwait(false);
|
await SendData(true).ConfigureAwait(false);
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Jellyfin.Data.Enums;
|
||||||
|
using MediaBrowser.Controller.Authentication;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Net;
|
using MediaBrowser.Controller.Net;
|
||||||
using MediaBrowser.Controller.Session;
|
using MediaBrowser.Controller.Session;
|
||||||
@ -66,6 +68,20 @@ public class SessionInfoWebSocketListener : BasePeriodicWebSocketListener<IEnume
|
|||||||
base.Dispose(dispose);
|
base.Dispose(dispose);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts sending messages over a session info web socket.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The message.</param>
|
||||||
|
protected override void Start(WebSocketMessageInfo message)
|
||||||
|
{
|
||||||
|
if (!message.Connection.AuthorizationInfo.User.HasPermission(PermissionKind.IsAdministrator))
|
||||||
|
{
|
||||||
|
throw new AuthenticationException("Only admin users can subscribe to session information.");
|
||||||
|
}
|
||||||
|
|
||||||
|
base.Start(message);
|
||||||
|
}
|
||||||
|
|
||||||
private async void OnSessionManagerSessionActivity(object? sender, SessionEventArgs e)
|
private async void OnSessionManagerSessionActivity(object? sender, SessionEventArgs e)
|
||||||
{
|
{
|
||||||
await SendData(false).ConfigureAwait(false);
|
await SendData(false).ConfigureAwait(false);
|
||||||
|
@ -164,7 +164,7 @@ namespace Jellyfin.Networking.Configuration
|
|||||||
public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty<string>();
|
public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty<string>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the filter for remote IP connectivity. Used in conjuntion with <seealso cref="IsRemoteIPFilterBlacklist"/>.
|
/// Gets or sets the filter for remote IP connectivity. Used in conjunction with <seealso cref="IsRemoteIPFilterBlacklist"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string[] RemoteIPFilter { get; set; } = Array.Empty<string>();
|
public string[] RemoteIPFilter { get; set; } = Array.Empty<string>();
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ public static partial class NetworkExtensions
|
|||||||
Span<byte> bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? Network.IPv4MaskBytes : Network.IPv6MaskBytes];
|
Span<byte> bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? Network.IPv4MaskBytes : Network.IPv6MaskBytes];
|
||||||
if (!mask.TryWriteBytes(bytes, out var bytesWritten))
|
if (!mask.TryWriteBytes(bytes, out var bytesWritten))
|
||||||
{
|
{
|
||||||
Console.WriteLine("Unable to write address bytes, only {bytesWritten} bytes written.");
|
Console.WriteLine("Unable to write address bytes, only ${bytesWritten} bytes written.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var zeroed = false;
|
var zeroed = false;
|
||||||
|
@ -2,9 +2,8 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Events;
|
|
||||||
using MediaBrowser.Controller.Events;
|
using MediaBrowser.Controller.Events;
|
||||||
using MediaBrowser.Controller.Session;
|
using MediaBrowser.Controller.Events.Authentication;
|
||||||
using MediaBrowser.Model.Activity;
|
using MediaBrowser.Model.Activity;
|
||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@ -14,7 +13,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an entry in the activity log when there is a failed login attempt.
|
/// Creates an entry in the activity log when there is a failed login attempt.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AuthenticationFailedLogger : IEventConsumer<GenericEventArgs<AuthenticationRequest>>
|
public class AuthenticationFailedLogger : IEventConsumer<AuthenticationRequestEventArgs>
|
||||||
{
|
{
|
||||||
private readonly ILocalizationManager _localizationManager;
|
private readonly ILocalizationManager _localizationManager;
|
||||||
private readonly IActivityManager _activityManager;
|
private readonly IActivityManager _activityManager;
|
||||||
@ -31,13 +30,13 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task OnEvent(GenericEventArgs<AuthenticationRequest> eventArgs)
|
public async Task OnEvent(AuthenticationRequestEventArgs eventArgs)
|
||||||
{
|
{
|
||||||
await _activityManager.CreateAsync(new ActivityLog(
|
await _activityManager.CreateAsync(new ActivityLog(
|
||||||
string.Format(
|
string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
_localizationManager.GetLocalizedString("FailedLoginAttemptWithUserName"),
|
_localizationManager.GetLocalizedString("FailedLoginAttemptWithUserName"),
|
||||||
eventArgs.Argument.Username),
|
eventArgs.Username),
|
||||||
"AuthenticationFailed",
|
"AuthenticationFailed",
|
||||||
Guid.Empty)
|
Guid.Empty)
|
||||||
{
|
{
|
||||||
@ -45,7 +44,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security
|
|||||||
ShortOverview = string.Format(
|
ShortOverview = string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
_localizationManager.GetLocalizedString("LabelIpAddressValue"),
|
_localizationManager.GetLocalizedString("LabelIpAddressValue"),
|
||||||
eventArgs.Argument.RemoteEndPoint),
|
eventArgs.RemoteEndPoint),
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Events;
|
using Jellyfin.Data.Events;
|
||||||
using MediaBrowser.Controller.Authentication;
|
|
||||||
using MediaBrowser.Controller.Events;
|
using MediaBrowser.Controller.Events;
|
||||||
|
using MediaBrowser.Controller.Events.Authentication;
|
||||||
using MediaBrowser.Model.Activity;
|
using MediaBrowser.Model.Activity;
|
||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates an entry in the activity log when there is a successful login attempt.
|
/// Creates an entry in the activity log when there is a successful login attempt.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AuthenticationSucceededLogger : IEventConsumer<GenericEventArgs<AuthenticationResult>>
|
public class AuthenticationSucceededLogger : IEventConsumer<AuthenticationResultEventArgs>
|
||||||
{
|
{
|
||||||
private readonly ILocalizationManager _localizationManager;
|
private readonly ILocalizationManager _localizationManager;
|
||||||
private readonly IActivityManager _activityManager;
|
private readonly IActivityManager _activityManager;
|
||||||
@ -29,20 +29,20 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Security
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task OnEvent(GenericEventArgs<AuthenticationResult> eventArgs)
|
public async Task OnEvent(AuthenticationResultEventArgs eventArgs)
|
||||||
{
|
{
|
||||||
await _activityManager.CreateAsync(new ActivityLog(
|
await _activityManager.CreateAsync(new ActivityLog(
|
||||||
string.Format(
|
string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
_localizationManager.GetLocalizedString("AuthenticationSucceededWithUserName"),
|
_localizationManager.GetLocalizedString("AuthenticationSucceededWithUserName"),
|
||||||
eventArgs.Argument.User.Name),
|
eventArgs.User.Name),
|
||||||
"AuthenticationSucceeded",
|
"AuthenticationSucceeded",
|
||||||
eventArgs.Argument.User.Id)
|
eventArgs.User.Id)
|
||||||
{
|
{
|
||||||
ShortOverview = string.Format(
|
ShortOverview = string.Format(
|
||||||
CultureInfo.InvariantCulture,
|
CultureInfo.InvariantCulture,
|
||||||
_localizationManager.GetLocalizedString("LabelIpAddressValue"),
|
_localizationManager.GetLocalizedString("LabelIpAddressValue"),
|
||||||
eventArgs.Argument.SessionInfo.RemoteEndPoint),
|
eventArgs.SessionInfo?.RemoteEndPoint),
|
||||||
}).ConfigureAwait(false);
|
}).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,10 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
|
|||||||
GetItemName(eventArgs.MediaInfo),
|
GetItemName(eventArgs.MediaInfo),
|
||||||
eventArgs.DeviceName),
|
eventArgs.DeviceName),
|
||||||
GetPlaybackNotificationType(eventArgs.MediaInfo.MediaType),
|
GetPlaybackNotificationType(eventArgs.MediaInfo.MediaType),
|
||||||
user.Id))
|
user.Id)
|
||||||
|
{
|
||||||
|
ItemId = eventArgs.Item?.Id.ToString("N", CultureInfo.InvariantCulture),
|
||||||
|
})
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +73,10 @@ namespace Jellyfin.Server.Implementations.Events.Consumers.Session
|
|||||||
GetItemName(item),
|
GetItemName(item),
|
||||||
eventArgs.DeviceName),
|
eventArgs.DeviceName),
|
||||||
notificationType,
|
notificationType,
|
||||||
user.Id))
|
user.Id)
|
||||||
|
{
|
||||||
|
ItemId = eventArgs.Item?.Id.ToString("N", CultureInfo.InvariantCulture),
|
||||||
|
})
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,12 +8,11 @@ using Jellyfin.Server.Implementations.Events.Consumers.System;
|
|||||||
using Jellyfin.Server.Implementations.Events.Consumers.Updates;
|
using Jellyfin.Server.Implementations.Events.Consumers.Updates;
|
||||||
using Jellyfin.Server.Implementations.Events.Consumers.Users;
|
using Jellyfin.Server.Implementations.Events.Consumers.Users;
|
||||||
using MediaBrowser.Common.Updates;
|
using MediaBrowser.Common.Updates;
|
||||||
using MediaBrowser.Controller.Authentication;
|
|
||||||
using MediaBrowser.Controller.Events;
|
using MediaBrowser.Controller.Events;
|
||||||
|
using MediaBrowser.Controller.Events.Authentication;
|
||||||
using MediaBrowser.Controller.Events.Session;
|
using MediaBrowser.Controller.Events.Session;
|
||||||
using MediaBrowser.Controller.Events.Updates;
|
using MediaBrowser.Controller.Events.Updates;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Controller.Session;
|
|
||||||
using MediaBrowser.Controller.Subtitles;
|
using MediaBrowser.Controller.Subtitles;
|
||||||
using MediaBrowser.Model.Tasks;
|
using MediaBrowser.Model.Tasks;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@ -35,8 +34,8 @@ namespace Jellyfin.Server.Implementations.Events
|
|||||||
collection.AddScoped<IEventConsumer<SubtitleDownloadFailureEventArgs>, SubtitleDownloadFailureLogger>();
|
collection.AddScoped<IEventConsumer<SubtitleDownloadFailureEventArgs>, SubtitleDownloadFailureLogger>();
|
||||||
|
|
||||||
// Security consumers
|
// Security consumers
|
||||||
collection.AddScoped<IEventConsumer<GenericEventArgs<AuthenticationRequest>>, AuthenticationFailedLogger>();
|
collection.AddScoped<IEventConsumer<AuthenticationRequestEventArgs>, AuthenticationFailedLogger>();
|
||||||
collection.AddScoped<IEventConsumer<GenericEventArgs<AuthenticationResult>>, AuthenticationSucceededLogger>();
|
collection.AddScoped<IEventConsumer<AuthenticationResultEventArgs>, AuthenticationSucceededLogger>();
|
||||||
|
|
||||||
// Session consumers
|
// Session consumers
|
||||||
collection.AddScoped<IEventConsumer<PlaybackStartEventArgs>, PlaybackStartLogger>();
|
collection.AddScoped<IEventConsumer<PlaybackStartEventArgs>, PlaybackStartLogger>();
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using MediaBrowser.Controller.Authentication;
|
using MediaBrowser.Controller.Authentication;
|
||||||
@ -39,14 +40,18 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
// This is the version that we need to use for local users. Because reasons.
|
// This is the version that we need to use for local users. Because reasons.
|
||||||
public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser)
|
public Task<ProviderAuthenticationResult> Authenticate(string username, string password, User? resolvedUser)
|
||||||
{
|
{
|
||||||
if (resolvedUser is null)
|
[DoesNotReturn]
|
||||||
|
static void ThrowAuthenticationException()
|
||||||
{
|
{
|
||||||
throw new AuthenticationException("Specified user does not exist.");
|
throw new AuthenticationException("Invalid username or password");
|
||||||
}
|
}
|
||||||
|
|
||||||
bool success = false;
|
if (resolvedUser is null)
|
||||||
|
{
|
||||||
|
ThrowAuthenticationException();
|
||||||
|
}
|
||||||
|
|
||||||
// As long as jellyfin supports password-less users, we need this little block here to accommodate
|
// As long as jellyfin supports password-less users, we need this little block here to accommodate
|
||||||
if (!HasPassword(resolvedUser) && string.IsNullOrEmpty(password))
|
if (!HasPassword(resolvedUser) && string.IsNullOrEmpty(password))
|
||||||
@ -60,15 +65,13 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
// Handle the case when the stored password is null, but the user tried to login with a password
|
// Handle the case when the stored password is null, but the user tried to login with a password
|
||||||
if (resolvedUser.Password is null)
|
if (resolvedUser.Password is null)
|
||||||
{
|
{
|
||||||
throw new AuthenticationException("Invalid username or password");
|
ThrowAuthenticationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password);
|
PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password);
|
||||||
success = _cryptographyProvider.Verify(readyHash, password);
|
if (!_cryptographyProvider.Verify(readyHash, password))
|
||||||
|
|
||||||
if (!success)
|
|
||||||
{
|
{
|
||||||
throw new AuthenticationException("Invalid username or password");
|
ThrowAuthenticationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migrate old hashes to the new default
|
// Migrate old hashes to the new default
|
||||||
|
@ -833,7 +833,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||||||
}
|
}
|
||||||
catch (AuthenticationException ex)
|
catch (AuthenticationException ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error authenticating with provider {Provider}", provider.Name);
|
_logger.LogDebug(ex, "Error authenticating with provider {Provider}", provider.Name);
|
||||||
|
|
||||||
return (username, false);
|
return (username, false);
|
||||||
}
|
}
|
||||||
|
@ -276,7 +276,7 @@ namespace Jellyfin.Server.Extensions
|
|||||||
}
|
}
|
||||||
else if (NetworkExtensions.TryParseToSubnet(allowedProxies[i], out var subnet))
|
else if (NetworkExtensions.TryParseToSubnet(allowedProxies[i], out var subnet))
|
||||||
{
|
{
|
||||||
if (subnet != null)
|
if (subnet is not null)
|
||||||
{
|
{
|
||||||
AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength);
|
AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength);
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,6 @@ using MediaBrowser.Model.IO;
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using SQLitePCL;
|
|
||||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||||
|
|
||||||
namespace Jellyfin.Server.Helpers;
|
namespace Jellyfin.Server.Helpers;
|
||||||
@ -297,7 +296,5 @@ public static class StartupHelpers
|
|||||||
// Disable the "Expect: 100-Continue" header by default
|
// Disable the "Expect: 100-Continue" header by default
|
||||||
// http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
|
// http://stackoverflow.com/questions/566437/http-post-returns-the-error-417-expectation-failed-c
|
||||||
ServicePointManager.Expect100Continue = false;
|
ServicePointManager.Expect100Continue = false;
|
||||||
|
|
||||||
Batteries_V2.Init();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,6 @@
|
|||||||
<PackageReference Include="Serilog.Sinks.Console" />
|
<PackageReference Include="Serilog.Sinks.Console" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" />
|
<PackageReference Include="Serilog.Sinks.File" />
|
||||||
<PackageReference Include="Serilog.Sinks.Graylog" />
|
<PackageReference Include="Serilog.Sinks.Graylog" />
|
||||||
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Xml;
|
using System.Xml;
|
||||||
using System.Xml.Serialization;
|
using System.Xml.Serialization;
|
||||||
@ -59,22 +59,18 @@ public class MigrateMusicBrainzTimeout : IMigrationRoutine
|
|||||||
|
|
||||||
private OldMusicBrainzConfiguration? ReadOld(string path)
|
private OldMusicBrainzConfiguration? ReadOld(string path)
|
||||||
{
|
{
|
||||||
using (var xmlReader = XmlReader.Create(path))
|
using var xmlReader = XmlReader.Create(path);
|
||||||
{
|
|
||||||
var serverConfigSerializer = new XmlSerializer(typeof(OldMusicBrainzConfiguration), new XmlRootAttribute("PluginConfiguration"));
|
var serverConfigSerializer = new XmlSerializer(typeof(OldMusicBrainzConfiguration), new XmlRootAttribute("PluginConfiguration"));
|
||||||
return serverConfigSerializer.Deserialize(xmlReader) as OldMusicBrainzConfiguration;
|
return serverConfigSerializer.Deserialize(xmlReader) as OldMusicBrainzConfiguration;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void WriteNew(string path, PluginConfiguration newPluginConfiguration)
|
private void WriteNew(string path, PluginConfiguration newPluginConfiguration)
|
||||||
{
|
{
|
||||||
var pluginConfigurationSerializer = new XmlSerializer(typeof(PluginConfiguration), new XmlRootAttribute("PluginConfiguration"));
|
var pluginConfigurationSerializer = new XmlSerializer(typeof(PluginConfiguration), new XmlRootAttribute("PluginConfiguration"));
|
||||||
var xmlWriterSettings = new XmlWriterSettings { Indent = true };
|
var xmlWriterSettings = new XmlWriterSettings { Indent = true };
|
||||||
using (var xmlWriter = XmlWriter.Create(path, xmlWriterSettings))
|
using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings);
|
||||||
{
|
|
||||||
pluginConfigurationSerializer.Serialize(xmlWriter, newPluginConfiguration);
|
pluginConfigurationSerializer.Serialize(xmlWriter, newPluginConfiguration);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#pragma warning disable
|
#pragma warning disable
|
||||||
public sealed class OldMusicBrainzConfiguration
|
public sealed class OldMusicBrainzConfiguration
|
||||||
|
@ -43,11 +43,9 @@ public class MigrateNetworkConfiguration : IMigrationRoutine
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var xmlReader = XmlReader.Create(path))
|
using var xmlReader = XmlReader.Create(path);
|
||||||
{
|
|
||||||
oldNetworkConfiguration = (OldNetworkConfiguration?)oldNetworkConfigSerializer.Deserialize(xmlReader);
|
oldNetworkConfiguration = (OldNetworkConfiguration?)oldNetworkConfigSerializer.Deserialize(xmlReader);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch (InvalidOperationException ex)
|
catch (InvalidOperationException ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Migrate NetworkConfiguration deserialize Invalid Operation error");
|
_logger.LogError(ex, "Migrate NetworkConfiguration deserialize Invalid Operation error");
|
||||||
@ -97,12 +95,10 @@ public class MigrateNetworkConfiguration : IMigrationRoutine
|
|||||||
|
|
||||||
var networkConfigSerializer = new XmlSerializer(typeof(NetworkConfiguration));
|
var networkConfigSerializer = new XmlSerializer(typeof(NetworkConfiguration));
|
||||||
var xmlWriterSettings = new XmlWriterSettings { Indent = true };
|
var xmlWriterSettings = new XmlWriterSettings { Indent = true };
|
||||||
using (var xmlWriter = XmlWriter.Create(path, xmlWriterSettings))
|
using var xmlWriter = XmlWriter.Create(path, xmlWriterSettings);
|
||||||
{
|
|
||||||
networkConfigSerializer.Serialize(xmlWriter, networkConfiguration);
|
networkConfigSerializer.Serialize(xmlWriter, networkConfiguration);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#pragma warning disable
|
#pragma warning disable
|
||||||
public sealed class OldNetworkConfiguration
|
public sealed class OldNetworkConfiguration
|
||||||
|
@ -5,9 +5,9 @@ using Emby.Server.Implementations.Data;
|
|||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Server.Implementations;
|
using Jellyfin.Server.Implementations;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SQLitePCL.pretty;
|
|
||||||
|
|
||||||
namespace Jellyfin.Server.Migrations.Routines
|
namespace Jellyfin.Server.Migrations.Routines
|
||||||
{
|
{
|
||||||
@ -61,17 +61,15 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
};
|
};
|
||||||
|
|
||||||
var dataPath = _paths.DataPath;
|
var dataPath = _paths.DataPath;
|
||||||
using (var connection = SQLite3.Open(
|
using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
|
||||||
Path.Combine(dataPath, DbFilename),
|
|
||||||
ConnectionFlags.ReadOnly,
|
|
||||||
null))
|
|
||||||
{
|
{
|
||||||
using var userDbConnection = SQLite3.Open(Path.Combine(dataPath, "users.db"), ConnectionFlags.ReadOnly, null);
|
connection.Open();
|
||||||
|
|
||||||
|
using var userDbConnection = new SqliteConnection($"Filename={Path.Combine(dataPath, "users.db")}");
|
||||||
|
userDbConnection.Open();
|
||||||
_logger.LogWarning("Migrating the activity database may take a while, do not stop Jellyfin.");
|
_logger.LogWarning("Migrating the activity database may take a while, do not stop Jellyfin.");
|
||||||
using var dbContext = _provider.CreateDbContext();
|
using var dbContext = _provider.CreateDbContext();
|
||||||
|
|
||||||
var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id");
|
|
||||||
|
|
||||||
// Make sure that the database is empty in case of failed migration due to power outages, etc.
|
// Make sure that the database is empty in case of failed migration due to power outages, etc.
|
||||||
dbContext.ActivityLogs.RemoveRange(dbContext.ActivityLogs);
|
dbContext.ActivityLogs.RemoveRange(dbContext.ActivityLogs);
|
||||||
dbContext.SaveChanges();
|
dbContext.SaveChanges();
|
||||||
@ -81,51 +79,52 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
|
|
||||||
var newEntries = new List<ActivityLog>();
|
var newEntries = new List<ActivityLog>();
|
||||||
|
|
||||||
|
var queryResult = connection.Query("SELECT * FROM ActivityLog ORDER BY Id");
|
||||||
|
|
||||||
foreach (var entry in queryResult)
|
foreach (var entry in queryResult)
|
||||||
{
|
{
|
||||||
if (!logLevelDictionary.TryGetValue(entry[8].ToString(), out var severity))
|
if (!logLevelDictionary.TryGetValue(entry.GetString(8), out var severity))
|
||||||
{
|
{
|
||||||
severity = LogLevel.Trace;
|
severity = LogLevel.Trace;
|
||||||
}
|
}
|
||||||
|
|
||||||
var guid = Guid.Empty;
|
var guid = Guid.Empty;
|
||||||
if (entry[6].SQLiteType != SQLiteType.Null && !Guid.TryParse(entry[6].ToString(), out guid))
|
if (!entry.IsDBNull(6) && !entry.TryGetGuid(6, out guid))
|
||||||
{
|
{
|
||||||
|
var id = entry.GetString(6);
|
||||||
// This is not a valid Guid, see if it is an internal ID from an old Emby schema
|
// This is not a valid Guid, see if it is an internal ID from an old Emby schema
|
||||||
_logger.LogWarning("Invalid Guid in UserId column: {Guid}", entry[6].ToString());
|
_logger.LogWarning("Invalid Guid in UserId column: {Guid}", id);
|
||||||
|
|
||||||
using var statement = userDbConnection.PrepareStatement("SELECT guid FROM LocalUsersv2 WHERE Id=@Id");
|
using var statement = userDbConnection.PrepareStatement("SELECT guid FROM LocalUsersv2 WHERE Id=@Id");
|
||||||
statement.TryBind("@Id", entry[6].ToString());
|
statement.TryBind("@Id", id);
|
||||||
|
|
||||||
foreach (var row in statement.Query())
|
using var reader = statement.ExecuteReader();
|
||||||
{
|
if (reader.HasRows && reader.Read() && reader.TryGetGuid(0, out guid))
|
||||||
if (row.Count > 0 && Guid.TryParse(row[0].ToString(), out guid))
|
|
||||||
{
|
{
|
||||||
// Successfully parsed a Guid from the user table.
|
// Successfully parsed a Guid from the user table.
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var newEntry = new ActivityLog(entry[1].ToString(), entry[4].ToString(), guid)
|
var newEntry = new ActivityLog(entry.GetString(1), entry.GetString(4), guid)
|
||||||
{
|
{
|
||||||
DateCreated = entry[7].ReadDateTime(),
|
DateCreated = entry.GetDateTime(7),
|
||||||
LogSeverity = severity
|
LogSeverity = severity
|
||||||
};
|
};
|
||||||
|
|
||||||
if (entry[2].SQLiteType != SQLiteType.Null)
|
if (entry.TryGetString(2, out var result))
|
||||||
{
|
{
|
||||||
newEntry.Overview = entry[2].ToString();
|
newEntry.Overview = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry[3].SQLiteType != SQLiteType.Null)
|
if (entry.TryGetString(3, out result))
|
||||||
{
|
{
|
||||||
newEntry.ShortOverview = entry[3].ToString();
|
newEntry.ShortOverview = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry[5].SQLiteType != SQLiteType.Null)
|
if (entry.TryGetString(5, out result))
|
||||||
{
|
{
|
||||||
newEntry.ItemId = entry[5].ToString();
|
newEntry.ItemId = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
newEntries.Add(newEntry);
|
newEntries.Add(newEntry);
|
||||||
|
@ -6,9 +6,9 @@ using Jellyfin.Data.Entities.Security;
|
|||||||
using Jellyfin.Server.Implementations;
|
using Jellyfin.Server.Implementations;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SQLitePCL.pretty;
|
|
||||||
|
|
||||||
namespace Jellyfin.Server.Migrations.Routines
|
namespace Jellyfin.Server.Migrations.Routines
|
||||||
{
|
{
|
||||||
@ -56,34 +56,32 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
public void Perform()
|
public void Perform()
|
||||||
{
|
{
|
||||||
var dataPath = _appPaths.DataPath;
|
var dataPath = _appPaths.DataPath;
|
||||||
using (var connection = SQLite3.Open(
|
using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
|
||||||
Path.Combine(dataPath, DbFilename),
|
|
||||||
ConnectionFlags.ReadOnly,
|
|
||||||
null))
|
|
||||||
{
|
{
|
||||||
|
connection.Open();
|
||||||
using var dbContext = _dbProvider.CreateDbContext();
|
using var dbContext = _dbProvider.CreateDbContext();
|
||||||
|
|
||||||
var authenticatedDevices = connection.Query("SELECT * FROM Tokens");
|
var authenticatedDevices = connection.Query("SELECT * FROM Tokens");
|
||||||
|
|
||||||
foreach (var row in authenticatedDevices)
|
foreach (var row in authenticatedDevices)
|
||||||
{
|
{
|
||||||
var dateCreatedStr = row[9].ToString();
|
var dateCreatedStr = row.GetString(9);
|
||||||
_ = DateTime.TryParse(dateCreatedStr, out var dateCreated);
|
_ = DateTime.TryParse(dateCreatedStr, out var dateCreated);
|
||||||
var dateLastActivityStr = row[10].ToString();
|
var dateLastActivityStr = row.GetString(10);
|
||||||
_ = DateTime.TryParse(dateLastActivityStr, out var dateLastActivity);
|
_ = DateTime.TryParse(dateLastActivityStr, out var dateLastActivity);
|
||||||
|
|
||||||
if (row[6].IsDbNull())
|
if (row.IsDBNull(6))
|
||||||
{
|
{
|
||||||
dbContext.ApiKeys.Add(new ApiKey(row[3].ToString())
|
dbContext.ApiKeys.Add(new ApiKey(row.GetString(3))
|
||||||
{
|
{
|
||||||
AccessToken = row[1].ToString(),
|
AccessToken = row.GetString(1),
|
||||||
DateCreated = dateCreated,
|
DateCreated = dateCreated,
|
||||||
DateLastActivity = dateLastActivity
|
DateLastActivity = dateLastActivity
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var userId = new Guid(row[6].ToString());
|
var userId = row.GetGuid(6);
|
||||||
var user = _userManager.GetUserById(userId);
|
var user = _userManager.GetUserById(userId);
|
||||||
if (user is null)
|
if (user is null)
|
||||||
{
|
{
|
||||||
@ -92,14 +90,14 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
}
|
}
|
||||||
|
|
||||||
dbContext.Devices.Add(new Device(
|
dbContext.Devices.Add(new Device(
|
||||||
new Guid(row[6].ToString()),
|
userId,
|
||||||
row[3].ToString(),
|
row.GetString(3),
|
||||||
row[4].ToString(),
|
row.GetString(4),
|
||||||
row[5].ToString(),
|
row.GetString(5),
|
||||||
row[2].ToString())
|
row.GetString(2))
|
||||||
{
|
{
|
||||||
AccessToken = row[1].ToString(),
|
AccessToken = row.GetString(1),
|
||||||
IsActive = row[8].ToBool(),
|
IsActive = row.GetBoolean(8),
|
||||||
DateCreated = dateCreated,
|
DateCreated = dateCreated,
|
||||||
DateLastActivity = dateLastActivity
|
DateLastActivity = dateLastActivity
|
||||||
});
|
});
|
||||||
@ -110,12 +108,12 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
var deviceIds = new HashSet<string>();
|
var deviceIds = new HashSet<string>();
|
||||||
foreach (var row in deviceOptions)
|
foreach (var row in deviceOptions)
|
||||||
{
|
{
|
||||||
if (row[2].IsDbNull())
|
if (row.IsDBNull(2))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var deviceId = row[2].ToString();
|
var deviceId = row.GetString(2);
|
||||||
if (deviceIds.Contains(deviceId))
|
if (deviceIds.Contains(deviceId))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@ -125,7 +123,7 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
|
|
||||||
dbContext.DeviceOptions.Add(new DeviceOptions(deviceId)
|
dbContext.DeviceOptions.Add(new DeviceOptions(deviceId)
|
||||||
{
|
{
|
||||||
CustomName = row[1].IsDbNull() ? null : row[1].ToString()
|
CustomName = row.IsDBNull(1) ? null : row.GetString(1)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,15 +4,16 @@ using System.IO;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Emby.Server.Implementations.Data;
|
||||||
using Jellyfin.Data.Entities;
|
using Jellyfin.Data.Entities;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using Jellyfin.Server.Implementations;
|
using Jellyfin.Server.Implementations;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Library;
|
using MediaBrowser.Controller.Library;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SQLitePCL.pretty;
|
|
||||||
|
|
||||||
namespace Jellyfin.Server.Migrations.Routines
|
namespace Jellyfin.Server.Migrations.Routines
|
||||||
{
|
{
|
||||||
@ -83,22 +84,23 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
var displayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var displayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
var customDisplayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var customDisplayPrefs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
|
var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
|
||||||
using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null))
|
using (var connection = new SqliteConnection($"Filename={dbFilePath}"))
|
||||||
{
|
{
|
||||||
|
connection.Open();
|
||||||
using var dbContext = _provider.CreateDbContext();
|
using var dbContext = _provider.CreateDbContext();
|
||||||
|
|
||||||
var results = connection.Query("SELECT * FROM userdisplaypreferences");
|
var results = connection.Query("SELECT * FROM userdisplaypreferences");
|
||||||
foreach (var result in results)
|
foreach (var result in results)
|
||||||
{
|
{
|
||||||
var dto = JsonSerializer.Deserialize<DisplayPreferencesDto>(result[3].ToBlob(), _jsonOptions);
|
var dto = JsonSerializer.Deserialize<DisplayPreferencesDto>(result.GetStream(3), _jsonOptions);
|
||||||
if (dto is null)
|
if (dto is null)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var itemId = new Guid(result[1].ToBlob());
|
var itemId = result.GetGuid(1);
|
||||||
var dtoUserId = new Guid(result[1].ToBlob());
|
var dtoUserId = itemId;
|
||||||
var client = result[2].ToString();
|
var client = result.GetString(2);
|
||||||
var displayPreferencesKey = $"{dtoUserId}|{itemId}|{client}";
|
var displayPreferencesKey = $"{dtoUserId}|{itemId}|{client}";
|
||||||
if (displayPrefs.Contains(displayPreferencesKey))
|
if (displayPrefs.Contains(displayPreferencesKey))
|
||||||
{
|
{
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
|
||||||
using Emby.Server.Implementations.Data;
|
using Emby.Server.Implementations.Data;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
using MediaBrowser.Controller.Persistence;
|
using MediaBrowser.Controller.Persistence;
|
||||||
using MediaBrowser.Model.Globalization;
|
using MediaBrowser.Model.Globalization;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SQLitePCL.pretty;
|
|
||||||
|
|
||||||
namespace Jellyfin.Server.Migrations.Routines
|
namespace Jellyfin.Server.Migrations.Routines
|
||||||
{
|
{
|
||||||
@ -20,17 +19,14 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
private readonly ILogger<MigrateRatingLevels> _logger;
|
private readonly ILogger<MigrateRatingLevels> _logger;
|
||||||
private readonly IServerApplicationPaths _applicationPaths;
|
private readonly IServerApplicationPaths _applicationPaths;
|
||||||
private readonly ILocalizationManager _localizationManager;
|
private readonly ILocalizationManager _localizationManager;
|
||||||
private readonly IItemRepository _repository;
|
|
||||||
|
|
||||||
public MigrateRatingLevels(
|
public MigrateRatingLevels(
|
||||||
IServerApplicationPaths applicationPaths,
|
IServerApplicationPaths applicationPaths,
|
||||||
ILoggerFactory loggerFactory,
|
ILoggerFactory loggerFactory,
|
||||||
ILocalizationManager localizationManager,
|
ILocalizationManager localizationManager)
|
||||||
IItemRepository repository)
|
|
||||||
{
|
{
|
||||||
_applicationPaths = applicationPaths;
|
_applicationPaths = applicationPaths;
|
||||||
_localizationManager = localizationManager;
|
_localizationManager = localizationManager;
|
||||||
_repository = repository;
|
|
||||||
_logger = loggerFactory.CreateLogger<MigrateRatingLevels>();
|
_logger = loggerFactory.CreateLogger<MigrateRatingLevels>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,15 +66,14 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
|
|
||||||
// Migrate parental rating strings to new levels
|
// Migrate parental rating strings to new levels
|
||||||
_logger.LogInformation("Recalculating parental rating levels based on rating string.");
|
_logger.LogInformation("Recalculating parental rating levels based on rating string.");
|
||||||
using (var connection = SQLite3.Open(
|
using var connection = new SqliteConnection($"Filename={dbPath}");
|
||||||
dbPath,
|
connection.Open();
|
||||||
ConnectionFlags.ReadWrite,
|
using (var transaction = connection.BeginTransaction())
|
||||||
null))
|
|
||||||
{
|
{
|
||||||
var queryResult = connection.Query("SELECT DISTINCT OfficialRating FROM TypedBaseItems");
|
var queryResult = connection.Query("SELECT DISTINCT OfficialRating FROM TypedBaseItems");
|
||||||
foreach (var entry in queryResult)
|
foreach (var entry in queryResult)
|
||||||
{
|
{
|
||||||
var ratingString = entry[0].ToString();
|
var ratingString = entry.GetString(0);
|
||||||
if (string.IsNullOrEmpty(ratingString))
|
if (string.IsNullOrEmpty(ratingString))
|
||||||
{
|
{
|
||||||
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating IS NULL OR OfficialRating='';");
|
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating IS NULL OR OfficialRating='';");
|
||||||
@ -91,12 +86,14 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
ratingValue = "NULL";
|
ratingValue = "NULL";
|
||||||
}
|
}
|
||||||
|
|
||||||
var statement = connection.PrepareStatement("UPDATE TypedBaseItems SET InheritedParentalRatingValue = @Value WHERE OfficialRating = @Rating;");
|
using var statement = connection.PrepareStatement("UPDATE TypedBaseItems SET InheritedParentalRatingValue = @Value WHERE OfficialRating = @Rating;");
|
||||||
statement.TryBind("@Value", ratingValue);
|
statement.TryBind("@Value", ratingValue);
|
||||||
statement.TryBind("@Rating", ratingString);
|
statement.TryBind("@Rating", ratingString);
|
||||||
statement.ExecuteQuery();
|
statement.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,9 +11,9 @@ using MediaBrowser.Controller.Entities;
|
|||||||
using MediaBrowser.Model.Configuration;
|
using MediaBrowser.Model.Configuration;
|
||||||
using MediaBrowser.Model.Serialization;
|
using MediaBrowser.Model.Serialization;
|
||||||
using MediaBrowser.Model.Users;
|
using MediaBrowser.Model.Users;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SQLitePCL.pretty;
|
|
||||||
using JsonSerializer = System.Text.Json.JsonSerializer;
|
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||||
|
|
||||||
namespace Jellyfin.Server.Migrations.Routines
|
namespace Jellyfin.Server.Migrations.Routines
|
||||||
@ -64,8 +64,9 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
var dataPath = _paths.DataPath;
|
var dataPath = _paths.DataPath;
|
||||||
_logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin.");
|
_logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin.");
|
||||||
|
|
||||||
using (var connection = SQLite3.Open(Path.Combine(dataPath, DbFilename), ConnectionFlags.ReadOnly, null))
|
using (var connection = new SqliteConnection($"Filename={Path.Combine(dataPath, DbFilename)}"))
|
||||||
{
|
{
|
||||||
|
connection.Open();
|
||||||
var dbContext = _provider.CreateDbContext();
|
var dbContext = _provider.CreateDbContext();
|
||||||
|
|
||||||
var queryResult = connection.Query("SELECT * FROM LocalUsersv2");
|
var queryResult = connection.Query("SELECT * FROM LocalUsersv2");
|
||||||
@ -75,7 +76,7 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
|
|
||||||
foreach (var entry in queryResult)
|
foreach (var entry in queryResult)
|
||||||
{
|
{
|
||||||
UserMockup? mockup = JsonSerializer.Deserialize<UserMockup>(entry[2].ToBlob(), JsonDefaults.Options);
|
UserMockup? mockup = JsonSerializer.Deserialize<UserMockup>(entry.GetStream(2), JsonDefaults.Options);
|
||||||
if (mockup is null)
|
if (mockup is null)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@ -108,8 +109,8 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
|
|
||||||
var user = new User(mockup.Name, policy.AuthenticationProviderId!, policy.PasswordResetProviderId!)
|
var user = new User(mockup.Name, policy.AuthenticationProviderId!, policy.PasswordResetProviderId!)
|
||||||
{
|
{
|
||||||
Id = entry[1].ReadGuidFromBlob(),
|
Id = entry.GetGuid(1),
|
||||||
InternalId = entry[0].ToInt64(),
|
InternalId = entry.GetInt64(0),
|
||||||
MaxParentalAgeRating = policy.MaxParentalRating,
|
MaxParentalAgeRating = policy.MaxParentalRating,
|
||||||
EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess,
|
EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess,
|
||||||
RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit,
|
RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit,
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using Emby.Server.Implementations.Data;
|
||||||
using MediaBrowser.Controller;
|
using MediaBrowser.Controller;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using SQLitePCL.pretty;
|
|
||||||
|
|
||||||
namespace Jellyfin.Server.Migrations.Routines
|
namespace Jellyfin.Server.Migrations.Routines
|
||||||
{
|
{
|
||||||
@ -37,14 +38,13 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
{
|
{
|
||||||
var dataPath = _paths.DataPath;
|
var dataPath = _paths.DataPath;
|
||||||
var dbPath = Path.Combine(dataPath, DbFilename);
|
var dbPath = Path.Combine(dataPath, DbFilename);
|
||||||
using (var connection = SQLite3.Open(
|
using var connection = new SqliteConnection($"Filename={dbPath}");
|
||||||
dbPath,
|
connection.Open();
|
||||||
ConnectionFlags.ReadWrite,
|
using (var transaction = connection.BeginTransaction())
|
||||||
null))
|
|
||||||
{
|
{
|
||||||
// Query the database for the ids of duplicate extras
|
// Query the database for the ids of duplicate extras
|
||||||
var queryResult = connection.Query("SELECT t1.Path FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video'");
|
var queryResult = connection.Query("SELECT t1.Path FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video'");
|
||||||
var bads = string.Join(", ", queryResult.SelectScalarString());
|
var bads = string.Join(", ", queryResult.Select(x => x.GetString(0)));
|
||||||
|
|
||||||
// Do nothing if no duplicate extras were detected
|
// Do nothing if no duplicate extras were detected
|
||||||
if (bads.Length == 0)
|
if (bads.Length == 0)
|
||||||
@ -76,6 +76,7 @@ namespace Jellyfin.Server.Migrations.Routines
|
|||||||
// Delete all duplicate extras
|
// Delete all duplicate extras
|
||||||
_logger.LogInformation("Removing found duplicated extras for the following items: {DuplicateExtras}", bads);
|
_logger.LogInformation("Removing found duplicated extras for the following items: {DuplicateExtras}", bads);
|
||||||
connection.Execute("DELETE FROM TypedBaseItems WHERE rowid IN (SELECT t1.rowid FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video')");
|
connection.Execute("DELETE FROM TypedBaseItems WHERE rowid IN (SELECT t1.rowid FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video')");
|
||||||
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -23,7 +21,7 @@ namespace MediaBrowser.Controller.Authentication
|
|||||||
|
|
||||||
public interface IRequiresResolvedUser
|
public interface IRequiresResolvedUser
|
||||||
{
|
{
|
||||||
Task<ProviderAuthenticationResult> Authenticate(string username, string password, User resolvedUser);
|
Task<ProviderAuthenticationResult> Authenticate(string username, string password, User? resolvedUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IHasNewUserPolicy
|
public interface IHasNewUserPolicy
|
||||||
@ -33,8 +31,8 @@ namespace MediaBrowser.Controller.Authentication
|
|||||||
|
|
||||||
public class ProviderAuthenticationResult
|
public class ProviderAuthenticationResult
|
||||||
{
|
{
|
||||||
public string Username { get; set; }
|
public required string Username { get; set; }
|
||||||
|
|
||||||
public string DisplayName { get; set; }
|
public string? DisplayName { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@ namespace MediaBrowser.Controller.Drawing
|
|||||||
/// <returns>Guid.</returns>
|
/// <returns>Guid.</returns>
|
||||||
string GetImageCacheTag(BaseItem item, ItemImageInfo image);
|
string GetImageCacheTag(BaseItem item, ItemImageInfo image);
|
||||||
|
|
||||||
string GetImageCacheTag(BaseItem item, ChapterInfo chapter);
|
string? GetImageCacheTag(BaseItem item, ChapterInfo chapter);
|
||||||
|
|
||||||
string? GetImageCacheTag(User user);
|
string? GetImageCacheTag(User user);
|
||||||
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using MediaBrowser.Controller.Entities;
|
using MediaBrowser.Controller.Entities;
|
||||||
@ -9,12 +7,12 @@ namespace MediaBrowser.Controller.Drawing
|
|||||||
{
|
{
|
||||||
public static class ImageProcessorExtensions
|
public static class ImageProcessorExtensions
|
||||||
{
|
{
|
||||||
public static string GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType)
|
public static string? GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType)
|
||||||
{
|
{
|
||||||
return processor.GetImageCacheTag(item, imageType, 0);
|
return processor.GetImageCacheTag(item, imageType, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType, int imageIndex)
|
public static string? GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType, int imageIndex)
|
||||||
{
|
{
|
||||||
var imageInfo = item.GetImageInfo(imageType, imageIndex);
|
var imageInfo = item.GetImageInfo(imageType, imageIndex);
|
||||||
|
|
||||||
|
@ -183,6 +183,9 @@ namespace MediaBrowser.Controller.Entities.Audio
|
|||||||
progress.Report(percent * 95);
|
progress.Report(percent * 95);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get album LUFS
|
||||||
|
LUFS = items.OfType<Audio>().Max(item => item.LUFS);
|
||||||
|
|
||||||
var parentRefreshOptions = refreshOptions;
|
var parentRefreshOptions = refreshOptions;
|
||||||
if (childUpdateType > ItemUpdateType.None)
|
if (childUpdateType > ItemUpdateType.None)
|
||||||
{
|
{
|
||||||
|
@ -1864,7 +1864,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
/// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops.</exception>
|
/// <exception cref="ArgumentException">Backdrops should be accessed using Item.Backdrops.</exception>
|
||||||
public bool HasImage(ImageType type, int imageIndex)
|
public bool HasImage(ImageType type, int imageIndex)
|
||||||
{
|
{
|
||||||
return GetImageInfo(type, imageIndex) != null;
|
return GetImageInfo(type, imageIndex) is not null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetImage(ItemImageInfo image, int index)
|
public void SetImage(ItemImageInfo image, int index)
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@ -14,7 +12,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
/// Gets or sets the path.
|
/// Gets or sets the path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The path.</value>
|
/// <value>The path.</value>
|
||||||
public string Path { get; set; }
|
public required string Path { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the type.
|
/// Gets or sets the type.
|
||||||
@ -36,9 +34,9 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
/// Gets or sets the blurhash.
|
/// Gets or sets the blurhash.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The blurhash.</value>
|
/// <value>The blurhash.</value>
|
||||||
public string BlurHash { get; set; }
|
public string? BlurHash { get; set; }
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool IsLocalFile => Path is null || !Path.StartsWith("http", StringComparison.OrdinalIgnoreCase);
|
public bool IsLocalFile => !Path.StartsWith("http", StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -99,7 +99,7 @@ namespace MediaBrowser.Controller.Entities.TV
|
|||||||
}
|
}
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public bool IsInSeasonFolder => FindParent<Season>() != null;
|
public bool IsInSeasonFolder => FindParent<Season>() is not null;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public string SeriesPresentationUniqueKey { get; set; }
|
public string SeriesPresentationUniqueKey { get; set; }
|
||||||
|
@ -333,7 +333,7 @@ namespace MediaBrowser.Controller.Entities
|
|||||||
|
|
||||||
protected override bool IsActiveRecording()
|
protected override bool IsActiveRecording()
|
||||||
{
|
{
|
||||||
return LiveTvManager.GetActiveRecordingInfo(Path) != null;
|
return LiveTvManager.GetActiveRecordingInfo(Path) is not null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override bool CanDelete()
|
public override bool CanDelete()
|
||||||
|
@ -0,0 +1,60 @@
|
|||||||
|
using System;
|
||||||
|
using MediaBrowser.Controller.Session;
|
||||||
|
|
||||||
|
namespace MediaBrowser.Controller.Events.Authentication;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A class representing an authentication result event.
|
||||||
|
/// </summary>
|
||||||
|
public class AuthenticationRequestEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AuthenticationRequestEventArgs"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="request">The <see cref="AuthenticationRequest"/>.</param>
|
||||||
|
public AuthenticationRequestEventArgs(AuthenticationRequest request)
|
||||||
|
{
|
||||||
|
Username = request.Username;
|
||||||
|
UserId = request.UserId;
|
||||||
|
App = request.App;
|
||||||
|
AppVersion = request.AppVersion;
|
||||||
|
DeviceId = request.DeviceId;
|
||||||
|
DeviceName = request.DeviceName;
|
||||||
|
RemoteEndPoint = request.RemoteEndPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the user name.
|
||||||
|
/// </summary>
|
||||||
|
public string? Username { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the user id.
|
||||||
|
/// </summary>
|
||||||
|
public Guid? UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the app.
|
||||||
|
/// </summary>
|
||||||
|
public string? App { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the app version.
|
||||||
|
/// </summary>
|
||||||
|
public string? AppVersion { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the device id.
|
||||||
|
/// </summary>
|
||||||
|
public string? DeviceId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the device name.
|
||||||
|
/// </summary>
|
||||||
|
public string? DeviceName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the remote endpoint.
|
||||||
|
/// </summary>
|
||||||
|
public string? RemoteEndPoint { get; set; }
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
using System;
|
||||||
|
using MediaBrowser.Controller.Authentication;
|
||||||
|
using MediaBrowser.Controller.Session;
|
||||||
|
using MediaBrowser.Model.Dto;
|
||||||
|
|
||||||
|
namespace MediaBrowser.Controller.Events.Authentication;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A class representing an authentication result event.
|
||||||
|
/// </summary>
|
||||||
|
public class AuthenticationResultEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="AuthenticationResultEventArgs"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="result">The <see cref="AuthenticationResult"/>.</param>
|
||||||
|
public AuthenticationResultEventArgs(AuthenticationResult result)
|
||||||
|
{
|
||||||
|
User = result.User;
|
||||||
|
SessionInfo = result.SessionInfo;
|
||||||
|
ServerId = result.ServerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the user.
|
||||||
|
/// </summary>
|
||||||
|
public UserDto User { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the session information.
|
||||||
|
/// </summary>
|
||||||
|
public SessionInfo? SessionInfo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the server id.
|
||||||
|
/// </summary>
|
||||||
|
public string? ServerId { get; set; }
|
||||||
|
}
|
@ -217,7 +217,7 @@ namespace MediaBrowser.Controller.Library
|
|||||||
/// <returns><c>true</c> if [contains file system entry by name] [the specified name]; otherwise, <c>false</c>.</returns>
|
/// <returns><c>true</c> if [contains file system entry by name] [the specified name]; otherwise, <c>false</c>.</returns>
|
||||||
public bool ContainsFileSystemEntryByName(string name)
|
public bool ContainsFileSystemEntryByName(string name)
|
||||||
{
|
{
|
||||||
return GetFileSystemEntryByName(name) != null;
|
return GetFileSystemEntryByName(name) is not null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetCollectionType()
|
public string GetCollectionType()
|
||||||
|
@ -37,7 +37,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
private readonly IMediaEncoder _mediaEncoder;
|
private readonly IMediaEncoder _mediaEncoder;
|
||||||
private readonly ISubtitleEncoder _subtitleEncoder;
|
private readonly ISubtitleEncoder _subtitleEncoder;
|
||||||
private readonly IConfiguration _config;
|
private readonly IConfiguration _config;
|
||||||
private readonly Version _minKernelVersionAmdVkFmtModifier = new Version(5, 15);
|
private readonly IConfigurationManager _configurationManager;
|
||||||
|
|
||||||
// i915 hang was fixed by linux 6.2 (3f882f2)
|
// i915 hang was fixed by linux 6.2 (3f882f2)
|
||||||
private readonly Version _minKerneli915Hang = new Version(5, 18);
|
private readonly Version _minKerneli915Hang = new Version(5, 18);
|
||||||
private readonly Version _maxKerneli915Hang = new Version(6, 1, 3);
|
private readonly Version _maxKerneli915Hang = new Version(6, 1, 3);
|
||||||
@ -112,12 +113,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
IApplicationPaths appPaths,
|
IApplicationPaths appPaths,
|
||||||
IMediaEncoder mediaEncoder,
|
IMediaEncoder mediaEncoder,
|
||||||
ISubtitleEncoder subtitleEncoder,
|
ISubtitleEncoder subtitleEncoder,
|
||||||
IConfiguration config)
|
IConfiguration config,
|
||||||
|
IConfigurationManager configurationManager)
|
||||||
{
|
{
|
||||||
_appPaths = appPaths;
|
_appPaths = appPaths;
|
||||||
_mediaEncoder = mediaEncoder;
|
_mediaEncoder = mediaEncoder;
|
||||||
_subtitleEncoder = subtitleEncoder;
|
_subtitleEncoder = subtitleEncoder;
|
||||||
_config = config;
|
_config = config;
|
||||||
|
_configurationManager = configurationManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
[GeneratedRegex(@"\s+")]
|
[GeneratedRegex(@"\s+")]
|
||||||
@ -891,9 +894,11 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
}
|
}
|
||||||
else if (_mediaEncoder.IsVaapiDeviceAmd)
|
else if (_mediaEncoder.IsVaapiDeviceAmd)
|
||||||
{
|
{
|
||||||
|
// Disable AMD EFC feature since it's still unstable in upstream Mesa.
|
||||||
|
Environment.SetEnvironmentVariable("AMD_DEBUG", "noefc");
|
||||||
|
|
||||||
if (IsVulkanFullSupported()
|
if (IsVulkanFullSupported()
|
||||||
&& _mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier
|
&& _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop)
|
||||||
&& Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
|
|
||||||
{
|
{
|
||||||
args.Append(GetDrmDeviceArgs(options.VaapiDevice, DrmAlias));
|
args.Append(GetDrmDeviceArgs(options.VaapiDevice, DrmAlias));
|
||||||
args.Append(GetVaapiDeviceArgs(null, null, null, DrmAlias, VaapiAlias));
|
args.Append(GetVaapiDeviceArgs(null, null, null, DrmAlias, VaapiAlias));
|
||||||
@ -1056,7 +1061,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay)
|
if (state.MediaSource.VideoType == VideoType.Dvd || state.MediaSource.VideoType == VideoType.BluRay)
|
||||||
{
|
{
|
||||||
var tmpConcatPath = Path.Join(options.TranscodingTempPath, state.MediaSource.Id + ".concat");
|
var tmpConcatPath = Path.Join(_configurationManager.GetTranscodePath(), state.MediaSource.Id + ".concat");
|
||||||
_mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath);
|
_mediaEncoder.GenerateConcatConfig(state.MediaSource, tmpConcatPath);
|
||||||
arg.Append(" -f concat -safe 0 -i ")
|
arg.Append(" -f concat -safe 0 -i ")
|
||||||
.Append(tmpConcatPath);
|
.Append(tmpConcatPath);
|
||||||
@ -1211,6 +1216,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
int bitrate = state.OutputVideoBitrate.Value;
|
int bitrate = state.OutputVideoBitrate.Value;
|
||||||
|
|
||||||
|
// Bit rate under 1000k is not allowed in h264_qsv
|
||||||
|
if (string.Equals(videoCodec, "h264_qsv", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
bitrate = Math.Max(bitrate, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
// Currently use the same buffer size for all encoders
|
// Currently use the same buffer size for all encoders
|
||||||
int bufsize = bitrate * 2;
|
int bufsize = bitrate * 2;
|
||||||
|
|
||||||
@ -1905,7 +1916,9 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
|
|
||||||
if (!string.IsNullOrEmpty(profile))
|
if (!string.IsNullOrEmpty(profile))
|
||||||
{
|
{
|
||||||
if (!string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase))
|
// Currently there's no profile option in av1_nvenc encoder
|
||||||
|
if (!(string.Equals(videoEncoder, "av1_nvenc", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(videoEncoder, "h264_v4l2m2m", StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
param += " -profile:v:0 " + profile;
|
param += " -profile:v:0 " + profile;
|
||||||
}
|
}
|
||||||
@ -2690,7 +2703,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
string args = string.Empty;
|
string args = string.Empty;
|
||||||
|
|
||||||
// http://ffmpeg.org/ffmpeg-all.html#toc-Complex-filtergraphs-1
|
// http://ffmpeg.org/ffmpeg-all.html#toc-Complex-filtergraphs-1
|
||||||
if (state.VideoStream != null && videoProcessFilters.Contains("-filter_complex", StringComparison.Ordinal))
|
if (state.VideoStream is not null && videoProcessFilters.Contains("-filter_complex", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
int videoStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.VideoStream);
|
int videoStreamIndex = FindIndex(state.MediaSource.MediaStreams, state.VideoStream);
|
||||||
|
|
||||||
@ -4205,14 +4218,13 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
// prefered vaapi + vulkan filters pipeline
|
// prefered vaapi + vulkan filters pipeline
|
||||||
if (_mediaEncoder.IsVaapiDeviceAmd
|
if (_mediaEncoder.IsVaapiDeviceAmd
|
||||||
&& isVaapiVkSupported
|
&& isVaapiVkSupported
|
||||||
&& _mediaEncoder.IsVaapiDeviceSupportVulkanFmtModifier
|
&& _mediaEncoder.IsVaapiDeviceSupportVulkanDrmInterop)
|
||||||
&& Environment.OSVersion.Version >= _minKernelVersionAmdVkFmtModifier)
|
|
||||||
{
|
{
|
||||||
// AMD radeonsi path(Vega/gfx9+, kernel>=5.15), with extra vulkan tonemap and overlay support.
|
// AMD radeonsi path(targeting Polaris/gfx8+), with extra vulkan tonemap and overlay support.
|
||||||
return GetAmdVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
|
return GetAmdVaapiFullVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intel i965 and Amd radeonsi/r600 path(Polaris/gfx8-), only featuring scale and deinterlace support.
|
// Intel i965 and Amd legacy driver path, only featuring scale and deinterlace support.
|
||||||
return GetVaapiLimitedVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
|
return GetVaapiLimitedVidFiltersPrefered(state, options, vidDecoder, vidEncoder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4484,7 +4496,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
// INPUT vaapi surface(vram)
|
// INPUT vaapi surface(vram)
|
||||||
if (doVkTonemap || hasSubs)
|
if (doVkTonemap || hasSubs)
|
||||||
{
|
{
|
||||||
// map from vaapi to vulkan/drm via interop (Vega/gfx9+).
|
// map from vaapi to vulkan/drm via interop (Polaris/gfx8+).
|
||||||
mainFilters.Add("hwmap=derive_device=vulkan");
|
mainFilters.Add("hwmap=derive_device=vulkan");
|
||||||
mainFilters.Add("format=vulkan");
|
mainFilters.Add("format=vulkan");
|
||||||
}
|
}
|
||||||
@ -4513,9 +4525,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
if (doVkTonemap && !hasSubs)
|
if (doVkTonemap && !hasSubs)
|
||||||
{
|
{
|
||||||
// OUTPUT vaapi(nv12) surface(vram)
|
// OUTPUT vaapi(nv12) surface(vram)
|
||||||
// map from vulkan/drm to vaapi via interop (Vega/gfx9+).
|
// map from vulkan/drm to vaapi via interop (Polaris/gfx8+).
|
||||||
mainFilters.Add("hwmap=derive_device=drm");
|
|
||||||
mainFilters.Add("format=drm_prime");
|
|
||||||
mainFilters.Add("hwmap=derive_device=vaapi");
|
mainFilters.Add("hwmap=derive_device=vaapi");
|
||||||
mainFilters.Add("format=vaapi");
|
mainFilters.Add("format=vaapi");
|
||||||
|
|
||||||
@ -4581,9 +4591,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
else if (isVaapiEncoder)
|
else if (isVaapiEncoder)
|
||||||
{
|
{
|
||||||
// OUTPUT vaapi(nv12) surface(vram)
|
// OUTPUT vaapi(nv12) surface(vram)
|
||||||
// map from vulkan/drm to vaapi via interop (Vega/gfx9+).
|
// map from vulkan/drm to vaapi via interop (Polaris/gfx8+).
|
||||||
overlayFilters.Add("hwmap=derive_device=drm");
|
|
||||||
overlayFilters.Add("format=drm_prime");
|
|
||||||
overlayFilters.Add("hwmap=derive_device=vaapi");
|
overlayFilters.Add("hwmap=derive_device=vaapi");
|
||||||
overlayFilters.Add("format=vaapi");
|
overlayFilters.Add("format=vaapi");
|
||||||
|
|
||||||
|
@ -64,8 +64,8 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets a value indicating whether the configured Vaapi device supports vulkan drm format modifier.
|
/// Gets a value indicating whether the configured Vaapi device supports vulkan drm format modifier.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value><c>true</c> if the Vaapi device supports vulkan drm format modifier, <c>false</c> otherwise.</value>
|
/// <value><c>true</c> if the Vaapi device supports vulkan drm interop, <c>false</c> otherwise.</value>
|
||||||
bool IsVaapiDeviceSupportVulkanFmtModifier { get; }
|
bool IsVaapiDeviceSupportVulkanDrmInterop { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether given encoder codec is supported.
|
/// Whether given encoder codec is supported.
|
||||||
|
@ -20,12 +20,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StartStreamingLog(EncodingJobInfo state, Stream source, Stream target)
|
public async Task StartStreamingLog(EncodingJobInfo state, StreamReader reader, Stream target)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (target)
|
using (target)
|
||||||
using (var reader = new StreamReader(source))
|
using (reader)
|
||||||
{
|
{
|
||||||
while (!reader.EndOfStream && reader.BaseStream.CanRead)
|
while (!reader.EndOfStream && reader.BaseStream.CanRead)
|
||||||
{
|
{
|
||||||
|
@ -96,7 +96,7 @@ namespace MediaBrowser.Controller.Net
|
|||||||
/// Starts sending messages over a web socket.
|
/// Starts sending messages over a web socket.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="message">The message.</param>
|
/// <param name="message">The message.</param>
|
||||||
private void Start(WebSocketMessageInfo message)
|
protected virtual void Start(WebSocketMessageInfo message)
|
||||||
{
|
{
|
||||||
var vals = message.Data.Split(',');
|
var vals = message.Data.Split(',');
|
||||||
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
#pragma warning disable CS1591
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Net.WebSockets;
|
using System.Net.WebSockets;
|
||||||
@ -9,6 +7,9 @@ using MediaBrowser.Controller.Net.WebSocketMessages;
|
|||||||
|
|
||||||
namespace MediaBrowser.Controller.Net
|
namespace MediaBrowser.Controller.Net
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Interface for WebSocket connections.
|
||||||
|
/// </summary>
|
||||||
public interface IWebSocketConnection : IAsyncDisposable, IDisposable
|
public interface IWebSocketConnection : IAsyncDisposable, IDisposable
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -40,6 +41,11 @@ namespace MediaBrowser.Controller.Net
|
|||||||
/// <value>The state.</value>
|
/// <value>The state.</value>
|
||||||
WebSocketState State { get; }
|
WebSocketState State { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the authorization information.
|
||||||
|
/// </summary>
|
||||||
|
public AuthorizationInfo AuthorizationInfo { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the remote end point.
|
/// Gets the remote end point.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -65,6 +71,11 @@ namespace MediaBrowser.Controller.Net
|
|||||||
/// <exception cref="ArgumentNullException">The message is null.</exception>
|
/// <exception cref="ArgumentNullException">The message is null.</exception>
|
||||||
Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken);
|
Task SendAsync<T>(OutboundWebSocketMessage<T> message, CancellationToken cancellationToken);
|
||||||
|
|
||||||
Task ProcessAsync(CancellationToken cancellationToken = default);
|
/// <summary>
|
||||||
|
/// Receives a message asynchronously.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">The cancellation token.</param>
|
||||||
|
/// <returns>Task.</returns>
|
||||||
|
Task ReceiveAsync(CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
#nullable enable
|
using System.Collections.Generic;
|
||||||
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace MediaBrowser.Controller.Security
|
namespace MediaBrowser.Controller.Security
|
||||||
|
@ -553,7 +553,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
|
|
||||||
private string GetProcessOutput(string path, string arguments, bool readStdErr, string? testKey)
|
private string GetProcessOutput(string path, string arguments, bool readStdErr, string? testKey)
|
||||||
{
|
{
|
||||||
using (var process = new Process()
|
var redirectStandardIn = !string.IsNullOrEmpty(testKey);
|
||||||
|
using (var process = new Process
|
||||||
{
|
{
|
||||||
StartInfo = new ProcessStartInfo(path, arguments)
|
StartInfo = new ProcessStartInfo(path, arguments)
|
||||||
{
|
{
|
||||||
@ -561,7 +562,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
WindowStyle = ProcessWindowStyle.Hidden,
|
WindowStyle = ProcessWindowStyle.Hidden,
|
||||||
ErrorDialog = false,
|
ErrorDialog = false,
|
||||||
RedirectStandardInput = !string.IsNullOrEmpty(testKey),
|
RedirectStandardInput = redirectStandardIn,
|
||||||
RedirectStandardOutput = true,
|
RedirectStandardOutput = true,
|
||||||
RedirectStandardError = true
|
RedirectStandardError = true
|
||||||
}
|
}
|
||||||
@ -571,12 +572,14 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
|
|
||||||
process.Start();
|
process.Start();
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(testKey))
|
if (redirectStandardIn)
|
||||||
{
|
{
|
||||||
process.StandardInput.Write(testKey);
|
using var writer = process.StandardInput;
|
||||||
|
writer.Write(testKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
return readStdErr ? process.StandardError.ReadToEnd() : process.StandardOutput.ReadToEnd();
|
using var reader = readStdErr ? process.StandardError : process.StandardOutput;
|
||||||
|
return reader.ReadToEnd();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -76,12 +76,10 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
private bool _isVaapiDeviceAmd = false;
|
private bool _isVaapiDeviceAmd = false;
|
||||||
private bool _isVaapiDeviceInteliHD = false;
|
private bool _isVaapiDeviceInteliHD = false;
|
||||||
private bool _isVaapiDeviceInteli965 = false;
|
private bool _isVaapiDeviceInteli965 = false;
|
||||||
private bool _isVaapiDeviceSupportVulkanFmtModifier = false;
|
private bool _isVaapiDeviceSupportVulkanDrmInterop = false;
|
||||||
|
|
||||||
private static string[] _vulkanFmtModifierExts =
|
private static string[] _vulkanExternalMemoryDmaBufExts =
|
||||||
{
|
{
|
||||||
"VK_KHR_sampler_ycbcr_conversion",
|
|
||||||
"VK_EXT_image_drm_format_modifier",
|
|
||||||
"VK_KHR_external_memory_fd",
|
"VK_KHR_external_memory_fd",
|
||||||
"VK_EXT_external_memory_dma_buf",
|
"VK_EXT_external_memory_dma_buf",
|
||||||
"VK_KHR_external_semaphore_fd",
|
"VK_KHR_external_semaphore_fd",
|
||||||
@ -140,7 +138,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
public bool IsVaapiDeviceInteli965 => _isVaapiDeviceInteli965;
|
public bool IsVaapiDeviceInteli965 => _isVaapiDeviceInteli965;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public bool IsVaapiDeviceSupportVulkanFmtModifier => _isVaapiDeviceSupportVulkanFmtModifier;
|
public bool IsVaapiDeviceSupportVulkanDrmInterop => _isVaapiDeviceSupportVulkanDrmInterop;
|
||||||
|
|
||||||
[GeneratedRegex(@"[^\/\\]+?(\.[^\/\\\n.]+)?$")]
|
[GeneratedRegex(@"[^\/\\]+?(\.[^\/\\\n.]+)?$")]
|
||||||
private static partial Regex FfprobePathRegex();
|
private static partial Regex FfprobePathRegex();
|
||||||
@ -204,7 +202,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
_isVaapiDeviceAmd = validator.CheckVaapiDeviceByDriverName("Mesa Gallium driver", options.VaapiDevice);
|
_isVaapiDeviceAmd = validator.CheckVaapiDeviceByDriverName("Mesa Gallium driver", options.VaapiDevice);
|
||||||
_isVaapiDeviceInteliHD = validator.CheckVaapiDeviceByDriverName("Intel iHD driver", options.VaapiDevice);
|
_isVaapiDeviceInteliHD = validator.CheckVaapiDeviceByDriverName("Intel iHD driver", options.VaapiDevice);
|
||||||
_isVaapiDeviceInteli965 = validator.CheckVaapiDeviceByDriverName("Intel i965 driver", options.VaapiDevice);
|
_isVaapiDeviceInteli965 = validator.CheckVaapiDeviceByDriverName("Intel i965 driver", options.VaapiDevice);
|
||||||
_isVaapiDeviceSupportVulkanFmtModifier = validator.CheckVulkanDrmDeviceByExtensionName(options.VaapiDevice, _vulkanFmtModifierExts);
|
_isVaapiDeviceSupportVulkanDrmInterop = validator.CheckVulkanDrmDeviceByExtensionName(options.VaapiDevice, _vulkanExternalMemoryDmaBufExts);
|
||||||
|
|
||||||
if (_isVaapiDeviceAmd)
|
if (_isVaapiDeviceAmd)
|
||||||
{
|
{
|
||||||
@ -219,9 +217,9 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
_logger.LogInformation("VAAPI device {RenderNodePath} is Intel GPU (i965)", options.VaapiDevice);
|
_logger.LogInformation("VAAPI device {RenderNodePath} is Intel GPU (i965)", options.VaapiDevice);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_isVaapiDeviceSupportVulkanFmtModifier)
|
if (_isVaapiDeviceSupportVulkanDrmInterop)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM format modifier", options.VaapiDevice);
|
_logger.LogInformation("VAAPI device {RenderNodePath} supports Vulkan DRM interop", options.VaapiDevice);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -513,7 +511,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
|||||||
using (var processWrapper = new ProcessWrapper(process, this))
|
using (var processWrapper = new ProcessWrapper(process, this))
|
||||||
{
|
{
|
||||||
StartProcess(processWrapper);
|
StartProcess(processWrapper);
|
||||||
await process.StandardOutput.BaseStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
|
using var reader = process.StandardOutput;
|
||||||
|
await reader.BaseStream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false);
|
||||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||||
InternalMediaInfoResult result;
|
InternalMediaInfoResult result;
|
||||||
try
|
try
|
||||||
|
@ -762,9 +762,11 @@ namespace MediaBrowser.MediaEncoding.Probing
|
|||||||
&& !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase);
|
&& !string.Equals(streamInfo.FieldOrder, "progressive", StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
if (isAudio
|
if (isAudio
|
||||||
|| string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)
|
&& (string.Equals(stream.Codec, "bmp", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
|
|| string.Equals(stream.Codec, "gif", StringComparison.OrdinalIgnoreCase)
|
||||||
|| string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase))
|
|| string.Equals(stream.Codec, "mjpeg", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(stream.Codec, "png", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| string.Equals(stream.Codec, "webp", StringComparison.OrdinalIgnoreCase)))
|
||||||
{
|
{
|
||||||
stream.Type = MediaStreamType.EmbeddedImage;
|
stream.Type = MediaStreamType.EmbeddedImage;
|
||||||
}
|
}
|
||||||
|
@ -293,7 +293,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(format, SubtitleFormat.VTT, StringComparison.OrdinalIgnoreCase) || string.Equals(format, SubtitleFormat.WEBVTT, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
value = new VttWriter();
|
value = new VttWriter();
|
||||||
return true;
|
return true;
|
||||||
|
@ -314,7 +314,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
/// <param name="audioSampleRate">The audio sample rate.</param>
|
/// <param name="audioSampleRate">The audio sample rate.</param>
|
||||||
/// <param name="audioBitDepth">The audio bit depth.</param>
|
/// <param name="audioBitDepth">The audio bit depth.</param>
|
||||||
/// <returns>The <see cref="ResponseProfile"/>.</returns>
|
/// <returns>The <see cref="ResponseProfile"/>.</returns>
|
||||||
public ResponseProfile? GetAudioMediaProfile(string container, string? audioCodec, int? audioChannels, int? audioBitrate, int? audioSampleRate, int? audioBitDepth)
|
public ResponseProfile? GetAudioMediaProfile(string? container, string? audioCodec, int? audioChannels, int? audioBitrate, int? audioSampleRate, int? audioBitDepth)
|
||||||
{
|
{
|
||||||
foreach (var i in ResponseProfiles)
|
foreach (var i in ResponseProfiles)
|
||||||
{
|
{
|
||||||
@ -438,14 +438,14 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
/// <param name="isAvc">True if Avc.</param>
|
/// <param name="isAvc">True if Avc.</param>
|
||||||
/// <returns>The <see cref="ResponseProfile"/>.</returns>
|
/// <returns>The <see cref="ResponseProfile"/>.</returns>
|
||||||
public ResponseProfile? GetVideoMediaProfile(
|
public ResponseProfile? GetVideoMediaProfile(
|
||||||
string container,
|
string? container,
|
||||||
string? audioCodec,
|
string? audioCodec,
|
||||||
string? videoCodec,
|
string? videoCodec,
|
||||||
int? width,
|
int? width,
|
||||||
int? height,
|
int? height,
|
||||||
int? bitDepth,
|
int? bitDepth,
|
||||||
int? videoBitrate,
|
int? videoBitrate,
|
||||||
string videoProfile,
|
string? videoProfile,
|
||||||
VideoRangeType videoRangeType,
|
VideoRangeType videoRangeType,
|
||||||
double? videoLevel,
|
double? videoLevel,
|
||||||
float? videoFramerate,
|
float? videoFramerate,
|
||||||
@ -456,7 +456,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
int? refFrames,
|
int? refFrames,
|
||||||
int? numVideoStreams,
|
int? numVideoStreams,
|
||||||
int? numAudioStreams,
|
int? numAudioStreams,
|
||||||
string videoCodecTag,
|
string? videoCodecTag,
|
||||||
bool? isAvc)
|
bool? isAvc)
|
||||||
{
|
{
|
||||||
foreach (var i in ResponseProfiles)
|
foreach (var i in ResponseProfiles)
|
||||||
|
@ -135,7 +135,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transcodingProfile != null)
|
if (transcodingProfile is not null)
|
||||||
{
|
{
|
||||||
if (!item.SupportsTranscoding)
|
if (!item.SupportsTranscoding)
|
||||||
{
|
{
|
||||||
@ -179,15 +179,9 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
{
|
{
|
||||||
ValidateMediaOptions(options, true);
|
ValidateMediaOptions(options, true);
|
||||||
|
|
||||||
var mediaSources = new List<MediaSourceInfo>();
|
var mediaSources = string.IsNullOrEmpty(options.MediaSourceId)
|
||||||
foreach (var mediaSourceInfo in options.MediaSources)
|
? options.MediaSources
|
||||||
{
|
: options.MediaSources.Where(x => string.Equals(x.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase));
|
||||||
if (string.IsNullOrEmpty(options.MediaSourceId)
|
|
||||||
|| string.Equals(mediaSourceInfo.Id, options.MediaSourceId, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
mediaSources.Add(mediaSourceInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var streams = new List<StreamInfo>();
|
var streams = new List<StreamInfo>();
|
||||||
foreach (var mediaSourceInfo in mediaSources)
|
foreach (var mediaSourceInfo in mediaSources)
|
||||||
@ -216,7 +210,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
return streams.OrderBy(i =>
|
return streams.OrderBy(i =>
|
||||||
{
|
{
|
||||||
// Nothing beats direct playing a file
|
// Nothing beats direct playing a file
|
||||||
if (i.PlayMethod == PlayMethod.DirectPlay && i.MediaSource.Protocol == MediaProtocol.File)
|
if (i.PlayMethod == PlayMethod.DirectPlay && i.MediaSource?.Protocol == MediaProtocol.File)
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -235,7 +229,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
}
|
}
|
||||||
}).ThenBy(i =>
|
}).ThenBy(i =>
|
||||||
{
|
{
|
||||||
switch (i.MediaSource.Protocol)
|
switch (i.MediaSource?.Protocol)
|
||||||
{
|
{
|
||||||
case MediaProtocol.File:
|
case MediaProtocol.File:
|
||||||
return 0;
|
return 0;
|
||||||
@ -246,7 +240,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
{
|
{
|
||||||
if (maxBitrate > 0)
|
if (maxBitrate > 0)
|
||||||
{
|
{
|
||||||
if (i.MediaSource.Bitrate.HasValue)
|
if (i.MediaSource?.Bitrate is not null)
|
||||||
{
|
{
|
||||||
return Math.Abs(i.MediaSource.Bitrate.Value - maxBitrate);
|
return Math.Abs(i.MediaSource.Bitrate.Value - maxBitrate);
|
||||||
}
|
}
|
||||||
@ -585,10 +579,10 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
MediaSource = item,
|
MediaSource = item,
|
||||||
RunTimeTicks = item.RunTimeTicks,
|
RunTimeTicks = item.RunTimeTicks,
|
||||||
Context = options.Context,
|
Context = options.Context,
|
||||||
DeviceProfile = options.Profile
|
DeviceProfile = options.Profile,
|
||||||
|
SubtitleStreamIndex = options.SubtitleStreamIndex ?? GetDefaultSubtitleStreamIndex(item, options.Profile.SubtitleProfiles)
|
||||||
};
|
};
|
||||||
|
|
||||||
playlistItem.SubtitleStreamIndex = options.SubtitleStreamIndex ?? GetDefaultSubtitleStreamIndex(item, options.Profile.SubtitleProfiles);
|
|
||||||
var subtitleStream = playlistItem.SubtitleStreamIndex.HasValue ? item.GetMediaStream(MediaStreamType.Subtitle, playlistItem.SubtitleStreamIndex.Value) : null;
|
var subtitleStream = playlistItem.SubtitleStreamIndex.HasValue ? item.GetMediaStream(MediaStreamType.Subtitle, playlistItem.SubtitleStreamIndex.Value) : null;
|
||||||
|
|
||||||
var audioStream = item.GetDefaultAudioStream(options.AudioStreamIndex ?? item.DefaultAudioStreamIndex);
|
var audioStream = item.GetDefaultAudioStream(options.AudioStreamIndex ?? item.DefaultAudioStreamIndex);
|
||||||
@ -659,7 +653,8 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
if (audioStreamIndex.HasValue)
|
if (audioStreamIndex.HasValue)
|
||||||
{
|
{
|
||||||
playlistItem.AudioStreamIndex = audioStreamIndex;
|
playlistItem.AudioStreamIndex = audioStreamIndex;
|
||||||
playlistItem.AudioCodecs = new[] { item.GetMediaStream(MediaStreamType.Audio, audioStreamIndex.Value)?.Codec };
|
var audioCodec = item.GetMediaStream(MediaStreamType.Audio, audioStreamIndex.Value)?.Codec;
|
||||||
|
playlistItem.AudioCodecs = audioCodec is null ? Array.Empty<string>() : new[] { audioCodec };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (directPlay == PlayMethod.DirectStream)
|
else if (directPlay == PlayMethod.DirectStream)
|
||||||
@ -759,7 +754,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
{
|
{
|
||||||
// prefer direct copy profile
|
// prefer direct copy profile
|
||||||
float videoFramerate = videoStream?.AverageFrameRate ?? videoStream?.RealFrameRate ?? 0;
|
float videoFramerate = videoStream?.AverageFrameRate ?? videoStream?.RealFrameRate ?? 0;
|
||||||
TransportStreamTimestamp? timestamp = videoStream == null ? TransportStreamTimestamp.None : item.Timestamp;
|
TransportStreamTimestamp? timestamp = videoStream is null ? TransportStreamTimestamp.None : item.Timestamp;
|
||||||
int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio);
|
int? numAudioStreams = item.GetStreamCount(MediaStreamType.Audio);
|
||||||
int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video);
|
int? numVideoStreams = item.GetStreamCount(MediaStreamType.Video);
|
||||||
|
|
||||||
@ -842,7 +837,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
|
|
||||||
if (videoStream is not null && videoStream.Level != 0)
|
if (videoStream is not null && videoStream.Level != 0)
|
||||||
{
|
{
|
||||||
playlistItem.SetOption(qualifier, "level", videoStream.Level.ToString());
|
playlistItem.SetOption(qualifier, "level", videoStream.Level.ToString() ?? string.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prefer matching audio codecs, could do better here
|
// Prefer matching audio codecs, could do better here
|
||||||
@ -871,7 +866,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
|
|
||||||
// Copy matching audio codec options
|
// Copy matching audio codec options
|
||||||
playlistItem.AudioSampleRate = audioStream.SampleRate;
|
playlistItem.AudioSampleRate = audioStream.SampleRate;
|
||||||
playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels.ToString());
|
playlistItem.SetOption(qualifier, "audiochannels", audioStream.Channels.ToString() ?? string.Empty);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(audioStream.Profile))
|
if (!string.IsNullOrEmpty(audioStream.Profile))
|
||||||
{
|
{
|
||||||
@ -880,7 +875,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
|
|
||||||
if (audioStream.Level != 0)
|
if (audioStream.Level != 0)
|
||||||
{
|
{
|
||||||
playlistItem.SetOption(audioStream.Codec, "level", audioStream.Level.ToString());
|
playlistItem.SetOption(audioStream.Codec, "level", audioStream.Level.ToString() ?? string.Empty);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
#nullable disable
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using Jellyfin.Data.Enums;
|
using Jellyfin.Data.Enums;
|
||||||
using MediaBrowser.Model.Drawing;
|
using MediaBrowser.Model.Drawing;
|
||||||
using MediaBrowser.Model.Dto;
|
using MediaBrowser.Model.Dto;
|
||||||
@ -34,9 +34,9 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
|
|
||||||
public DlnaProfileType MediaType { get; set; }
|
public DlnaProfileType MediaType { get; set; }
|
||||||
|
|
||||||
public string Container { get; set; }
|
public string? Container { get; set; }
|
||||||
|
|
||||||
public string SubProtocol { get; set; }
|
public string? SubProtocol { get; set; }
|
||||||
|
|
||||||
public long StartPositionTicks { get; set; }
|
public long StartPositionTicks { get; set; }
|
||||||
|
|
||||||
@ -80,11 +80,11 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
|
|
||||||
public float? MaxFramerate { get; set; }
|
public float? MaxFramerate { get; set; }
|
||||||
|
|
||||||
public DeviceProfile DeviceProfile { get; set; }
|
public required DeviceProfile DeviceProfile { get; set; }
|
||||||
|
|
||||||
public string DeviceProfileId { get; set; }
|
public string? DeviceProfileId { get; set; }
|
||||||
|
|
||||||
public string DeviceId { get; set; }
|
public string? DeviceId { get; set; }
|
||||||
|
|
||||||
public long? RunTimeTicks { get; set; }
|
public long? RunTimeTicks { get; set; }
|
||||||
|
|
||||||
@ -92,21 +92,21 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
|
|
||||||
public bool EstimateContentLength { get; set; }
|
public bool EstimateContentLength { get; set; }
|
||||||
|
|
||||||
public MediaSourceInfo MediaSource { get; set; }
|
public MediaSourceInfo? MediaSource { get; set; }
|
||||||
|
|
||||||
public string[] SubtitleCodecs { get; set; }
|
public string[] SubtitleCodecs { get; set; }
|
||||||
|
|
||||||
public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; }
|
public SubtitleDeliveryMethod SubtitleDeliveryMethod { get; set; }
|
||||||
|
|
||||||
public string SubtitleFormat { get; set; }
|
public string? SubtitleFormat { get; set; }
|
||||||
|
|
||||||
public string PlaySessionId { get; set; }
|
public string? PlaySessionId { get; set; }
|
||||||
|
|
||||||
public TranscodeReason TranscodeReasons { get; set; }
|
public TranscodeReason TranscodeReasons { get; set; }
|
||||||
|
|
||||||
public Dictionary<string, string> StreamOptions { get; private set; }
|
public Dictionary<string, string> StreamOptions { get; private set; }
|
||||||
|
|
||||||
public string MediaSourceId => MediaSource?.Id;
|
public string? MediaSourceId => MediaSource?.Id;
|
||||||
|
|
||||||
public bool IsDirectStream => MediaSource?.VideoType is not (VideoType.Dvd or VideoType.BluRay)
|
public bool IsDirectStream => MediaSource?.VideoType is not (VideoType.Dvd or VideoType.BluRay)
|
||||||
&& PlayMethod is PlayMethod.DirectStream or PlayMethod.DirectPlay;
|
&& PlayMethod is PlayMethod.DirectStream or PlayMethod.DirectPlay;
|
||||||
@ -114,12 +114,12 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the audio stream that will be used.
|
/// Gets the audio stream that will be used.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MediaStream TargetAudioStream => MediaSource?.GetDefaultAudioStream(AudioStreamIndex);
|
public MediaStream? TargetAudioStream => MediaSource?.GetDefaultAudioStream(AudioStreamIndex);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the video stream that will be used.
|
/// Gets the video stream that will be used.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public MediaStream TargetVideoStream => MediaSource?.VideoStream;
|
public MediaStream? TargetVideoStream => MediaSource?.VideoStream;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the audio sample rate that will be in the output stream.
|
/// Gets the audio sample rate that will be in the output stream.
|
||||||
@ -259,7 +259,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the audio sample rate that will be in the output stream.
|
/// Gets the audio sample rate that will be in the output stream.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string TargetVideoProfile
|
public string? TargetVideoProfile
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
@ -307,7 +307,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
/// Gets the target video codec tag.
|
/// Gets the target video codec tag.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The target video codec tag.</value>
|
/// <value>The target video codec tag.</value>
|
||||||
public string TargetVideoCodecTag
|
public string? TargetVideoCodecTag
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
@ -364,7 +364,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
{
|
{
|
||||||
var stream = TargetAudioStream;
|
var stream = TargetAudioStream;
|
||||||
|
|
||||||
string inputCodec = stream?.Codec;
|
string? inputCodec = stream?.Codec;
|
||||||
|
|
||||||
if (IsDirectStream)
|
if (IsDirectStream)
|
||||||
{
|
{
|
||||||
@ -389,7 +389,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
{
|
{
|
||||||
var stream = TargetVideoStream;
|
var stream = TargetVideoStream;
|
||||||
|
|
||||||
string inputCodec = stream?.Codec;
|
string? inputCodec = stream?.Codec;
|
||||||
|
|
||||||
if (IsDirectStream)
|
if (IsDirectStream)
|
||||||
{
|
{
|
||||||
@ -417,7 +417,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
{
|
{
|
||||||
if (IsDirectStream)
|
if (IsDirectStream)
|
||||||
{
|
{
|
||||||
return MediaSource.Size;
|
return MediaSource?.Size;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RunTimeTicks.HasValue)
|
if (RunTimeTicks.HasValue)
|
||||||
@ -580,7 +580,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetOption(string qualifier, string name, string value)
|
public void SetOption(string? qualifier, string name, string value)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(qualifier))
|
if (string.IsNullOrEmpty(qualifier))
|
||||||
{
|
{
|
||||||
@ -597,7 +597,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
StreamOptions[name] = value;
|
StreamOptions[name] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetOption(string qualifier, string name)
|
public string? GetOption(string? qualifier, string name)
|
||||||
{
|
{
|
||||||
var value = GetOption(qualifier + "-" + name);
|
var value = GetOption(qualifier + "-" + name);
|
||||||
|
|
||||||
@ -609,7 +609,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetOption(string name)
|
public string? GetOption(string name)
|
||||||
{
|
{
|
||||||
if (StreamOptions.TryGetValue(name, out var value))
|
if (StreamOptions.TryGetValue(name, out var value))
|
||||||
{
|
{
|
||||||
@ -619,7 +619,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ToUrl(string baseUrl, string accessToken)
|
public string ToUrl(string baseUrl, string? accessToken)
|
||||||
{
|
{
|
||||||
ArgumentException.ThrowIfNullOrEmpty(baseUrl);
|
ArgumentException.ThrowIfNullOrEmpty(baseUrl);
|
||||||
|
|
||||||
@ -686,7 +686,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
|
return string.Format(CultureInfo.InvariantCulture, "{0}/videos/{1}/stream{2}?{3}", baseUrl, ItemId, extension, queryString);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IEnumerable<NameValuePair> BuildParams(StreamInfo item, string accessToken)
|
private static IEnumerable<NameValuePair> BuildParams(StreamInfo item, string? accessToken)
|
||||||
{
|
{
|
||||||
var list = new List<NameValuePair>();
|
var list = new List<NameValuePair>();
|
||||||
|
|
||||||
@ -730,7 +730,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty));
|
list.Add(new NameValuePair("PlaySessionId", item.PlaySessionId ?? string.Empty));
|
||||||
list.Add(new NameValuePair("api_key", accessToken ?? string.Empty));
|
list.Add(new NameValuePair("api_key", accessToken ?? string.Empty));
|
||||||
|
|
||||||
string liveStreamId = item.MediaSource?.LiveStreamId;
|
string? liveStreamId = item.MediaSource?.LiveStreamId;
|
||||||
list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty));
|
list.Add(new NameValuePair("LiveStreamId", liveStreamId ?? string.Empty));
|
||||||
|
|
||||||
list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty));
|
list.Add(new NameValuePair("SubtitleMethod", item.SubtitleStreamIndex.HasValue && item.SubtitleDeliveryMethod != SubtitleDeliveryMethod.External ? item.SubtitleDeliveryMethod.ToString() : string.Empty));
|
||||||
@ -772,7 +772,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
|
list.Add(new NameValuePair("RequireAvc", item.RequireAvc.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()));
|
||||||
}
|
}
|
||||||
|
|
||||||
list.Add(new NameValuePair("Tag", item.MediaSource.ETag ?? string.Empty));
|
list.Add(new NameValuePair("Tag", item.MediaSource?.ETag ?? string.Empty));
|
||||||
|
|
||||||
string subtitleCodecs = item.SubtitleCodecs.Length == 0 ?
|
string subtitleCodecs = item.SubtitleCodecs.Length == 0 ?
|
||||||
string.Empty :
|
string.Empty :
|
||||||
@ -816,13 +816,18 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string accessToken)
|
public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, string baseUrl, string? accessToken)
|
||||||
{
|
{
|
||||||
return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken);
|
return GetSubtitleProfiles(transcoderSupport, includeSelectedTrackOnly, false, baseUrl, accessToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string accessToken)
|
public IEnumerable<SubtitleStreamInfo> GetSubtitleProfiles(ITranscoderSupport transcoderSupport, bool includeSelectedTrackOnly, bool enableAllProfiles, string baseUrl, string? accessToken)
|
||||||
{
|
{
|
||||||
|
if (MediaSource is null)
|
||||||
|
{
|
||||||
|
return Enumerable.Empty<SubtitleStreamInfo>();
|
||||||
|
}
|
||||||
|
|
||||||
var list = new List<SubtitleStreamInfo>();
|
var list = new List<SubtitleStreamInfo>();
|
||||||
|
|
||||||
// HLS will preserve timestamps so we can just grab the full subtitle stream
|
// HLS will preserve timestamps so we can just grab the full subtitle stream
|
||||||
@ -856,27 +861,36 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddSubtitleProfiles(List<SubtitleStreamInfo> list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string accessToken, long startPositionTicks)
|
private void AddSubtitleProfiles(List<SubtitleStreamInfo> list, MediaStream stream, ITranscoderSupport transcoderSupport, bool enableAllProfiles, string baseUrl, string? accessToken, long startPositionTicks)
|
||||||
{
|
{
|
||||||
if (enableAllProfiles)
|
if (enableAllProfiles)
|
||||||
{
|
{
|
||||||
foreach (var profile in DeviceProfile.SubtitleProfiles)
|
foreach (var profile in DeviceProfile.SubtitleProfiles)
|
||||||
{
|
{
|
||||||
var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport);
|
var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, new[] { profile }, transcoderSupport);
|
||||||
|
if (info is not null)
|
||||||
|
{
|
||||||
list.Add(info);
|
list.Add(info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport);
|
var info = GetSubtitleStreamInfo(stream, baseUrl, accessToken, startPositionTicks, DeviceProfile.SubtitleProfiles, transcoderSupport);
|
||||||
|
if (info is not null)
|
||||||
|
{
|
||||||
list.Add(info);
|
list.Add(info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private SubtitleStreamInfo GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport)
|
private SubtitleStreamInfo? GetSubtitleStreamInfo(MediaStream stream, string baseUrl, string? accessToken, long startPositionTicks, SubtitleProfile[] subtitleProfiles, ITranscoderSupport transcoderSupport)
|
||||||
{
|
{
|
||||||
|
if (MediaSource is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol);
|
var subtitleProfile = StreamBuilder.GetSubtitleProfile(MediaSource, stream, subtitleProfiles, PlayMethod, transcoderSupport, Container, SubProtocol);
|
||||||
var info = new SubtitleStreamInfo
|
var info = new SubtitleStreamInfo
|
||||||
{
|
{
|
||||||
@ -920,7 +934,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int? GetTargetVideoBitDepth(string codec)
|
public int? GetTargetVideoBitDepth(string? codec)
|
||||||
{
|
{
|
||||||
var value = GetOption(codec, "videobitdepth");
|
var value = GetOption(codec, "videobitdepth");
|
||||||
|
|
||||||
@ -932,7 +946,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int? GetTargetAudioBitDepth(string codec)
|
public int? GetTargetAudioBitDepth(string? codec)
|
||||||
{
|
{
|
||||||
var value = GetOption(codec, "audiobitdepth");
|
var value = GetOption(codec, "audiobitdepth");
|
||||||
|
|
||||||
@ -944,7 +958,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public double? GetTargetVideoLevel(string codec)
|
public double? GetTargetVideoLevel(string? codec)
|
||||||
{
|
{
|
||||||
var value = GetOption(codec, "level");
|
var value = GetOption(codec, "level");
|
||||||
|
|
||||||
@ -956,7 +970,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int? GetTargetRefFrames(string codec)
|
public int? GetTargetRefFrames(string? codec)
|
||||||
{
|
{
|
||||||
var value = GetOption(codec, "maxrefframes");
|
var value = GetOption(codec, "maxrefframes");
|
||||||
|
|
||||||
@ -968,7 +982,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int? GetTargetAudioChannels(string codec)
|
public int? GetTargetAudioChannels(string? codec)
|
||||||
{
|
{
|
||||||
var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels;
|
var defaultValue = GlobalMaxAudioChannels ?? TranscodingMaxAudioChannels;
|
||||||
|
|
||||||
@ -988,7 +1002,7 @@ namespace MediaBrowser.Model.Dlna
|
|||||||
|
|
||||||
private int? GetMediaStreamCount(MediaStreamType type, int limit)
|
private int? GetMediaStreamCount(MediaStreamType type, int limit)
|
||||||
{
|
{
|
||||||
var count = MediaSource.GetStreamCount(type);
|
var count = MediaSource?.GetStreamCount(type);
|
||||||
|
|
||||||
if (count.HasValue)
|
if (count.HasValue)
|
||||||
{
|
{
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
#nullable disable
|
|
||||||
#pragma warning disable CS1591
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
@ -20,16 +19,16 @@ namespace MediaBrowser.Model.Entities
|
|||||||
/// Gets or sets the name.
|
/// Gets or sets the name.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The name.</value>
|
/// <value>The name.</value>
|
||||||
public string Name { get; set; }
|
public string? Name { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the image path.
|
/// Gets or sets the image path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <value>The image path.</value>
|
/// <value>The image path.</value>
|
||||||
public string ImagePath { get; set; }
|
public string? ImagePath { get; set; }
|
||||||
|
|
||||||
public DateTime ImageDateModified { get; set; }
|
public DateTime ImageDateModified { get; set; }
|
||||||
|
|
||||||
public string ImageTag { get; set; }
|
public string? ImageTag { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ namespace MediaBrowser.Model.MediaInfo
|
|||||||
public const string SSA = "ssa";
|
public const string SSA = "ssa";
|
||||||
public const string ASS = "ass";
|
public const string ASS = "ass";
|
||||||
public const string VTT = "vtt";
|
public const string VTT = "vtt";
|
||||||
|
public const string WEBVTT = "webvtt";
|
||||||
public const string TTML = "ttml";
|
public const string TTML = "ttml";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,8 +25,18 @@ namespace MediaBrowser.Model.Providers
|
|||||||
|
|
||||||
public float? CommunityRating { get; set; }
|
public float? CommunityRating { get; set; }
|
||||||
|
|
||||||
|
public float? FrameRate { get; set; }
|
||||||
|
|
||||||
public int? DownloadCount { get; set; }
|
public int? DownloadCount { get; set; }
|
||||||
|
|
||||||
public bool? IsHashMatch { get; set; }
|
public bool? IsHashMatch { get; set; }
|
||||||
|
|
||||||
|
public bool? AiTranslated { get; set; }
|
||||||
|
|
||||||
|
public bool? MachineTranslated { get; set; }
|
||||||
|
|
||||||
|
public bool? Forced { get; set; }
|
||||||
|
|
||||||
|
public bool? HearingImpaired { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ namespace MediaBrowser.Model.Querying
|
|||||||
EnableTotalRecordCount = true;
|
EnableTotalRecordCount = true;
|
||||||
DisableFirstEpisode = false;
|
DisableFirstEpisode = false;
|
||||||
NextUpDateCutoff = DateTime.MinValue;
|
NextUpDateCutoff = DateTime.MinValue;
|
||||||
|
EnableResumable = false;
|
||||||
EnableRewatching = false;
|
EnableRewatching = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,6 +84,11 @@ namespace MediaBrowser.Model.Querying
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime NextUpDateCutoff { get; set; }
|
public DateTime NextUpDateCutoff { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets a value indicating whether to include resumable episodes as next up.
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableResumable { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether getting rewatching next up list.
|
/// Gets or sets a value indicating whether getting rewatching next up list.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -720,7 +720,7 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
MergeData(localItem, temp, Array.Empty<MetadataField>(), !options.ReplaceAllMetadata, true);
|
MergeData(localItem, temp, Array.Empty<MetadataField>(), options.ReplaceAllMetadata, true);
|
||||||
refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
|
refreshResult.UpdateType |= ItemUpdateType.MetadataImport;
|
||||||
|
|
||||||
// Only one local provider allowed per item
|
// Only one local provider allowed per item
|
||||||
|
@ -765,10 +765,12 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var results = await GetSearchResults(provider, searchInfo.SearchInfo, cancellationToken).ConfigureAwait(false);
|
var results = await provider.GetSearchResults(searchInfo.SearchInfo, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
foreach (var result in results)
|
foreach (var result in results)
|
||||||
{
|
{
|
||||||
|
result.SearchProviderName = provider.Name;
|
||||||
|
|
||||||
var existingMatch = resultList.FirstOrDefault(i => i.ProviderIds.Any(p => string.Equals(result.GetProviderId(p.Key), p.Value, StringComparison.OrdinalIgnoreCase)));
|
var existingMatch = resultList.FirstOrDefault(i => i.ProviderIds.Any(p => string.Equals(result.GetProviderId(p.Key), p.Value, StringComparison.OrdinalIgnoreCase)));
|
||||||
|
|
||||||
if (existingMatch is null)
|
if (existingMatch is null)
|
||||||
@ -800,22 +802,6 @@ namespace MediaBrowser.Providers.Manager
|
|||||||
return resultList;
|
return resultList;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IEnumerable<RemoteSearchResult>> GetSearchResults<TLookupType>(
|
|
||||||
IRemoteSearchProvider<TLookupType> provider,
|
|
||||||
TLookupType searchInfo,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
where TLookupType : ItemLookupInfo
|
|
||||||
{
|
|
||||||
var results = await provider.GetSearchResults(searchInfo, cancellationToken).ConfigureAwait(false);
|
|
||||||
|
|
||||||
foreach (var item in results)
|
|
||||||
{
|
|
||||||
item.SearchProviderName = provider.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item)
|
private IEnumerable<IExternalId> GetExternalIds(IHasProviderIds item)
|
||||||
{
|
{
|
||||||
return _externalIds.Where(i =>
|
return _externalIds.Where(i =>
|
||||||
|
@ -130,7 +130,8 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
|
|
||||||
output = await process.StandardError.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
using var reader = process.StandardError;
|
||||||
|
output = await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
MatchCollection split = LUFSRegex().Matches(output);
|
MatchCollection split = LUFSRegex().Matches(output);
|
||||||
|
|
||||||
@ -222,6 +223,8 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
var people = new List<PersonInfo>();
|
var people = new List<PersonInfo>();
|
||||||
var albumArtists = tags.AlbumArtists;
|
var albumArtists = tags.AlbumArtists;
|
||||||
foreach (var albumArtist in albumArtists)
|
foreach (var albumArtist in albumArtists)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(albumArtist))
|
||||||
{
|
{
|
||||||
PeopleHelper.AddPerson(people, new PersonInfo
|
PeopleHelper.AddPerson(people, new PersonInfo
|
||||||
{
|
{
|
||||||
@ -229,9 +232,12 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
Type = PersonKind.AlbumArtist
|
Type = PersonKind.AlbumArtist
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var performers = tags.Performers;
|
var performers = tags.Performers;
|
||||||
foreach (var performer in performers)
|
foreach (var performer in performers)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(performer))
|
||||||
{
|
{
|
||||||
PeopleHelper.AddPerson(people, new PersonInfo
|
PeopleHelper.AddPerson(people, new PersonInfo
|
||||||
{
|
{
|
||||||
@ -239,8 +245,11 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
Type = PersonKind.Artist
|
Type = PersonKind.Artist
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var composer in tags.Composers)
|
foreach (var composer in tags.Composers)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(composer))
|
||||||
{
|
{
|
||||||
PeopleHelper.AddPerson(people, new PersonInfo
|
PeopleHelper.AddPerson(people, new PersonInfo
|
||||||
{
|
{
|
||||||
@ -248,6 +257,7 @@ namespace MediaBrowser.Providers.MediaInfo
|
|||||||
Type = PersonKind.Composer
|
Type = PersonKind.Composer
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_libraryManager.UpdatePeople(audio, people);
|
_libraryManager.UpdatePeople(audio, people);
|
||||||
audio.Artists = performers;
|
audio.Artists = performers;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user