update to current master to resolve merge conflict

This commit is contained in:
herby2212 2023-10-21 01:20:59 +02:00
commit 27ceee8b6c
226 changed files with 5626 additions and 3298 deletions

12
.config/dotnet-tools.json Normal file
View File

@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "7.0.12",
"commands": [
"dotnet-ef"
]
}
}
}

View File

@ -20,18 +20,18 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- 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@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0 uses: github/codeql-action/init@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
queries: +security-extended queries: +security-extended
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0 uses: github/codeql-action/autobuild@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0 uses: github/codeql-action/analyze@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3

View File

@ -17,14 +17,14 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Notify as seen - name: Notify as seen
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2 uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
comment-id: ${{ github.event.comment.id }} comment-id: ${{ github.event.comment.id }}
reactions: '+1' reactions: '+1'
- name: Checkout the latest code - name: Checkout the latest code
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0 fetch-depth: 0
@ -43,7 +43,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Notify as seen - name: Notify as seen
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2 uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ github.event.comment != null }} if: ${{ github.event.comment != null }}
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
@ -51,14 +51,14 @@ jobs:
reactions: eyes reactions: eyes
- name: Checkout the latest code - name: Checkout the latest code
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0 fetch-depth: 0
- name: Notify as running - name: Notify as running
id: comment_running id: comment_running
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2 uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ github.event.comment != null }} if: ${{ github.event.comment != null }}
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
@ -93,7 +93,7 @@ jobs:
exit ${retcode} exit ${retcode}
- name: Notify with result success - name: Notify with result success
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2 uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ github.event.comment != null && success() }} if: ${{ github.event.comment != null && success() }}
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}
@ -108,7 +108,7 @@ jobs:
reactions: hooray reactions: hooray
- name: Notify with result failure - name: Notify with result failure
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2 uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ github.event.comment != null && failure() }} if: ${{ github.event.comment != null && failure() }}
with: with:
token: ${{ secrets.JF_BOT_TOKEN }} token: ${{ secrets.JF_BOT_TOKEN }}

View File

@ -14,7 +14,7 @@ jobs:
permissions: read-all permissions: read-all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
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 }}
@ -39,7 +39,7 @@ jobs:
permissions: read-all permissions: read-all
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
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 }}
@ -112,7 +112,7 @@ jobs:
direction: last direction: last
body-includes: openapi-diff-workflow-comment body-includes: openapi-diff-workflow-comment
- name: Reply or edit difference comment (changed) - name: Reply or edit difference comment (changed)
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2 uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ steps.read-diff.outputs.body != '' }} if: ${{ steps.read-diff.outputs.body != '' }}
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}
@ -127,7 +127,7 @@ jobs:
</details> </details>
- name: Edit difference comment (unchanged) - name: Edit difference comment (unchanged)
uses: peter-evans/create-or-update-comment@c6c9a1a66007646a28c153e2a8580a5bad27bcfa # v3.0.2 uses: peter-evans/create-or-update-comment@23ff15729ef2fc348714a3bb66d2f655ca9066f2 # v3.1.0
if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }} if: ${{ steps.read-diff.outputs.body == '' && steps.find-comment.outputs.comment-id != '' }}
with: with:
issue-number: ${{ github.event.pull_request.number }} issue-number: ${{ github.event.pull_request.number }}

View File

@ -33,7 +33,7 @@ jobs:
yq-version: v4.9.8 yq-version: v4.9.8
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with: with:
ref: ${{ env.TAG_BRANCH }} ref: ${{ env.TAG_BRANCH }}
@ -66,7 +66,7 @@ jobs:
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }} NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
steps: steps:
- name: Checkout Repository - name: Checkout Repository
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0 uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
with: with:
ref: ${{ env.TAG_BRANCH }} ref: ${{ env.TAG_BRANCH }}

View File

@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }} if: ${{ contains(github.repository, 'jellyfin/') }}
steps: steps:
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8 - uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
with: with:
repo-token: ${{ secrets.JF_BOT_TOKEN }} repo-token: ${{ secrets.JF_BOT_TOKEN }}
days-before-stale: 120 days-before-stale: 120

View File

@ -57,6 +57,7 @@
- [hawken93](https://github.com/hawken93) - [hawken93](https://github.com/hawken93)
- [HelloWorld017](https://github.com/HelloWorld017) - [HelloWorld017](https://github.com/HelloWorld017)
- [ikomhoog](https://github.com/ikomhoog) - [ikomhoog](https://github.com/ikomhoog)
- [iwalton3](https://github.com/iwalton3)
- [jftuga](https://github.com/jftuga) - [jftuga](https://github.com/jftuga)
- [jmshrv](https://github.com/jmshrv) - [jmshrv](https://github.com/jmshrv)
- [joern-h](https://github.com/joern-h) - [joern-h](https://github.com/joern-h)
@ -88,6 +89,7 @@
- [neilsb](https://github.com/neilsb) - [neilsb](https://github.com/neilsb)
- [nevado](https://github.com/nevado) - [nevado](https://github.com/nevado)
- [Nickbert7](https://github.com/Nickbert7) - [Nickbert7](https://github.com/Nickbert7)
- [nicknsy](https://github.com/nicknsy)
- [nvllsvm](https://github.com/nvllsvm) - [nvllsvm](https://github.com/nvllsvm)
- [nyanmisaka](https://github.com/nyanmisaka) - [nyanmisaka](https://github.com/nyanmisaka)
- [OancaAndrei](https://github.com/OancaAndrei) - [OancaAndrei](https://github.com/OancaAndrei)

View File

@ -17,7 +17,7 @@
<PackageVersion Include="Diacritics" Version="3.3.18" /> <PackageVersion Include="Diacritics" Version="3.3.18" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" /> <PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.2" /> <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.4" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" /> <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" /> <PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
@ -25,15 +25,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.11" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.12" />
<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.11" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.12" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.11" /> <PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.12" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.11" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.12" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.11" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.12" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.11" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.12" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.11" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.12" />
<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" />
@ -42,8 +42,8 @@
<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.11" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.12" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.11" /> <PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.12" />
<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" />
@ -86,8 +86,8 @@
<PackageVersion Include="TMDbLib" Version="2.0.0" /> <PackageVersion Include="TMDbLib" Version="2.0.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" /> <PackageVersion Include="UTF.Unknown" Version="2.5.1" />
<PackageVersion Include="Xunit.Priority" Version="1.1.6" /> <PackageVersion Include="Xunit.Priority" Version="1.1.6" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.1" /> <PackageVersion Include="xunit.runner.visualstudio" Version="2.5.3" />
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" /> <PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
<PackageVersion Include="xunit" Version="2.5.1" /> <PackageVersion Include="xunit" Version="2.5.3" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -228,7 +228,7 @@ namespace Emby.Dlna
try try
{ {
return _fileSystem.GetFilePaths(path) return _fileSystem.GetFilePaths(path)
.Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase)) .Where(i => Path.GetExtension(i.AsSpan()).Equals(".xml", StringComparison.OrdinalIgnoreCase))
.Select(i => ParseProfileFile(i, type)) .Select(i => ParseProfileFile(i, type))
.Where(i => i is not null) .Where(i => i is not null)
.ToList()!; // We just filtered out all the nulls .ToList()!; // We just filtered out all the nulls

View File

@ -0,0 +1,69 @@
using System;
using System.Globalization;
using System.Net;
using System.Net.Http;
using System.Text;
using Emby.Dlna.ConnectionManager;
using Emby.Dlna.ContentDirectory;
using Emby.Dlna.MediaReceiverRegistrar;
using Emby.Dlna.Ssdp;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Rssdp.Infrastructure;
namespace Emby.Dlna.Extensions;
/// <summary>
/// Extension methods for adding DLNA services.
/// </summary>
public static class DlnaServiceCollectionExtensions
{
/// <summary>
/// Adds DLNA services to the provided <see cref="IServiceCollection"/>.
/// </summary>
/// <param name="services">The <see cref="IServiceCollection"/>.</param>
/// <param name="applicationHost">The <see cref="IServerApplicationHost"/>.</param>
public static void AddDlnaServices(
this IServiceCollection services,
IServerApplicationHost applicationHost)
{
services.AddHttpClient(NamedClient.Dlna, c =>
{
c.DefaultRequestHeaders.UserAgent.ParseAdd(
string.Format(
CultureInfo.InvariantCulture,
"{0}/{1} UPnP/1.0 {2}/{3}",
Environment.OSVersion.Platform,
Environment.OSVersion,
applicationHost.Name,
applicationHost.ApplicationVersionString));
c.DefaultRequestHeaders.Add("CPFN.UPNP.ORG", applicationHost.FriendlyName); // Required for UPnP DeviceArchitecture v2.0
c.DefaultRequestHeaders.Add("FriendlyName.DLNA.ORG", applicationHost.FriendlyName); // REVIEW: where does this come from?
})
.ConfigurePrimaryHttpMessageHandler(_ => new SocketsHttpHandler
{
AutomaticDecompression = DecompressionMethods.All,
RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8
});
services.AddSingleton<IDlnaManager, DlnaManager>();
services.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
services.AddSingleton<IContentDirectory, ContentDirectoryService>();
services.AddSingleton<IConnectionManager, ConnectionManagerService>();
services.AddSingleton<IMediaReceiverRegistrar, MediaReceiverRegistrarService>();
services.AddSingleton<ISsdpCommunicationsServer>(provider => new SsdpCommunicationsServer(
provider.GetRequiredService<ISocketFactory>(),
provider.GetRequiredService<INetworkManager>(),
provider.GetRequiredService<ILogger<SsdpCommunicationsServer>>())
{
IsShared = true
});
}
}

View File

@ -23,10 +23,8 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Plugins; using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session; using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Rssdp; using Rssdp;
using Rssdp.Infrastructure; using Rssdp.Infrastructure;
@ -49,14 +47,13 @@ namespace Emby.Dlna.Main
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly IDeviceDiscovery _deviceDiscovery; private readonly IDeviceDiscovery _deviceDiscovery;
private readonly ISocketFactory _socketFactory; private readonly ISsdpCommunicationsServer _communicationsServer;
private readonly INetworkManager _networkManager; private readonly INetworkManager _networkManager;
private readonly object _syncLock = new object(); private readonly object _syncLock = new();
private readonly bool _disabled; private readonly bool _disabled;
private PlayToManager _manager; private PlayToManager _manager;
private SsdpDevicePublisher _publisher; private SsdpDevicePublisher _publisher;
private ISsdpCommunicationsServer _communicationsServer;
private bool _disposed; private bool _disposed;
@ -75,10 +72,8 @@ namespace Emby.Dlna.Main
IMediaSourceManager mediaSourceManager, IMediaSourceManager mediaSourceManager,
IDeviceDiscovery deviceDiscovery, IDeviceDiscovery deviceDiscovery,
IMediaEncoder mediaEncoder, IMediaEncoder mediaEncoder,
ISocketFactory socketFactory, ISsdpCommunicationsServer communicationsServer,
INetworkManager networkManager, INetworkManager networkManager)
IUserViewManager userViewManager,
ITVSeriesManager tvSeriesManager)
{ {
_config = config; _config = config;
_appHost = appHost; _appHost = appHost;
@ -93,37 +88,10 @@ namespace Emby.Dlna.Main
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;
_deviceDiscovery = deviceDiscovery; _deviceDiscovery = deviceDiscovery;
_mediaEncoder = mediaEncoder; _mediaEncoder = mediaEncoder;
_socketFactory = socketFactory; _communicationsServer = communicationsServer;
_networkManager = networkManager; _networkManager = networkManager;
_logger = loggerFactory.CreateLogger<DlnaEntryPoint>(); _logger = loggerFactory.CreateLogger<DlnaEntryPoint>();
ContentDirectory = new ContentDirectory.ContentDirectoryService(
dlnaManager,
userDataManager,
imageProcessor,
libraryManager,
config,
userManager,
loggerFactory.CreateLogger<ContentDirectory.ContentDirectoryService>(),
httpClientFactory,
localizationManager,
mediaSourceManager,
userViewManager,
mediaEncoder,
tvSeriesManager);
ConnectionManager = new ConnectionManager.ConnectionManagerService(
dlnaManager,
config,
loggerFactory.CreateLogger<ConnectionManager.ConnectionManagerService>(),
httpClientFactory);
MediaReceiverRegistrar = new MediaReceiverRegistrar.MediaReceiverRegistrarService(
loggerFactory.CreateLogger<MediaReceiverRegistrar.MediaReceiverRegistrarService>(),
httpClientFactory,
config);
Current = this;
var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey); var netConfig = config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
_disabled = appHost.ListenWithHttps && netConfig.RequireHttps; _disabled = appHost.ListenWithHttps && netConfig.RequireHttps;
@ -133,19 +101,6 @@ namespace Emby.Dlna.Main
} }
} }
public static DlnaEntryPoint Current { get; private set; }
/// <summary>
/// Gets a value indicating whether the dlna server is enabled.
/// </summary>
public static bool Enabled { get; private set; }
public IContentDirectory ContentDirectory { get; private set; }
public IConnectionManager ConnectionManager { get; private set; }
public IMediaReceiverRegistrar MediaReceiverRegistrar { get; private set; }
public async Task RunAsync() public async Task RunAsync()
{ {
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false); await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
@ -172,9 +127,7 @@ namespace Emby.Dlna.Main
private void ReloadComponents() private void ReloadComponents()
{ {
var options = _config.GetDlnaConfiguration(); var options = _config.GetDlnaConfiguration();
Enabled = options.EnableServer; StartDeviceDiscovery();
StartSsdpHandler();
if (options.EnableServer) if (options.EnableServer)
{ {
@ -195,36 +148,11 @@ namespace Emby.Dlna.Main
} }
} }
private void StartSsdpHandler() private void StartDeviceDiscovery()
{ {
try try
{ {
if (_communicationsServer is null) ((DeviceDiscovery)_deviceDiscovery).Start(_communicationsServer);
{
var enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux();
_communicationsServer = new SsdpCommunicationsServer(_socketFactory, _networkManager, _logger, enableMultiSocketBinding)
{
IsShared = true
};
StartDeviceDiscovery(_communicationsServer);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error starting ssdp handlers");
}
}
private void StartDeviceDiscovery(ISsdpCommunicationsServer communicationsServer)
{
try
{
if (communicationsServer is not null)
{
((DeviceDiscovery)_deviceDiscovery).Start(communicationsServer);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -232,19 +160,6 @@ namespace Emby.Dlna.Main
} }
} }
private void DisposeDeviceDiscovery()
{
try
{
_logger.LogInformation("Disposing DeviceDiscovery");
((DeviceDiscovery)_deviceDiscovery).Dispose();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error stopping device discovery");
}
}
public void StartDevicePublisher(Configuration.DlnaOptions options) public void StartDevicePublisher(Configuration.DlnaOptions options)
{ {
if (_publisher is not null) if (_publisher is not null)
@ -317,7 +232,7 @@ namespace Emby.Dlna.Main
// This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
}; };
SetProperies(device, fullService); SetProperties(device, fullService);
_publisher.AddDevice(device); _publisher.AddDevice(device);
var embeddedDevices = new[] var embeddedDevices = new[]
@ -338,13 +253,13 @@ namespace Emby.Dlna.Main
// This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc. // This must be a globally unique value that survives reboots etc. Get from storage or embedded hardware etc.
}; };
SetProperies(embeddedDevice, subDevice); SetProperties(embeddedDevice, subDevice);
device.AddDevice(embeddedDevice); device.AddDevice(embeddedDevice);
} }
} }
} }
private string CreateUuid(string text) private static string CreateUuid(string text)
{ {
if (!Guid.TryParse(text, out var guid)) if (!Guid.TryParse(text, out var guid))
{ {
@ -354,15 +269,14 @@ namespace Emby.Dlna.Main
return guid.ToString("D", CultureInfo.InvariantCulture); return guid.ToString("D", CultureInfo.InvariantCulture);
} }
private void SetProperies(SsdpDevice device, string fullDeviceType) private static void SetProperties(SsdpDevice device, string fullDeviceType)
{ {
var service = fullDeviceType.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase).Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase); var serviceParts = fullDeviceType
.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase)
.Replace(":1", string.Empty, StringComparison.OrdinalIgnoreCase)
.Split(':');
var serviceParts = service.Split(':'); device.DeviceTypeNamespace = serviceParts[0].Replace('.', '-');
var deviceTypeNamespace = serviceParts[0].Replace('.', '-');
device.DeviceTypeNamespace = deviceTypeNamespace;
device.DeviceClass = serviceParts[1]; device.DeviceClass = serviceParts[1];
device.DeviceType = serviceParts[2]; device.DeviceType = serviceParts[2];
} }
@ -443,20 +357,6 @@ namespace Emby.Dlna.Main
DisposeDevicePublisher(); DisposeDevicePublisher();
DisposePlayToManager(); DisposePlayToManager();
DisposeDeviceDiscovery();
if (_communicationsServer is not null)
{
_logger.LogInformation("Disposing SsdpCommunicationsServer");
_communicationsServer.Dispose();
_communicationsServer = null;
}
ContentDirectory = null;
ConnectionManager = null;
MediaReceiverRegistrar = null;
Current = null;
_disposed = true; _disposed = true;
} }
} }

View File

@ -927,15 +927,12 @@ namespace Emby.Dlna.PlayTo
var resElement = container.Element(UPnpNamespaces.Res); var resElement = container.Element(UPnpNamespaces.Res);
if (resElement is not null) var info = resElement?.Attribute(UPnpNamespaces.ProtocolInfo);
{
var info = resElement.Attribute(UPnpNamespaces.ProtocolInfo);
if (info is not null && !string.IsNullOrWhiteSpace(info.Value)) if (info is not null && !string.IsNullOrWhiteSpace(info.Value))
{ {
return info.Value.Split(':'); return info.Value.Split(':');
} }
}
return new string[4]; return new string[4];
} }
@ -1139,7 +1136,6 @@ namespace Emby.Dlna.PlayTo
return new Device(deviceProperties, httpClientFactory, logger); return new Device(deviceProperties, httpClientFactory, logger);
} }
#nullable enable
private static DeviceIcon CreateIcon(XElement element) private static DeviceIcon CreateIcon(XElement element)
{ {
ArgumentNullException.ThrowIfNull(element); ArgumentNullException.ThrowIfNull(element);

View File

@ -55,21 +55,21 @@ namespace Emby.Dlna.PlayTo
var client = _httpClientFactory.CreateClient(NamedClient.Dlna); var client = _httpClientFactory.CreateClient(NamedClient.Dlna);
using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false); using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
await using MemoryStream ms = new MemoryStream(); Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await response.Content.CopyToAsync(ms, cancellationToken).ConfigureAwait(false); await using (stream.ConfigureAwait(false))
ms.Position = 0; {
try try
{ {
return await XDocument.LoadAsync( return await XDocument.LoadAsync(
ms, stream,
LoadOptions.None, LoadOptions.None,
cancellationToken).ConfigureAwait(false); cancellationToken).ConfigureAwait(false);
} }
catch (XmlException) catch (XmlException)
{ {
// try correcting the Xml response with common errors // try correcting the Xml response with common errors
ms.Position = 0; stream.Position = 0;
using StreamReader sr = new StreamReader(ms); using StreamReader sr = new StreamReader(stream);
var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false); var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
// find and replace unescaped ampersands (&) // find and replace unescaped ampersands (&)
@ -93,6 +93,7 @@ namespace Emby.Dlna.PlayTo
} }
} }
} }
}
public async Task<XDocument?> GetDataAsync(string url, CancellationToken cancellationToken) public async Task<XDocument?> GetDataAsync(string url, CancellationToken cancellationToken)
{ {

View File

@ -39,9 +39,9 @@ namespace Emby.Dlna.PlayTo
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
private readonly IMediaEncoder _mediaEncoder; private readonly IMediaEncoder _mediaEncoder;
private readonly SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
private readonly CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
private bool _disposed; private bool _disposed;
private SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
private CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder) public PlayToManager(ILogger logger, ISessionManager sessionManager, ILibraryManager libraryManager, IUserManager userManager, IDlnaManager dlnaManager, IServerApplicationHost appHost, IImageProcessor imageProcessor, IDeviceDiscovery deviceDiscovery, IHttpClientFactory httpClientFactory, IUserDataManager userDataManager, ILocalizationManager localization, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder)
{ {

View File

@ -318,7 +318,7 @@ 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[]
{ {
@ -328,7 +328,7 @@ namespace Emby.Naming.Common
"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[]
{ {
@ -376,7 +376,7 @@ namespace Emby.Naming.Common
IsNamed = true, IsNamed = true,
SupportsAbsoluteEpisodeNumbers = false SupportsAbsoluteEpisodeNumbers = false
}, },
new EpisodeExpression("[\\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\\/]*)$") new EpisodeExpression(@"[\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\/]*)$")
{ {
SupportsAbsoluteEpisodeNumbers = true SupportsAbsoluteEpisodeNumbers = true
}, },
@ -417,7 +417,7 @@ namespace Emby.Naming.Common
}, },
// "1-12 episode title" // "1-12 episode title"
new EpisodeExpression(@"([0-9]+)-([0-9]+)"), new EpisodeExpression("([0-9]+)-([0-9]+)"),
// "01 - blah.avi", "01-blah.avi" // "01 - blah.avi", "01-blah.avi"
new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\s?-\s?[^\\\/]*$") new EpisodeExpression(@".*(\\|\/)(?<epnumber>[0-9]{1,3})(-(?<endingepnumber>[0-9]{2,3}))*\s?-\s?[^\\\/]*$")
@ -712,7 +712,7 @@ namespace Emby.Naming.Common
// Chapter is often beginning of filename // Chapter is often beginning of filename
"^(?<chapter>[0-9]+)", "^(?<chapter>[0-9]+)",
// Part if often ending of filename // Part if often ending of filename
@"(?<!ch(?:apter) )(?<part>[0-9]+)$", "(?<!ch(?:apter) )(?<part>[0-9]+)$",
// Sometimes named as 0001_005 (chapter_part) // Sometimes named as 0001_005 (chapter_part)
"(?<chapter>[0-9]+)_(?<part>[0-9]+)", "(?<chapter>[0-9]+)_(?<part>[0-9]+)",
// Some audiobooks are ripped from cd's, and will be named by disk number. // Some audiobooks are ripped from cd's, and will be named by disk number.

View File

@ -43,7 +43,7 @@ namespace Emby.Naming.ExternalFiles
return null; return null;
} }
var extension = Path.GetExtension(path); var extension = Path.GetExtension(path.AsSpan());
if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
&& !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))) && !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
{ {

View File

@ -26,19 +26,18 @@ namespace Emby.Naming.Video
return false; return false;
} }
var extension = Path.GetExtension(path); var extension = Path.GetExtension(path.AsSpan());
if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{ {
return false; return false;
} }
path = Path.GetFileNameWithoutExtension(path); var token = Path.GetExtension(Path.GetFileNameWithoutExtension(path.AsSpan())).TrimStart('.');
var token = Path.GetExtension(path).TrimStart('.');
foreach (var rule in options.StubTypes) foreach (var rule in options.StubTypes)
{ {
if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase)) if (token.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
{ {
stubType = rule.StubType; stubType = rule.StubType;
return true; return true;

View File

@ -61,7 +61,7 @@ namespace Emby.Photos
item.SetImagePath(ImageType.Primary, item.Path); item.SetImagePath(ImageType.Primary, item.Path);
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs // Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase)) if (_includeExtensions.Contains(Path.GetExtension(item.Path.AsSpan()), StringComparison.OrdinalIgnoreCase))
{ {
try try
{ {

View File

@ -10,8 +10,6 @@ namespace Emby.Server.Implementations.AppBase
/// </summary> /// </summary>
public abstract class BaseApplicationPaths : IApplicationPaths public abstract class BaseApplicationPaths : IApplicationPaths
{ {
private string _dataPath;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="BaseApplicationPaths"/> class. /// Initializes a new instance of the <see cref="BaseApplicationPaths"/> class.
/// </summary> /// </summary>
@ -33,7 +31,7 @@ namespace Emby.Server.Implementations.AppBase
CachePath = cacheDirectoryPath; CachePath = cacheDirectoryPath;
WebPath = webDirectoryPath; WebPath = webDirectoryPath;
_dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName; DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
} }
/// <summary> /// <summary>
@ -55,7 +53,7 @@ namespace Emby.Server.Implementations.AppBase
/// Gets the folder path to the data directory. /// Gets the folder path to the data directory.
/// </summary> /// </summary>
/// <value>The data directory.</value> /// <value>The data directory.</value>
public string DataPath => _dataPath; public string DataPath { get; }
/// <inheritdoc /> /// <inheritdoc />
public string VirtualDataPath => "%AppDataPath%"; public string VirtualDataPath => "%AppDataPath%";

View File

@ -13,9 +13,7 @@ using System.Net;
using System.Reflection; using System.Reflection;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna;
using Emby.Dlna.Main; using Emby.Dlna.Main;
using Emby.Dlna.Ssdp;
using Emby.Naming.Common; using Emby.Naming.Common;
using Emby.Photos; using Emby.Photos;
using Emby.Server.Implementations.Channels; using Emby.Server.Implementations.Channels;
@ -58,7 +56,6 @@ using MediaBrowser.Controller.Chapters;
using MediaBrowser.Controller.ClientEvent; using MediaBrowser.Controller.ClientEvent;
using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -82,7 +79,6 @@ using MediaBrowser.LocalMetadata.Savers;
using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.MediaEncoding.BdInfo;
using MediaBrowser.MediaEncoding.Subtitles; using MediaBrowser.MediaEncoding.Subtitles;
using MediaBrowser.Model.Cryptography; using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Globalization; using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
@ -101,7 +97,6 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Prometheus.DotNetRuntime; using Prometheus.DotNetRuntime;
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
@ -133,7 +128,7 @@ namespace Emby.Server.Implementations
/// <value>All concrete types.</value> /// <value>All concrete types.</value>
private Type[] _allConcreteTypes; private Type[] _allConcreteTypes;
private bool _disposed = false; private bool _disposed;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ApplicationHost"/> class. /// Initializes a new instance of the <see cref="ApplicationHost"/> class.
@ -184,26 +179,16 @@ namespace Emby.Server.Implementations
public bool CoreStartupHasCompleted { get; private set; } public bool CoreStartupHasCompleted { get; private set; }
public virtual bool CanLaunchWebBrowser => Environment.UserInteractive
&& !_startupOptions.IsService
&& (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
/// <summary> /// <summary>
/// Gets the <see cref="INetworkManager"/> singleton instance. /// Gets the <see cref="INetworkManager"/> singleton instance.
/// </summary> /// </summary>
public INetworkManager NetManager { get; private set; } public INetworkManager NetManager { get; private set; }
/// <summary> /// <inheritdoc />
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
/// </summary>
/// <value><c>true</c> if this instance has pending application restart; otherwise, <c>false</c>.</value>
public bool HasPendingRestart { get; private set; } public bool HasPendingRestart { get; private set; }
/// <inheritdoc /> /// <inheritdoc />
public bool IsShuttingDown { get; private set; } public bool ShouldRestart { get; set; }
/// <inheritdoc />
public bool ShouldRestart { get; private set; }
/// <summary> /// <summary>
/// Gets the logger. /// Gets the logger.
@ -461,7 +446,7 @@ namespace Emby.Server.Implementations
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>()); ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>()); NetManager = new NetworkManager(ConfigurationManager, _startupConfig, LoggerFactory.CreateLogger<NetworkManager>());
// Initialize runtime stat collection // Initialize runtime stat collection
if (ConfigurationManager.Configuration.EnableMetrics) if (ConfigurationManager.Configuration.EnableMetrics)
@ -507,6 +492,8 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>(); serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>(); serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
serviceCollection.AddScoped<ISystemManager, SystemManager>();
serviceCollection.AddSingleton<TmdbClientManager>(); serviceCollection.AddSingleton<TmdbClientManager>();
serviceCollection.AddSingleton(NetManager); serviceCollection.AddSingleton(NetManager);
@ -572,8 +559,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<ISessionManager, SessionManager>(); serviceCollection.AddSingleton<ISessionManager, SessionManager>();
serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
serviceCollection.AddSingleton<ICollectionManager, CollectionManager>(); serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>(); serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
@ -585,8 +570,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IUserViewManager, UserViewManager>(); serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
serviceCollection.AddSingleton<IChapterManager, ChapterManager>(); serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>(); serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>();
@ -850,24 +833,6 @@ namespace Emby.Server.Implementations
} }
} }
/// <inheritdoc />
public void Restart()
{
ShouldRestart = true;
Shutdown();
}
/// <inheritdoc />
public void Shutdown()
{
Task.Run(async () =>
{
await Task.Delay(100).ConfigureAwait(false);
IsShuttingDown = true;
Resolve<IHostApplicationLifetime>().StopApplication();
});
}
/// <summary> /// <summary>
/// Gets the composable part assemblies. /// Gets the composable part assemblies.
/// </summary> /// </summary>
@ -923,49 +888,6 @@ namespace Emby.Server.Implementations
protected abstract IEnumerable<Assembly> GetAssembliesWithPartsInternal(); protected abstract IEnumerable<Assembly> GetAssembliesWithPartsInternal();
/// <summary>
/// Gets the system status.
/// </summary>
/// <param name="request">Where this request originated.</param>
/// <returns>SystemInfo.</returns>
public SystemInfo GetSystemInfo(HttpRequest request)
{
return new SystemInfo
{
HasPendingRestart = HasPendingRestart,
IsShuttingDown = IsShuttingDown,
Version = ApplicationVersionString,
WebSocketPortNumber = HttpPort,
CompletedInstallations = Resolve<IInstallationManager>().CompletedInstallations.ToArray(),
Id = SystemId,
ProgramDataPath = ApplicationPaths.ProgramDataPath,
WebPath = ApplicationPaths.WebPath,
LogPath = ApplicationPaths.LogDirectoryPath,
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
CachePath = ApplicationPaths.CachePath,
CanLaunchWebBrowser = CanLaunchWebBrowser,
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
ServerName = FriendlyName,
LocalAddress = GetSmartApiUrl(request),
SupportsLibraryMonitor = true,
PackageName = _startupOptions.PackageName
};
}
public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
{
return new PublicSystemInfo
{
Version = ApplicationVersionString,
ProductName = ApplicationProductName,
Id = SystemId,
ServerName = FriendlyName,
LocalAddress = GetSmartApiUrl(request),
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
};
}
/// <inheritdoc/> /// <inheritdoc/>
public string GetSmartApiUrl(IPAddress remoteAddr) public string GetSmartApiUrl(IPAddress remoteAddr)
{ {
@ -983,7 +905,7 @@ namespace Emby.Server.Implementations
/// <inheritdoc/> /// <inheritdoc/>
public string GetSmartApiUrl(HttpRequest request) public string GetSmartApiUrl(HttpRequest request)
{ {
// Return the host in the HTTP request as the API url // Return the host in the HTTP request as the API URL if not configured otherwise
if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest) if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest)
{ {
int? requestPort = request.Host.Port; int? requestPort = request.Host.Port;
@ -1018,7 +940,7 @@ namespace Emby.Server.Implementations
public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true) public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true)
{ {
// With an empty source, the port will be null // With an empty source, the port will be null
var smart = NetManager.GetBindAddress(ipAddress, out _, true); var smart = NetManager.GetBindAddress(ipAddress, out _, false);
var scheme = !allowHttps ? Uri.UriSchemeHttp : null; var scheme = !allowHttps ? Uri.UriSchemeHttp : null;
int? port = !allowHttps ? HttpPort : null; int? port = !allowHttps ? HttpPort : null;
return GetLocalApiUrl(smart, scheme, port); return GetLocalApiUrl(smart, scheme, port);

View File

@ -371,9 +371,12 @@ namespace Emby.Server.Implementations.Channels
Directory.CreateDirectory(Path.GetDirectoryName(path)); Directory.CreateDirectory(Path.GetDirectoryName(path));
await using FileStream createStream = File.Create(path); FileStream createStream = File.Create(path);
await using (createStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false); await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
} }
}
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<MediaSourceInfo> GetStaticMediaSources(BaseItem item, CancellationToken cancellationToken) public IEnumerable<MediaSourceInfo> GetStaticMediaSources(BaseItem item, CancellationToken cancellationToken)
@ -1156,7 +1159,7 @@ namespace Emby.Server.Implementations.Channels
if (info.People is not null && info.People.Count > 0) if (info.People is not null && info.People.Count > 0)
{ {
_libraryManager.UpdatePeople(item, info.People); await _libraryManager.UpdatePeopleAsync(item, info.People, cancellationToken).ConfigureAwait(false);
} }
} }
else if (forceUpdate) else if (forceUpdate)

View File

@ -3540,10 +3540,7 @@ namespace Emby.Server.Implementations.Data
.Append(paramName) .Append(paramName)
.Append("))) OR "); .Append("))) OR ");
if (statement is not null) statement?.TryBind(paramName, query.PersonIds[i]);
{
statement.TryBind(paramName, query.PersonIds[i]);
}
} }
clauseBuilder.Length -= Or.Length; clauseBuilder.Length -= Or.Length;
@ -4382,7 +4379,7 @@ namespace Emby.Server.Implementations.Data
foreach (var videoType in query.VideoTypes) foreach (var videoType in query.VideoTypes)
{ {
videoTypes.Add("data like '%\"VideoType\":\"" + videoType.ToString() + "\"%'"); videoTypes.Add("data like '%\"VideoType\":\"" + videoType + "\"%'");
} }
whereClauses.Add("(" + string.Join(" OR ", videoTypes) + ")"); whereClauses.Add("(" + string.Join(" OR ", videoTypes) + ")");

View File

@ -22,6 +22,7 @@ using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
@ -52,6 +53,7 @@ namespace Emby.Server.Implementations.Dto
private readonly Lazy<ILiveTvManager> _livetvManagerFactory; private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
private readonly ILyricManager _lyricManager; private readonly ILyricManager _lyricManager;
private readonly ITrickplayManager _trickplayManager;
public DtoService( public DtoService(
ILogger<DtoService> logger, ILogger<DtoService> logger,
@ -63,7 +65,8 @@ namespace Emby.Server.Implementations.Dto
IApplicationHost appHost, IApplicationHost appHost,
IMediaSourceManager mediaSourceManager, IMediaSourceManager mediaSourceManager,
Lazy<ILiveTvManager> livetvManagerFactory, Lazy<ILiveTvManager> livetvManagerFactory,
ILyricManager lyricManager) ILyricManager lyricManager,
ITrickplayManager trickplayManager)
{ {
_logger = logger; _logger = logger;
_libraryManager = libraryManager; _libraryManager = libraryManager;
@ -75,6 +78,7 @@ namespace Emby.Server.Implementations.Dto
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;
_livetvManagerFactory = livetvManagerFactory; _livetvManagerFactory = livetvManagerFactory;
_lyricManager = lyricManager; _lyricManager = lyricManager;
_trickplayManager = trickplayManager;
} }
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value; private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
@ -1059,6 +1063,11 @@ namespace Emby.Server.Implementations.Dto
dto.Chapters = _itemRepo.GetChapters(item); dto.Chapters = _itemRepo.GetChapters(item);
} }
if (options.ContainsField(ItemFields.Trickplay))
{
dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
}
if (video.ExtraType.HasValue) if (video.ExtraType.HasValue)
{ {
dto.ExtraType = video.ExtraType.Value.ToString(); dto.ExtraType = video.ExtraType.Value.ToString();

View File

@ -43,8 +43,6 @@
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
<NoWarn>AD0001</NoWarn>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">

View File

@ -18,7 +18,7 @@ using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.EntryPoints namespace Emby.Server.Implementations.EntryPoints
{ {
/// <summary> /// <summary>
/// Class UdpServerEntryPoint. /// Class responsible for registering all UDP broadcast endpoints and their handlers.
/// </summary> /// </summary>
public sealed class UdpServerEntryPoint : IServerEntryPoint public sealed class UdpServerEntryPoint : IServerEntryPoint
{ {
@ -35,14 +35,13 @@ namespace Emby.Server.Implementations.EntryPoints
private readonly IConfiguration _config; private readonly IConfiguration _config;
private readonly IConfigurationManager _configurationManager; private readonly IConfigurationManager _configurationManager;
private readonly INetworkManager _networkManager; private readonly INetworkManager _networkManager;
private readonly bool _enableMultiSocketBinding;
/// <summary> /// <summary>
/// The UDP server. /// The UDP server.
/// </summary> /// </summary>
private List<UdpServer> _udpServers; private readonly List<UdpServer> _udpServers;
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private bool _disposed = false; private bool _disposed;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class. /// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class.
@ -65,7 +64,6 @@ namespace Emby.Server.Implementations.EntryPoints
_configurationManager = configurationManager; _configurationManager = configurationManager;
_networkManager = networkManager; _networkManager = networkManager;
_udpServers = new List<UdpServer>(); _udpServers = new List<UdpServer>();
_enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux();
} }
/// <inheritdoc /> /// <inheritdoc />
@ -80,14 +78,16 @@ namespace Emby.Server.Implementations.EntryPoints
try try
{ {
if (_enableMultiSocketBinding) // Linux needs to bind to the broadcast addresses to get broadcast traffic
// Windows receives broadcast fine when binding to just the interface, it is unable to bind to broadcast addresses
if (OperatingSystem.IsLinux())
{ {
// Add global broadcast socket // Add global broadcast listener
var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber); var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber);
server.Start(_cancellationTokenSource.Token); server.Start(_cancellationTokenSource.Token);
_udpServers.Add(server); _udpServers.Add(server);
// Add bind address specific broadcast sockets // Add bind address specific broadcast listeners
// IPv6 is currently unsupported // IPv6 is currently unsupported
var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork); var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
foreach (var intf in validInterfaces) foreach (var intf in validInterfaces)
@ -102,11 +102,20 @@ namespace Emby.Server.Implementations.EntryPoints
} }
else else
{ {
var server = new UdpServer(_logger, _appHost, _config, IPAddress.Any, PortNumber); // Add bind address specific broadcast listeners
// IPv6 is currently unsupported
var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
foreach (var intf in validInterfaces)
{
var intfAddress = intf.Address;
_logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", intfAddress, PortNumber);
var server = new UdpServer(_logger, _appHost, _config, intfAddress, PortNumber);
server.Start(_cancellationTokenSource.Token); server.Start(_cancellationTokenSource.Token);
_udpServers.Add(server); _udpServers.Add(server);
} }
} }
}
catch (SocketException ex) catch (SocketException ex)
{ {
_logger.LogWarning(ex, "Unable to start AutoDiscovery listener on UDP port {PortNumber}", PortNumber); _logger.LogWarning(ex, "Unable to start AutoDiscovery listener on UDP port {PortNumber}", PortNumber);
@ -119,7 +128,7 @@ namespace Emby.Server.Implementations.EntryPoints
{ {
if (_disposed) if (_disposed)
{ {
throw new ObjectDisposedException(this.GetType().Name); throw new ObjectDisposedException(GetType().Name);
} }
} }

View File

@ -12,7 +12,6 @@ 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

View File

@ -210,7 +210,6 @@ namespace Emby.Server.Implementations.IO
DisposeTimer(); DisposeTimer();
_disposed = true; _disposed = true;
GC.SuppressFinalize(this);
} }
} }
} }

View File

@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.IO
} }
// unc path // unc path
if (filePath.StartsWith("\\\\", StringComparison.Ordinal)) if (filePath.StartsWith(@"\\", StringComparison.Ordinal))
{ {
return filePath; return filePath;
} }
@ -103,15 +103,17 @@ namespace Emby.Server.Implementations.IO
return filePath; return filePath;
} }
var filePathSpan = filePath.AsSpan();
// relative path // relative path
if (firstChar == '\\') if (firstChar == '\\')
{ {
filePath = filePath.Substring(1); filePathSpan = filePathSpan.Slice(1);
} }
try try
{ {
return Path.GetFullPath(Path.Combine(folderPath, filePath)); return Path.GetFullPath(Path.Join(folderPath, filePathSpan));
} }
catch (ArgumentException) catch (ArgumentException)
{ {

View File

@ -46,7 +46,6 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.Library; using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying; using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks; using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode; using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using EpisodeInfo = Emby.Naming.TV.EpisodeInfo; using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
@ -839,19 +838,12 @@ namespace Emby.Server.Implementations.Library
{ {
var path = Person.GetPath(name); var path = Person.GetPath(name);
var id = GetItemByNameId<Person>(path); var id = GetItemByNameId<Person>(path);
if (GetItemById(id) is not Person item) if (GetItemById(id) is Person item)
{ {
item = new Person return item;
{
Name = name,
Id = id,
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow,
Path = path
};
} }
return item; return null;
} }
/// <summary> /// <summary>
@ -1162,7 +1154,7 @@ namespace Emby.Server.Implementations.Library
Name = Path.GetFileName(dir), Name = Path.GetFileName(dir),
Locations = _fileSystem.GetFilePaths(dir, false) Locations = _fileSystem.GetFilePaths(dir, false)
.Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
.Select(i => .Select(i =>
{ {
try try
@ -2858,7 +2850,7 @@ namespace Emby.Server.Implementations.Library
{ {
var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection"); var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection");
File.WriteAllBytes(path, Array.Empty<byte>()); await File.WriteAllBytesAsync(path, Array.Empty<byte>()).ConfigureAwait(false);
} }
CollectionFolder.SaveLibraryOptions(virtualFolderPath, options); CollectionFolder.SaveLibraryOptions(virtualFolderPath, options);
@ -2900,9 +2892,18 @@ namespace Emby.Server.Implementations.Library
var saveEntity = false; var saveEntity = false;
var personEntity = GetPerson(person.Name); var personEntity = GetPerson(person.Name);
// if PresentationUniqueKey is empty it's likely a new item. if (personEntity is null)
if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey))
{ {
var path = Person.GetPath(person.Name);
personEntity = new Person()
{
Name = person.Name,
Id = GetItemByNameId<Person>(path),
DateCreated = DateTime.UtcNow,
DateModified = DateTime.UtcNow,
Path = path
};
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey(); personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
saveEntity = true; saveEntity = true;
} }
@ -3135,7 +3136,7 @@ namespace Emby.Server.Implementations.Library
} }
var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true) var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
.Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
.FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase)); .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
if (!string.IsNullOrEmpty(shortcut)) if (!string.IsNullOrEmpty(shortcut))

View File

@ -48,15 +48,20 @@ namespace Emby.Server.Implementations.Library
if (!string.IsNullOrEmpty(cacheKey)) if (!string.IsNullOrEmpty(cacheKey))
{ {
FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
try try
{ {
await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info"); // _logger.LogDebug("Found cached media info");
} }
catch catch (Exception ex)
{ {
_logger.LogError(ex, "Error deserializing mediainfo cache");
}
finally
{
await jsonStream.DisposeAsync().ConfigureAwait(false);
} }
} }
@ -84,10 +89,13 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath is not null) if (cacheFilePath is not null)
{ {
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
await using FileStream createStream = AsyncFile.OpenWrite(cacheFilePath); FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
await using (createStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
}
// _logger.LogDebug("Saved media info to {0}", cacheFilePath); _logger.LogDebug("Saved media info to {0}", cacheFilePath);
} }
} }

View File

@ -625,17 +625,19 @@ namespace Emby.Server.Implementations.Library
if (!string.IsNullOrEmpty(cacheKey)) if (!string.IsNullOrEmpty(cacheKey))
{ {
FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
try try
{ {
await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
// _logger.LogDebug("Found cached media info");
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception."); _logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception.");
} }
finally
{
await jsonStream.DisposeAsync().ConfigureAwait(false);
}
} }
if (mediaInfo is null) if (mediaInfo is null)
@ -664,8 +666,11 @@ namespace Emby.Server.Implementations.Library
if (cacheFilePath is not null) if (cacheFilePath is not null)
{ {
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
await using FileStream createStream = File.Create(cacheFilePath); FileStream createStream = File.Create(cacheFilePath);
await using (createStream.ConfigureAwait(false))
{
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
}
// _logger.LogDebug("Saved media info to {0}", cacheFilePath); // _logger.LogDebug("Saved media info to {0}", cacheFilePath);
} }

View File

@ -94,9 +94,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
if (AudioFileParser.IsAudioFile(args.Path, _namingOptions)) if (AudioFileParser.IsAudioFile(args.Path, _namingOptions))
{ {
var extension = Path.GetExtension(args.Path); var extension = Path.GetExtension(args.Path.AsSpan());
if (string.Equals(extension, ".cue", StringComparison.OrdinalIgnoreCase)) if (extension.Equals(".cue", StringComparison.OrdinalIgnoreCase))
{ {
// if audio file exists of same name, return null // if audio file exists of same name, return null
return null; return null;
@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
if (item is not null) if (item is not null)
{ {
item.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase); item.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase);
item.IsInMixedFolder = true; item.IsInMixedFolder = true;
} }

View File

@ -263,7 +263,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
return false; return false;
} }
return directoryService.GetFilePaths(fullPath).Any(i => string.Equals(Path.GetExtension(i), ".vob", StringComparison.OrdinalIgnoreCase)); return directoryService.GetFilePaths(fullPath).Any(i => Path.GetExtension(i.AsSpan()).Equals(".vob", StringComparison.OrdinalIgnoreCase));
} }
/// <summary> /// <summary>

View File

@ -32,9 +32,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
return GetBook(args); return GetBook(args);
} }
var extension = Path.GetExtension(args.Path); var extension = Path.GetExtension(args.Path.AsSpan());
if (extension is not null && _validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) if (_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
{ {
// It's a book // It's a book
return new Book return new Book
@ -51,12 +51,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
{ {
var bookFiles = args.FileSystemChildren.Where(f => var bookFiles = args.FileSystemChildren.Where(f =>
{ {
var fileExtension = Path.GetExtension(f.FullName) var fileExtension = Path.GetExtension(f.FullName.AsSpan());
?? string.Empty;
return _validExtensions.Contains( return _validExtensions.Contains(
fileExtension, fileExtension,
StringComparer.OrdinalIgnoreCase); StringComparison.OrdinalIgnoreCase);
}).ToList(); }).ToList();
// Don't return a Book if there is more (or less) than one document in the directory // Don't return a Book if there is more (or less) than one document in the directory

View File

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Emby.Naming.Common; using Emby.Naming.Common;

View File

@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var resolver = new Naming.TV.EpisodeResolver(namingOptions); var resolver = new Naming.TV.EpisodeResolver(namingOptions);
var folderName = System.IO.Path.GetFileName(path); var folderName = System.IO.Path.GetFileName(path);
var testPath = "\\\\test\\" + folderName; var testPath = @"\\test\" + folderName;
var episodeInfo = resolver.Resolve(testPath, true); var episodeInfo = resolver.Resolve(testPath, true);

View File

@ -1851,7 +1851,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return; return;
} }
await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
await using (stream.ConfigureAwait(false))
{ {
var settings = new XmlWriterSettings var settings = new XmlWriterSettings
{ {
@ -1860,7 +1861,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
Async = true Async = true
}; };
await using (var writer = XmlWriter.Create(stream, settings)) var writer = XmlWriter.Create(stream, settings);
await using (writer.ConfigureAwait(false))
{ {
await writer.WriteStartDocumentAsync(true).ConfigureAwait(false); await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false); await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false);
@ -1914,7 +1916,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
return; return;
} }
await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
await using (stream.ConfigureAwait(false))
{ {
var settings = new XmlWriterSettings var settings = new XmlWriterSettings
{ {
@ -1927,7 +1930,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
var isSeriesEpisode = timer.IsProgramSeries; var isSeriesEpisode = timer.IsProgramSeries;
await using (var writer = XmlWriter.Create(stream, settings)) var writer = XmlWriter.Create(stream, settings);
await using (writer.ConfigureAwait(false))
{ {
await writer.WriteStartDocumentAsync(true).ConfigureAwait(false); await writer.WriteStartDocumentAsync(true).ConfigureAwait(false);
@ -1965,7 +1969,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
} }
else else
{ {
await writer.WriteStartElementAsync(null, "movie", null); await writer.WriteStartElementAsync(null, "movie", null).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(item.Name)) if (!string.IsNullOrWhiteSpace(item.Name))
{ {

View File

@ -106,8 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
options.Content = JsonContent.Create(requestList, options: _jsonOptions); options.Content = JsonContent.Create(requestList, options: _jsonOptions);
options.Headers.TryAddWithoutValidation("token", token); options.Headers.TryAddWithoutValidation("token", token);
using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false); using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var dailySchedules = await response.Content.ReadFromJsonAsync<IReadOnlyList<DayDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
var dailySchedules = await JsonSerializer.DeserializeAsync<IReadOnlyList<DayDto>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (dailySchedules is null) if (dailySchedules is null)
{ {
return Array.Empty<ProgramInfo>(); return Array.Empty<ProgramInfo>();
@ -122,8 +121,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions); programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false); using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false);
await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var programDetails = await innerResponse.Content.ReadFromJsonAsync<IReadOnlyList<ProgramDetailsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
var programDetails = await JsonSerializer.DeserializeAsync<IReadOnlyList<ProgramDetailsDto>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (programDetails is null) if (programDetails is null)
{ {
return Array.Empty<ProgramInfo>(); return Array.Empty<ProgramInfo>();
@ -482,8 +480,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
try try
{ {
using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false); using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
await using var response = await innerResponse2.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); return await innerResponse2.Content.ReadFromJsonAsync<IReadOnlyList<ShowImagesDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
return await JsonSerializer.DeserializeAsync<IReadOnlyList<ShowImagesDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -510,10 +507,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
try try
{ {
using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false); using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false);
await using var response = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var root = await httpResponse.Content.ReadFromJsonAsync<IReadOnlyList<HeadendsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
var root = await JsonSerializer.DeserializeAsync<IReadOnlyList<HeadendsDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (root is not null) if (root is not null)
{ {
foreach (HeadendsDto headend in root) foreach (HeadendsDto headend in root)
@ -649,8 +643,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false); using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var root = await response.Content.ReadFromJsonAsync<TokenDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
var root = await JsonSerializer.DeserializeAsync<TokenDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (string.Equals(root?.Message, "OK", StringComparison.Ordinal)) if (string.Equals(root?.Message, "OK", StringComparison.Ordinal))
{ {
_logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token); _logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token);
@ -691,10 +684,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
{ {
using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false); using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false);
httpResponse.EnsureSuccessStatusCode(); httpResponse.EnsureSuccessStatusCode();
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var root = await httpResponse.Content.ReadFromJsonAsync<LineupsDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
using var response = httpResponse.Content;
var root = await JsonSerializer.DeserializeAsync<LineupsDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
return root?.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase)) ?? false; return root?.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase)) ?? false;
} }
catch (HttpRequestException ex) catch (HttpRequestException ex)
@ -748,8 +738,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
options.Headers.TryAddWithoutValidation("token", token); options.Headers.TryAddWithoutValidation("token", token);
using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false); using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false);
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var root = await httpResponse.Content.ReadFromJsonAsync<ChannelDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
var root = await JsonSerializer.DeserializeAsync<ChannelDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
if (root is null) if (root is null)
{ {
return new List<ChannelInfo>(); return new List<ChannelInfo>();

View File

@ -17,7 +17,6 @@ using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.LiveTv;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts namespace Emby.Server.Implementations.LiveTv.TunerHosts

View File

@ -9,6 +9,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json; using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -27,7 +28,6 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
@ -76,13 +76,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false);
using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL ?? model.BaseURL + "/lineup.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL ?? model.BaseURL + "/lineup.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var lineup = await response.Content.ReadFromJsonAsync<IEnumerable<Channels>>(_jsonOptions, cancellationToken).ConfigureAwait(false) ?? Enumerable.Empty<Channels>();
var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, _jsonOptions, cancellationToken)
.ConfigureAwait(false) ?? new List<Channels>();
if (info.ImportFavoritesOnly) if (info.ImportFavoritesOnly)
{ {
lineup = lineup.Where(i => i.Favorite).ToList(); lineup = lineup.Where(i => i.Favorite);
} }
return lineup.Where(i => !i.DRM).ToList(); return lineup.Where(i => !i.DRM).ToList();
@ -129,9 +126,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
.GetAsync(GetApiUrl(info) + "/discover.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken) .GetAsync(GetApiUrl(info) + "/discover.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); var discoverResponse = await response.Content.ReadFromJsonAsync<DiscoverResponse>(_jsonOptions, cancellationToken).ConfigureAwait(false);
var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, _jsonOptions, cancellationToken)
.ConfigureAwait(false);
if (!string.IsNullOrEmpty(cacheKey)) if (!string.IsNullOrEmpty(cacheKey))
{ {
@ -175,9 +170,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
using var response = await _httpClientFactory.CreateClient(NamedClient.Default) using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
.GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken) .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
.ConfigureAwait(false); .ConfigureAwait(false);
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
var tuners = new List<LiveTvTunerInfo>(); var tuners = new List<LiveTvTunerInfo>();
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
await using (stream.ConfigureAwait(false))
{
using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false)) await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false))
{ {
string stripedLine = StripXML(line); string stripedLine = StripXML(line);
@ -205,6 +202,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}); });
} }
} }
}
return tuners; return tuners;
} }

View File

@ -44,8 +44,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
StopStreaming(socket).GetAwaiter().GetResult(); StopStreaming(socket).GetAwaiter().GetResult();
} }
} }
GC.SuppressFinalize(this);
} }
public async Task<bool> CheckTunerAvailability(IPAddress remoteIP, int tuner, CancellationToken cancellationToken) public async Task<bool> CheckTunerAvailability(IPAddress remoteIP, int tuner, CancellationToken cancellationToken)

View File

@ -5,7 +5,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
@ -22,7 +21,6 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;

View File

@ -112,6 +112,8 @@
"TaskCleanLogsDescription": "Deletes log files that are more than {0} days old.", "TaskCleanLogsDescription": "Deletes log files that are more than {0} days old.",
"TaskRefreshPeople": "Refresh People", "TaskRefreshPeople": "Refresh People",
"TaskRefreshPeopleDescription": "Updates metadata for actors and directors in your media library.", "TaskRefreshPeopleDescription": "Updates metadata for actors and directors in your media library.",
"TaskRefreshTrickplayImages": "Generate Trickplay Images",
"TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries.",
"TaskUpdatePlugins": "Update Plugins", "TaskUpdatePlugins": "Update Plugins",
"TaskUpdatePluginsDescription": "Downloads and installs updates for plugins that are configured to update automatically.", "TaskUpdatePluginsDescription": "Downloads and installs updates for plugins that are configured to update automatically.",
"TaskCleanTranscode": "Clean Transcode Directory", "TaskCleanTranscode": "Clean Transcode Directory",

View File

@ -0,0 +1,18 @@
{
"Artists": "Listafólk",
"Collections": "Søvn",
"Default": "Sjálvgildi",
"DeviceOfflineWithName": "{0} hevur slitið sambandið",
"External": "Ytri",
"Genres": "Greinar",
"Albums": "Album",
"AppDeviceValues": "App: {0}, Eind: {1}",
"Application": "Nýtsluskipan",
"Books": "Bøkur",
"Channels": "Rásir",
"ChapterNameValue": "Kapittul {0}",
"DeviceOnlineWithName": "{0} er sambundið",
"Favorites": "Yndis",
"Folders": "Mappur",
"Forced": "Kravt"
}

View File

@ -13,8 +13,8 @@
"HeaderFavoriteArtists": "Uppáhalds Listamenn", "HeaderFavoriteArtists": "Uppáhalds Listamenn",
"HeaderFavoriteAlbums": "Uppáhalds Plötur", "HeaderFavoriteAlbums": "Uppáhalds Plötur",
"HeaderContinueWatching": "Halda áfram að horfa", "HeaderContinueWatching": "Halda áfram að horfa",
"HeaderAlbumArtists": "Höfundur plötu", "HeaderAlbumArtists": "Listamaður á umslagi",
"Genres": "Tegundir", "Genres": "Stefnur",
"Folders": "Möppur", "Folders": "Möppur",
"Favorites": "Uppáhalds", "Favorites": "Uppáhalds",
"FailedLoginAttemptWithUserName": "{0} reyndi að auðkenna sig", "FailedLoginAttemptWithUserName": "{0} reyndi að auðkenna sig",
@ -22,32 +22,32 @@
"DeviceOfflineWithName": "{0} hefur aftengst", "DeviceOfflineWithName": "{0} hefur aftengst",
"Collections": "Söfn", "Collections": "Söfn",
"ChapterNameValue": "Kafli {0}", "ChapterNameValue": "Kafli {0}",
"Channels": "Stöðvar", "Channels": "Rásir",
"CameraImageUploadedFrom": "Ný ljósmynd frá myndavél hefur verið hlaðið upp frá {0}", "CameraImageUploadedFrom": "{0} hefur hlaðið upp nýrri ljósmynd úr myndavél sinni",
"Books": "Bækur", "Books": "Bækur",
"AuthenticationSucceededWithUserName": "{0} auðkenning tókst", "AuthenticationSucceededWithUserName": "Auðkenning fyrir {0} tókst",
"Artists": "Listamaður", "Artists": "Listamenn",
"Application": "Forrit", "Application": "Forrit",
"AppDeviceValues": "Snjallforrit: {0}, Tæki: {1}", "AppDeviceValues": "Snjallforrit: {0}, Tæki: {1}",
"Albums": "Plötur", "Albums": "Plötur",
"Plugin": "Viðbót", "Plugin": "Viðbótarvirkni",
"Photos": "Myndir", "Photos": "Ljósmyndir",
"NotificationOptionVideoPlaybackStopped": "Myndbandafspilun stöðvuð", "NotificationOptionVideoPlaybackStopped": "Myndbandsafspilun stöðvuð",
"NotificationOptionVideoPlayback": "Myndbandafspilun hafin", "NotificationOptionVideoPlayback": "Myndbandsafspilun hafin",
"NotificationOptionUserLockedOut": "Notandi læstur úti", "NotificationOptionUserLockedOut": "Notandi læstur úti",
"NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynileg", "NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynleg",
"NotificationOptionPluginUpdateInstalled": "Viðbótar uppfærsla uppsett", "NotificationOptionPluginUpdateInstalled": "Uppfærslu á viðbótarvirkni lokið",
"NotificationOptionPluginUninstalled": "Viðbót fjarlægð", "NotificationOptionPluginUninstalled": "Viðbótarvirkni fjarlægð",
"NotificationOptionPluginInstalled": "Viðbót sett upp", "NotificationOptionPluginInstalled": "Viðbótarvirkni sett upp",
"NotificationOptionPluginError": "Bilun í viðbót", "NotificationOptionPluginError": "Bilun í viðbót",
"NotificationOptionInstallationFailed": "Uppsetning tókst ekki", "NotificationOptionInstallationFailed": "Uppsetning tókst ekki",
"NotificationOptionCameraImageUploaded": "Myndavélarmynd hlaðið upp", "NotificationOptionCameraImageUploaded": "Ljósmynd hlaðið upp",
"NotificationOptionAudioPlaybackStopped": "Hljóðafspilun stöðvuð", "NotificationOptionAudioPlaybackStopped": "Hljóðafspilun stöðvuð",
"NotificationOptionAudioPlayback": "Hljóðafspilun hafin", "NotificationOptionAudioPlayback": "Hljóðafspilun hafin",
"NotificationOptionApplicationUpdateInstalled": "Uppfærsla uppsett", "NotificationOptionApplicationUpdateInstalled": "Uppfærsla uppsett",
"NotificationOptionApplicationUpdateAvailable": "Uppfærsla í boði", "NotificationOptionApplicationUpdateAvailable": "Uppfærsla í boði",
"NameSeasonUnknown": "Sería óþekkt", "NameSeasonUnknown": "Þáttaröð óþekkt",
"NameSeasonNumber": "Sería {0}", "NameSeasonNumber": "Þáttaröð {0}",
"MixedContent": "Blandað efni", "MixedContent": "Blandað efni",
"MessageServerConfigurationUpdated": "Stillingar þjóns hafa verið uppfærðar", "MessageServerConfigurationUpdated": "Stillingar þjóns hafa verið uppfærðar",
"MessageApplicationUpdatedTo": "Jellyfin þjónn hefur verið uppfærður í {0}", "MessageApplicationUpdatedTo": "Jellyfin þjónn hefur verið uppfærður í {0}",
@ -57,24 +57,24 @@
"User": "Notandi", "User": "Notandi",
"System": "Kerfi", "System": "Kerfi",
"NotificationOptionNewLibraryContent": "Nýju efni bætt við", "NotificationOptionNewLibraryContent": "Nýju efni bætt við",
"NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er fáanleg til niðurhals.", "NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er tilbúin til niðurhals.",
"NameInstallFailed": "{0} uppsetning mistókst", "NameInstallFailed": "{0} uppsetning mistókst",
"MusicVideos": "Tónlistarmyndbönd", "MusicVideos": "Tónlistarmyndbönd",
"Music": "Tónlist", "Music": "Tónlist",
"Movies": "Kvikmyndir", "Movies": "Kvikmyndir",
"UserDeletedWithName": "Notanda {0} hefur verið eytt", "UserDeletedWithName": "Notanda {0} hefur verið eytt",
"UserCreatedWithName": "Notandi {0} hefur verið stofnaður", "UserCreatedWithName": "Notandi {0} hefur verið stofnaður",
"TvShows": "Þættir", "TvShows": "Sjónvarpsþættir",
"Sync": "Samstilla", "Sync": "Samstilla",
"Songs": "Lög", "Songs": "Lög",
"ServerNameNeedsToBeRestarted": "{0} þarf að endurræsa", "ServerNameNeedsToBeRestarted": "{0} þarf að vera endurræstur",
"ScheduledTaskStartedWithName": "{0} hafin", "ScheduledTaskStartedWithName": "{0} hafin",
"ScheduledTaskFailedWithName": "{0} mistókst", "ScheduledTaskFailedWithName": "{0} mistókst",
"PluginUpdatedWithName": "{0} var uppfært", "PluginUpdatedWithName": "{0} var uppfært",
"PluginUninstalledWithName": "{0} var fjarlægt", "PluginUninstalledWithName": "{0} var fjarlægt",
"PluginInstalledWithName": "{0} var sett upp", "PluginInstalledWithName": "{0} var sett upp",
"NotificationOptionTaskFailed": "Tímasett verkefni mistókst", "NotificationOptionTaskFailed": "Tímasett verkefni mistókst",
"StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að hlaðast. Vinsamlega prufaðu aftur fljótlega.", "StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að ræsa sig upp. Vinsamlegast reyndu aftur fljótlega.",
"VersionNumber": "Útgáfa {0}", "VersionNumber": "Útgáfa {0}",
"ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt", "ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt",
"UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}", "UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}",
@ -83,14 +83,14 @@
"UserPasswordChangedWithName": "Lykilorði fyrir notandann {0} hefur verið breytt", "UserPasswordChangedWithName": "Lykilorði fyrir notandann {0} hefur verið breytt",
"UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}", "UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}",
"UserOfflineFromDevice": "{0} hefur aftengst frá {1}", "UserOfflineFromDevice": "{0} hefur aftengst frá {1}",
"UserLockedOutWithName": "Notanda {0} hefur verið heflaður aðgangur", "UserLockedOutWithName": "Notandi {0} hefur verið læstur úti",
"UserDownloadingItemWithValues": "{0} Hleður niður {1}", "UserDownloadingItemWithValues": "{0} hleður niður {1}",
"SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}", "SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}",
"ProviderValue": "Veitandi: {0}", "ProviderValue": "Efnisveita: {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón", "MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón",
"ValueSpecialEpisodeName": "Sérstakt - {0}", "ValueSpecialEpisodeName": "Sérstaktur - {0}",
"Shows": "Sýningar", "Shows": "Þættir",
"Playlists": "Spilunarlisti", "Playlists": "Efnisskrár",
"TaskRefreshChannelsDescription": "Endurhlaða upplýsingum netrása.", "TaskRefreshChannelsDescription": "Endurhlaða upplýsingum netrása.",
"TaskRefreshChannels": "Endurhlaða Rásir", "TaskRefreshChannels": "Endurhlaða Rásir",
"TaskCleanTranscodeDescription": "Eyða umkóðuðum skrám sem eru meira en einum degi eldri.", "TaskCleanTranscodeDescription": "Eyða umkóðuðum skrám sem eru meira en einum degi eldri.",
@ -116,5 +116,12 @@
"TaskCleanLogsDescription": "Eyðir færslu skrám sem eru meira en {0} gömul.", "TaskCleanLogsDescription": "Eyðir færslu skrám sem eru meira en {0} gömul.",
"TaskCleanLogs": "Hreinsa færslu skrá", "TaskCleanLogs": "Hreinsa færslu skrá",
"TaskDownloadMissingSubtitlesDescription": "Leitar á netinu að texta sem vantar miðað við uppsetningu lýsigagna.", "TaskDownloadMissingSubtitlesDescription": "Leitar á netinu að texta sem vantar miðað við uppsetningu lýsigagna.",
"HearingImpaired": "Heyrnarskertur" "HearingImpaired": "Heyrnarskertur",
"TaskOptimizeDatabaseDescription": "Þjappar gagnagrunni og bætir við lausu diskaplássi. Að keyra þessa aðgerð eftir skönnun safnsins, eða eftir einhverjar breytingar sem fela í sér gagnagrunnsbreytingar, gætu aukið hraðvirkni.",
"TaskKeyframeExtractor": "Lykilrammaplokkari",
"TaskKeyframeExtractorDescription": "Plokkar lykilramma úr myndbandsskrám til að búa til nákvæmari HLS uppskiptingarlista. Þetta verk getur tekið langan tíma.",
"TaskRefreshChapterImages": "Plokka kafla-myndir",
"TaskCleanActivityLogDescription": "Eyðir virkniskráningarfærslum sem hafa náð settum hámarksaldri.",
"Forced": "Þvingað",
"External": "Útvær"
} }

View File

@ -1,7 +1,7 @@
{ {
"ServerNameNeedsToBeRestarted": "{0} ir vajadzīgs restarts", "ServerNameNeedsToBeRestarted": "{0} ir vajadzīgs restarts",
"NotificationOptionTaskFailed": "Plānota uzdevuma kļūme", "NotificationOptionTaskFailed": "Plānota uzdevuma kļūme",
"HeaderRecordingGroups": "Ierakstu Grupas", "HeaderRecordingGroups": "Ierakstu grupas",
"UserPolicyUpdatedWithName": "Lietotāju politika atjaunota priekš {0}", "UserPolicyUpdatedWithName": "Lietotāju politika atjaunota priekš {0}",
"SubtitleDownloadFailureFromForItem": "Subtitru lejupielāde no {0} priekš {1} neizdevās", "SubtitleDownloadFailureFromForItem": "Subtitru lejupielāde no {0} priekš {1} neizdevās",
"NotificationOptionVideoPlaybackStopped": "Video atskaņošana apturēta", "NotificationOptionVideoPlaybackStopped": "Video atskaņošana apturēta",
@ -14,7 +14,7 @@
"Photos": "Attēli", "Photos": "Attēli",
"NotificationOptionUserLockedOut": "Lietotājs bloķēts", "NotificationOptionUserLockedOut": "Lietotājs bloķēts",
"LabelRunningTimeValue": "Garums: {0}", "LabelRunningTimeValue": "Garums: {0}",
"Inherit": "Mantot", "Inherit": "Pārmantot",
"AppDeviceValues": "Lietotne: {0}, Ierīce: {1}", "AppDeviceValues": "Lietotne: {0}, Ierīce: {1}",
"VersionNumber": "Versija {0}", "VersionNumber": "Versija {0}",
"ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots jūsu multvides bibliotēkai", "ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots jūsu multvides bibliotēkai",
@ -28,7 +28,7 @@
"UserDeletedWithName": "Lietotājs {0} ir izdzēsts", "UserDeletedWithName": "Lietotājs {0} ir izdzēsts",
"UserCreatedWithName": "Lietotājs {0} ir ticis izveidots", "UserCreatedWithName": "Lietotājs {0} ir ticis izveidots",
"User": "Lietotājs", "User": "Lietotājs",
"TvShows": "TV Raidījumi", "TvShows": "TV raidījumi",
"Sync": "Sinhronizācija", "Sync": "Sinhronizācija",
"System": "Sistēma", "System": "Sistēma",
"StartupEmbyServerIsLoading": "Jellyfin Serveris lādējas. Lūdzu mēģiniet vēlreiz pēc brīža.", "StartupEmbyServerIsLoading": "Jellyfin Serveris lādējas. Lūdzu mēģiniet vēlreiz pēc brīža.",
@ -38,11 +38,11 @@
"PluginUninstalledWithName": "{0} tika noņemts", "PluginUninstalledWithName": "{0} tika noņemts",
"PluginInstalledWithName": "{0} tika uzstādīts", "PluginInstalledWithName": "{0} tika uzstādīts",
"Plugin": "Paplašinājums", "Plugin": "Paplašinājums",
"Playlists": "Atskaņošanas Saraksti", "Playlists": "Atskaņošanas saraksti",
"MixedContent": "Jaukts saturs", "MixedContent": "Jaukts saturs",
"HomeVideos": "Mājas Video", "HomeVideos": "Mājas video",
"HeaderNextUp": "Nākamais", "HeaderNextUp": "Nākamais",
"ChapterNameValue": "Nodaļa {0}", "ChapterNameValue": "{0}. nodaļa",
"Application": "Lietotne", "Application": "Lietotne",
"NotificationOptionServerRestartRequired": "Vajadzīgs servera restarts", "NotificationOptionServerRestartRequired": "Vajadzīgs servera restarts",
"NotificationOptionPluginUpdateInstalled": "Paplašinājuma atjauninājums uzstādīts", "NotificationOptionPluginUpdateInstalled": "Paplašinājuma atjauninājums uzstādīts",
@ -56,14 +56,14 @@
"NotificationOptionApplicationUpdateInstalled": "Lietotnes atjauninājums uzstādīts", "NotificationOptionApplicationUpdateInstalled": "Lietotnes atjauninājums uzstādīts",
"NotificationOptionApplicationUpdateAvailable": "Lietotnes atjauninājums pieejams", "NotificationOptionApplicationUpdateAvailable": "Lietotnes atjauninājums pieejams",
"NewVersionIsAvailable": "Lejupielādei ir pieejama jauna Jellyfin Server versija.", "NewVersionIsAvailable": "Lejupielādei ir pieejama jauna Jellyfin Server versija.",
"NameSeasonUnknown": "Nezināma Sezona", "NameSeasonUnknown": "Nezināma sezona",
"NameSeasonNumber": "Sezona {0}", "NameSeasonNumber": "{0}. sezona",
"NameInstallFailed": "{0} instalācija neizdevās", "NameInstallFailed": "{0} instalācija neizdevās",
"MusicVideos": "Mūzikas video", "MusicVideos": "Mūzikas video",
"Music": "Mūzika", "Music": "Mūzika",
"Movies": "Filmas", "Movies": "Filmas",
"MessageServerConfigurationUpdated": "Servera konfigurācija ir tikusi atjaunota", "MessageServerConfigurationUpdated": "Servera konfigurācija ir tikusi atjaunota",
"MessageNamedServerConfigurationUpdatedWithValue": "Servera konfigurācijas sadaļa {0} ir tikusi atjaunota", "MessageNamedServerConfigurationUpdatedWithValue": "Servera konfigurācijas sadaļa {0} tika atjaunota",
"MessageApplicationUpdatedTo": "Jellyfin Server ir ticis atjaunots uz {0}", "MessageApplicationUpdatedTo": "Jellyfin Server ir ticis atjaunots uz {0}",
"MessageApplicationUpdated": "Jellyfin Server ir ticis atjaunots", "MessageApplicationUpdated": "Jellyfin Server ir ticis atjaunots",
"Latest": "Jaunākais", "Latest": "Jaunākais",
@ -71,57 +71,57 @@
"ItemRemovedWithName": "{0} tika noņemts no bibliotēkas", "ItemRemovedWithName": "{0} tika noņemts no bibliotēkas",
"ItemAddedWithName": "{0} tika pievienots bibliotēkai", "ItemAddedWithName": "{0} tika pievienots bibliotēkai",
"HeaderLiveTV": "Tiešraides TV", "HeaderLiveTV": "Tiešraides TV",
"HeaderContinueWatching": "Turpināt Skatīšanos", "HeaderContinueWatching": "Turpināt skatīšanos",
"HeaderAlbumArtists": "Albumu Izpildītāji", "HeaderAlbumArtists": "Albumu izpildītāji",
"Genres": "Žanri", "Genres": "Žanri",
"Folders": "Mapes", "Folders": "Mapes",
"Favorites": "Favorīti", "Favorites": "Izlase",
"FailedLoginAttemptWithUserName": "Neizdevies pieslēgšanās mēģinājums no {0}", "FailedLoginAttemptWithUserName": "Neizdevies ieiešanas mēģinājums no {0}",
"DeviceOnlineWithName": "{0} ir pievienojies", "DeviceOnlineWithName": "Savienojums ar {0} ir izveidots",
"DeviceOfflineWithName": "{0} ir atvienojies", "DeviceOfflineWithName": "Savienojums ar {0} ir pārtraukts",
"Collections": "Kolekcijas", "Collections": "Kolekcijas",
"Channels": "Kanāli", "Channels": "Kanāli",
"CameraImageUploadedFrom": "Jauns kameras attēls ir ticis augšupielādēts no {0}", "CameraImageUploadedFrom": "Jauns kameras attēls tika augšupielādēts no {0}",
"Books": "Grāmatas", "Books": "Grāmatas",
"Artists": "Izpildītāji", "Artists": "Izpildītāji",
"Albums": "Albumi", "Albums": "Albumi",
"ProviderValue": "Provider: {0}", "ProviderValue": "Provider: {0}",
"HeaderFavoriteSongs": "Dziesmu Favorīti", "HeaderFavoriteSongs": "Dziesmu izlase",
"HeaderFavoriteShows": "Raidījumu Favorīti", "HeaderFavoriteShows": "Raidījumu izlase",
"HeaderFavoriteEpisodes": "Episožu Favorīti", "HeaderFavoriteEpisodes": "Sēriju izlase",
"HeaderFavoriteArtists": "Izpildītāju Favorīti", "HeaderFavoriteArtists": "Izpildītāju izlase",
"HeaderFavoriteAlbums": "Albumu Favorīti", "HeaderFavoriteAlbums": "Albumu izlase",
"TaskCleanCacheDescription": "Nodzēš keša datnes, kas vairs nav sistēmai vajadzīgas.", "TaskCleanCacheDescription": "Nodzēš kešatmiņas datnes, kas vairs nav sistēmai vajadzīgas.",
"TaskRefreshChapterImages": "Izvilkt Nodaļu Attēlus", "TaskRefreshChapterImages": "Izvilkt nodaļu attēlus",
"TasksApplicationCategory": "Lietotne", "TasksApplicationCategory": "Lietotne",
"TasksLibraryCategory": "Bibliotēka", "TasksLibraryCategory": "Bibliotēka",
"TaskDownloadMissingSubtitlesDescription": "Internetā meklē trūkstošus subtitrus balstoties uz metadatu uzstādījumiem.", "TaskDownloadMissingSubtitlesDescription": "Internetā meklē trūkstošus subtitrus balstoties uz metadatu uzstādījumiem.",
"TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošus subtitrus", "TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošos subtitrus",
"TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.", "TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.",
"TaskRefreshChannels": "Atjaunot Kanālus", "TaskRefreshChannels": "Atjaunot kanālus",
"TaskCleanTranscodeDescription": "Izdzēš trans-kodēšanas datnes, kas ir vecākas par vienu dienu.", "TaskCleanTranscodeDescription": "Izdzēš transkodēšanas datnes, kas ir senākas par vienu dienu.",
"TaskCleanTranscode": "Iztīrīt Trans-kodēšanas Mapi", "TaskCleanTranscode": "Iztīrīt transkodēšanas mapi",
"TaskUpdatePluginsDescription": "Lejupielādē un uzstāda atjauninājumus paplašinājumiem, kam ir uzstādīta automātiskā atjaunināšana.", "TaskUpdatePluginsDescription": "Lejupielādē un uzstāda atjauninājumus paplašinājumiem, kam ir uzstādīta automātiskā atjaunināšana.",
"TaskUpdatePlugins": "Atjaunot Paplašinājumus", "TaskUpdatePlugins": "Atjaunot paplašinājumus",
"TaskRefreshPeopleDescription": "Atjauno metadatus aktieriem un direktoriem jūsu multivides bibliotēkā.", "TaskRefreshPeopleDescription": "Atjauno metadatus aktieriem un direktoriem jūsu multivides bibliotēkā.",
"TaskRefreshPeople": "Atjaunot Cilvēkus", "TaskRefreshPeople": "Atjaunot cilvēkus",
"TaskCleanLogsDescription": "Nodzēš log datnes, kas ir vairāk par {0} dienām vecas.", "TaskCleanLogsDescription": "Nodzēš logdatnes, kas ir senākas par {0} dienām.",
"TaskCleanLogs": "Iztīrīt Logdatņu Mapi", "TaskCleanLogs": "Iztīrīt logdatņu mapi",
"TaskRefreshLibraryDescription": "Skenē jūsu multivides bibliotēku, lai atrastu jaunas datnes, un atsvaidzina metadatus.", "TaskRefreshLibraryDescription": "Skenē jūsu multivides bibliotēku, lai atrastu jaunas datnes, un atsvaidzina metadatus.",
"TaskRefreshLibrary": "Skenēt Multivides Bibliotēku", "TaskRefreshLibrary": "Skenēt multivides bibliotēku",
"TaskRefreshChapterImagesDescription": "Izveido sīktēlus priekš video ar sadaļām.", "TaskRefreshChapterImagesDescription": "Izveido sīktēlus priekš video ar sadaļām.",
"TaskCleanCache": "Iztīrīt Kešošanas Mapi", "TaskCleanCache": "Iztīrīt kešatmiņas mapi",
"TasksChannelsCategory": "Interneta Kanāli", "TasksChannelsCategory": "Interneta kanāli",
"TasksMaintenanceCategory": "Apkope", "TasksMaintenanceCategory": "Apkope",
"Forced": "Piespiests", "Forced": "Piespiedu",
"TaskCleanActivityLogDescription": "Nodzēš darbību žurnāla ierakstus, kuri ir vecāki par doto vecumu.", "TaskCleanActivityLogDescription": "Nodzēš darbību žurnāla ierakstus, kuri ir vecāki par doto vecumu.",
"TaskCleanActivityLog": "Notīrīt Darbību Žurnālu", "TaskCleanActivityLog": "Notīrīt darbību žurnālu",
"Undefined": "Nenoteikts", "Undefined": "Nenoteikts",
"Default": "Noklusējuma", "Default": "Noklusējuma",
"TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Uzdevum palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.", "TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Šī uzdevuma palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.",
"TaskOptimizeDatabase": "Optimizēt datubāzi", "TaskOptimizeDatabase": "Optimizēt datubāzi",
"External": "Ārējais", "External": "Ārējais",
"HearingImpaired": "Ar dzirdes traucējumiem", "HearingImpaired": "Ar dzirdes traucējumiem",
"TaskKeyframeExtractor": "Atslēgkadru Ekstraktors", "TaskKeyframeExtractor": "Atslēgkadru ekstraktors",
"TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs." "TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs."
} }

View File

@ -121,5 +121,7 @@
"TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്‌കാൻ ചെയ്‌തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്‌ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്‌തതിന് ശേഷം ഈ ടാസ്‌ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.", "TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്‌കാൻ ചെയ്‌തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്‌ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്‌തതിന് ശേഷം ഈ ടാസ്‌ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
"TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക", "TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക",
"HearingImpaired": "കേൾവി തകരാറുകൾ", "HearingImpaired": "കേൾവി തകരാറുകൾ",
"External": "പുറമേയുള്ള" "External": "പുറമേയുള്ള",
"TaskKeyframeExtractorDescription": "കൂടുതൽ കൃത്യമായ HLS പ്ലേലിസ്റ്റുകൾ സൃഷ്‌ടിക്കുന്നതിന് വീഡിയോ ഫയലുകളിൽ നിന്ന് കീഫ്രെയിമുകൾ എക്‌സ്‌ട്രാക്‌റ്റ് ചെയ്യുന്നു. ഈ പ്രവർത്തനം പൂർത്തിയാവാൻ കുറച്ചധികം സമയം എടുത്തേക്കാം.",
"TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ"
} }

View File

@ -0,0 +1 @@
{}

View File

@ -124,5 +124,5 @@
"TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS playlistov. Táto úloha môže trvať dlhšiu dobu.", "TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS playlistov. Táto úloha môže trvať dlhšiu dobu.",
"TaskKeyframeExtractor": "Extraktor kľúčových snímkov", "TaskKeyframeExtractor": "Extraktor kľúčových snímkov",
"External": "Externé", "External": "Externé",
"HearingImpaired": "Sluchovo Postihnutý" "HearingImpaired": "Sluchovo postihnutí"
} }

View File

@ -71,8 +71,10 @@ namespace Emby.Server.Implementations.Localization
string countryCode = resource.Substring(RatingsPath.Length, 2); string countryCode = resource.Substring(RatingsPath.Length, 2);
var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase); var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
await using var stream = _assembly.GetManifestResourceStream(resource); var stream = _assembly.GetManifestResourceStream(resource);
using var reader = new StreamReader(stream!); // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames() await using (stream!.ConfigureAwait(false)) // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames()
{
using var reader = new StreamReader(stream!);
await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false)) await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
{ {
if (string.IsNullOrWhiteSpace(line)) if (string.IsNullOrWhiteSpace(line))
@ -92,6 +94,7 @@ namespace Emby.Server.Implementations.Localization
_logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode); _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
} }
} }
}
_allParentalRatings[countryCode] = dict; _allParentalRatings[countryCode] = dict;
} }

View File

@ -222,7 +222,7 @@ namespace Emby.Server.Implementations.MediaEncoder
{ {
var deadImages = images var deadImages = images
.Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase) .Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase)
.Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase))
.ToList(); .ToList();
foreach (var image in deadImages) foreach (var image in deadImages)

View File

@ -1,12 +1,15 @@
#pragma warning disable CS1591
using System; using System;
using System.Linq;
using System.Net; using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets; using System.Net.Sockets;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
namespace Emby.Server.Implementations.Net namespace Emby.Server.Implementations.Net
{ {
/// <summary>
/// Factory class to create different kinds of sockets.
/// </summary>
public class SocketFactory : ISocketFactory public class SocketFactory : ISocketFactory
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -29,7 +32,7 @@ namespace Emby.Server.Implementations.Net
} }
catch catch
{ {
socket?.Dispose(); socket.Dispose();
throw; throw;
} }
@ -38,7 +41,8 @@ namespace Emby.Server.Implementations.Net
/// <inheritdoc /> /// <inheritdoc />
public Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort) public Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort)
{ {
ArgumentNullException.ThrowIfNull(bindInterface.Address); var interfaceAddress = bindInterface.Address;
ArgumentNullException.ThrowIfNull(interfaceAddress);
if (localPort < 0) if (localPort < 0)
{ {
@ -49,13 +53,13 @@ namespace Emby.Server.Implementations.Net
try try
{ {
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
socket.Bind(new IPEndPoint(bindInterface.Address, localPort)); socket.Bind(new IPEndPoint(interfaceAddress, localPort));
return socket; return socket;
} }
catch catch
{ {
socket?.Dispose(); socket.Dispose();
throw; throw;
} }
@ -82,22 +86,31 @@ namespace Emby.Server.Implementations.Net
try try
{ {
var interfaceIndex = bindInterface.Index;
var interfaceIndexSwapped = (int)IPAddress.HostToNetworkOrder(interfaceIndex);
socket.MulticastLoopback = false; socket.MulticastLoopback = false;
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true); socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true);
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive); socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive);
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastInterface, interfaceIndexSwapped);
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex)); if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
{
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress));
socket.Bind(new IPEndPoint(multicastAddress, localPort)); socket.Bind(new IPEndPoint(multicastAddress, localPort));
}
else
{
// Only create socket if interface supports multicast
var interfaceIndex = bindInterface.Index;
var interfaceIndexSwapped = IPAddress.HostToNetworkOrder(interfaceIndex);
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex));
socket.Bind(new IPEndPoint(bindIPAddress, localPort));
}
return socket; return socket;
} }
catch catch
{ {
socket?.Dispose(); socket.Dispose();
throw; throw;
} }

View File

@ -327,9 +327,9 @@ namespace Emby.Server.Implementations.Playlists
// this is probably best done as a metadata provider // this is probably best done as a metadata provider
// saving a file over itself will require some work to prevent this from happening when not needed // saving a file over itself will require some work to prevent this from happening when not needed
var playlistPath = item.Path; var playlistPath = item.Path;
var extension = Path.GetExtension(playlistPath); var extension = Path.GetExtension(playlistPath.AsSpan());
if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase)) if (extension.Equals(".wpl", StringComparison.OrdinalIgnoreCase))
{ {
var playlist = new WplPlaylist(); var playlist = new WplPlaylist();
foreach (var child in item.GetLinkedChildren()) foreach (var child in item.GetLinkedChildren())
@ -362,8 +362,7 @@ namespace Emby.Server.Implementations.Playlists
string text = new WplContent().ToText(playlist); string text = new WplContent().ToText(playlist);
File.WriteAllText(playlistPath, text); File.WriteAllText(playlistPath, text);
} }
else if (extension.Equals(".zpl", StringComparison.OrdinalIgnoreCase))
if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
{ {
var playlist = new ZplPlaylist(); var playlist = new ZplPlaylist();
foreach (var child in item.GetLinkedChildren()) foreach (var child in item.GetLinkedChildren())
@ -396,8 +395,7 @@ namespace Emby.Server.Implementations.Playlists
string text = new ZplContent().ToText(playlist); string text = new ZplContent().ToText(playlist);
File.WriteAllText(playlistPath, text); File.WriteAllText(playlistPath, text);
} }
else if (extension.Equals(".m3u", StringComparison.OrdinalIgnoreCase))
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
{ {
var playlist = new M3uPlaylist var playlist = new M3uPlaylist
{ {
@ -428,8 +426,7 @@ namespace Emby.Server.Implementations.Playlists
string text = new M3uContent().ToText(playlist); string text = new M3uContent().ToText(playlist);
File.WriteAllText(playlistPath, text); File.WriteAllText(playlistPath, text);
} }
else if (extension.Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
{ {
var playlist = new M3uPlaylist(); var playlist = new M3uPlaylist();
playlist.IsExtended = true; playlist.IsExtended = true;
@ -458,8 +455,7 @@ namespace Emby.Server.Implementations.Playlists
string text = new M3uContent().ToText(playlist); string text = new M3uContent().ToText(playlist);
File.WriteAllText(playlistPath, text); File.WriteAllText(playlistPath, text);
} }
else if (extension.Equals(".pls", StringComparison.OrdinalIgnoreCase))
if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
{ {
var playlist = new PlsPlaylist(); var playlist = new PlsPlaylist();
foreach (var child in item.GetLinkedChildren()) foreach (var child in item.GetLinkedChildren())

View File

@ -386,11 +386,11 @@ namespace Emby.Server.Implementations.Plugins
var url = new Uri(packageInfo.ImageUrl); var url = new Uri(packageInfo.ImageUrl);
imagePath = Path.Join(path, url.Segments[^1]); imagePath = Path.Join(path, url.Segments[^1]);
await using var fileStream = AsyncFile.OpenWrite(imagePath); var fileStream = AsyncFile.OpenWrite(imagePath);
Stream? downloadStream = null;
try try
{ {
await using var downloadStream = await HttpClientFactory downloadStream = await HttpClientFactory
.CreateClient(NamedClient.Default) .CreateClient(NamedClient.Default)
.GetStreamAsync(url) .GetStreamAsync(url)
.ConfigureAwait(false); .ConfigureAwait(false);
@ -402,6 +402,14 @@ namespace Emby.Server.Implementations.Plugins
_logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath); _logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath);
imagePath = string.Empty; imagePath = string.Empty;
} }
finally
{
await fileStream.DisposeAsync().ConfigureAwait(false);
if (downloadStream is not null)
{
await downloadStream.DisposeAsync().ConfigureAwait(false);
}
}
} }
var manifest = new PluginManifest var manifest = new PluginManifest
@ -421,7 +429,7 @@ namespace Emby.Server.Implementations.Plugins
ImagePath = imagePath ImagePath = imagePath
}; };
if (!await ReconcileManifest(manifest, path)) if (!await ReconcileManifest(manifest, path).ConfigureAwait(false))
{ {
// An error occurred during reconciliation and saving could be undesirable. // An error occurred during reconciliation and saving could be undesirable.
return false; return false;
@ -458,7 +466,7 @@ namespace Emby.Server.Implementations.Plugins
} }
using var metaStream = File.OpenRead(metafile); using var metaStream = File.OpenRead(metafile);
var localManifest = await JsonSerializer.DeserializeAsync<PluginManifest>(metaStream, _jsonOptions); var localManifest = await JsonSerializer.DeserializeAsync<PluginManifest>(metaStream, _jsonOptions).ConfigureAwait(false);
localManifest ??= new PluginManifest(); localManifest ??= new PluginManifest();
if (!Equals(localManifest.Id, manifest.Id)) if (!Equals(localManifest.Id, manifest.Id))

View File

@ -115,7 +115,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
{ {
try try
{ {
previouslyFailedImages = File.ReadAllText(failHistoryPath) previouslyFailedImages = (await File.ReadAllTextAsync(failHistoryPath, cancellationToken).ConfigureAwait(false))
.Split('|', StringSplitOptions.RemoveEmptyEntries) .Split('|', StringSplitOptions.RemoveEmptyEntries)
.ToList(); .ToList();
} }
@ -156,7 +156,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
} }
string text = string.Join('|', previouslyFailedImages); string text = string.Join('|', previouslyFailedImages);
File.WriteAllText(failHistoryPath, text); await File.WriteAllTextAsync(failHistoryPath, text, cancellationToken).ConfigureAwait(false);
} }
numComplete++; numComplete++;

View File

@ -0,0 +1,104 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Updates;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.System;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Hosting;
namespace Emby.Server.Implementations;
/// <inheritdoc />
public class SystemManager : ISystemManager
{
private readonly IHostApplicationLifetime _applicationLifetime;
private readonly IServerApplicationHost _applicationHost;
private readonly IServerApplicationPaths _applicationPaths;
private readonly IServerConfigurationManager _configurationManager;
private readonly IStartupOptions _startupOptions;
private readonly IInstallationManager _installationManager;
/// <summary>
/// Initializes a new instance of the <see cref="SystemManager"/> class.
/// </summary>
/// <param name="applicationLifetime">Instance of <see cref="IHostApplicationLifetime"/>.</param>
/// <param name="applicationHost">Instance of <see cref="IServerApplicationHost"/>.</param>
/// <param name="applicationPaths">Instance of <see cref="IServerApplicationPaths"/>.</param>
/// <param name="configurationManager">Instance of <see cref="IServerConfigurationManager"/>.</param>
/// <param name="startupOptions">Instance of <see cref="IStartupOptions"/>.</param>
/// <param name="installationManager">Instance of <see cref="IInstallationManager"/>.</param>
public SystemManager(
IHostApplicationLifetime applicationLifetime,
IServerApplicationHost applicationHost,
IServerApplicationPaths applicationPaths,
IServerConfigurationManager configurationManager,
IStartupOptions startupOptions,
IInstallationManager installationManager)
{
_applicationLifetime = applicationLifetime;
_applicationHost = applicationHost;
_applicationPaths = applicationPaths;
_configurationManager = configurationManager;
_startupOptions = startupOptions;
_installationManager = installationManager;
}
/// <inheritdoc />
public SystemInfo GetSystemInfo(HttpRequest request)
{
return new SystemInfo
{
HasPendingRestart = _applicationHost.HasPendingRestart,
IsShuttingDown = _applicationLifetime.ApplicationStopping.IsCancellationRequested,
Version = _applicationHost.ApplicationVersionString,
WebSocketPortNumber = _applicationHost.HttpPort,
CompletedInstallations = _installationManager.CompletedInstallations.ToArray(),
Id = _applicationHost.SystemId,
ProgramDataPath = _applicationPaths.ProgramDataPath,
WebPath = _applicationPaths.WebPath,
LogPath = _applicationPaths.LogDirectoryPath,
ItemsByNamePath = _applicationPaths.InternalMetadataPath,
InternalMetadataPath = _applicationPaths.InternalMetadataPath,
CachePath = _applicationPaths.CachePath,
TranscodingTempPath = _configurationManager.GetTranscodePath(),
ServerName = _applicationHost.FriendlyName,
LocalAddress = _applicationHost.GetSmartApiUrl(request),
SupportsLibraryMonitor = true,
PackageName = _startupOptions.PackageName,
CastReceiverApplications = _configurationManager.Configuration.CastReceiverApplications
};
}
/// <inheritdoc />
public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
{
return new PublicSystemInfo
{
Version = _applicationHost.ApplicationVersionString,
ProductName = _applicationHost.Name,
Id = _applicationHost.SystemId,
ServerName = _applicationHost.FriendlyName,
LocalAddress = _applicationHost.GetSmartApiUrl(request),
StartupWizardCompleted = _configurationManager.CommonConfiguration.IsStartupWizardCompleted
};
}
/// <inheritdoc />
public void Restart() => ShutdownInternal(true);
/// <inheritdoc />
public void Shutdown() => ShutdownInternal(false);
private void ShutdownInternal(bool restart)
{
Task.Run(async () =>
{
await Task.Delay(100).ConfigureAwait(false);
_applicationHost.ShouldRestart = restart;
_applicationLifetime.StopApplication();
});
}
}

View File

@ -27,9 +27,9 @@ namespace Emby.Server.Implementations.Udp
private readonly byte[] _receiveBuffer = new byte[8192]; private readonly byte[] _receiveBuffer = new byte[8192];
private Socket _udpSocket; private readonly Socket _udpSocket;
private IPEndPoint _endpoint; private readonly IPEndPoint _endpoint;
private bool _disposed = false; private bool _disposed;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="UdpServer" /> class. /// Initializes a new instance of the <see cref="UdpServer" /> class.
@ -52,7 +52,10 @@ namespace Emby.Server.Implementations.Udp
_endpoint = new IPEndPoint(bindAddress, port); _endpoint = new IPEndPoint(bindAddress, port);
_udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp)
{
MulticastLoopback = false,
};
_udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
} }
@ -74,6 +77,7 @@ namespace Emby.Server.Implementations.Udp
try try
{ {
_logger.LogDebug("Sending AutoDiscovery response");
await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false); await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false);
} }
catch (SocketException ex) catch (SocketException ex)
@ -99,7 +103,8 @@ namespace Emby.Server.Implementations.Udp
{ {
try try
{ {
var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint, cancellationToken).ConfigureAwait(false); var endpoint = (EndPoint)new IPEndPoint(IPAddress.Any, 0);
var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, endpoint, cancellationToken).ConfigureAwait(false);
var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes); var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes);
if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase)) if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
{ {
@ -112,7 +117,7 @@ namespace Emby.Server.Implementations.Udp
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// Don't throw _logger.LogDebug("Broadcast socket operation cancelled");
} }
} }
} }
@ -125,9 +130,8 @@ namespace Emby.Server.Implementations.Udp
return; return;
} }
_udpSocket?.Dispose(); _udpSocket.Dispose();
_disposed = true;
GC.SuppressFinalize(this);
} }
} }
} }

View File

@ -504,8 +504,7 @@ namespace Emby.Server.Implementations.Updates
private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken) private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken)
{ {
var extension = Path.GetExtension(package.SourceUrl); if (!Path.GetExtension(package.SourceUrl.AsSpan()).Equals(".zip", StringComparison.OrdinalIgnoreCase))
if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
{ {
_logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl); _logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl);
return; return;
@ -521,10 +520,9 @@ namespace Emby.Server.Implementations.Updates
// CA5351: Do Not Use Broken Cryptographic Algorithms // CA5351: Do Not Use Broken Cryptographic Algorithms
#pragma warning disable CA5351 #pragma warning disable CA5351
using var md5 = MD5.Create();
cancellationToken.ThrowIfCancellationRequested(); cancellationToken.ThrowIfCancellationRequested();
var hash = Convert.ToHexString(md5.ComputeHash(stream)); var hash = Convert.ToHexString(await MD5.HashDataAsync(stream, cancellationToken).ConfigureAwait(false));
if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase))
{ {
_logger.LogError( _logger.LogError(
@ -557,7 +555,7 @@ namespace Emby.Server.Implementations.Updates
reader.ExtractToDirectory(targetDir, true); reader.ExtractToDirectory(targetDir, true);
// Ensure we create one or populate existing ones with missing data. // Ensure we create one or populate existing ones with missing data.
await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status); await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false);
_pluginManager.ImportPluginFrom(targetDir); _pluginManager.ImportPluginFrom(targetDir);
} }

View File

@ -5,7 +5,6 @@ using System.IO;
using System.Net.Mime; using System.Net.Mime;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna; using Emby.Dlna;
using Emby.Dlna.Main;
using Jellyfin.Api.Attributes; using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
@ -33,12 +32,19 @@ public class DlnaServerController : BaseJellyfinApiController
/// Initializes a new instance of the <see cref="DlnaServerController"/> class. /// Initializes a new instance of the <see cref="DlnaServerController"/> class.
/// </summary> /// </summary>
/// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param> /// <param name="dlnaManager">Instance of the <see cref="IDlnaManager"/> interface.</param>
public DlnaServerController(IDlnaManager dlnaManager) /// <param name="contentDirectory">Instance of the <see cref="IContentDirectory"/> interface.</param>
/// <param name="connectionManager">Instance of the <see cref="IConnectionManager"/> interface.</param>
/// <param name="mediaReceiverRegistrar">Instance of the <see cref="IMediaReceiverRegistrar"/> interface.</param>
public DlnaServerController(
IDlnaManager dlnaManager,
IContentDirectory contentDirectory,
IConnectionManager connectionManager,
IMediaReceiverRegistrar mediaReceiverRegistrar)
{ {
_dlnaManager = dlnaManager; _dlnaManager = dlnaManager;
_contentDirectory = DlnaEntryPoint.Current.ContentDirectory; _contentDirectory = contentDirectory;
_connectionManager = DlnaEntryPoint.Current.ConnectionManager; _connectionManager = connectionManager;
_mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar; _mediaReceiverRegistrar = mediaReceiverRegistrar;
} }
/// <summary> /// <summary>

View File

@ -45,6 +45,8 @@ public class DynamicHlsController : BaseJellyfinApiController
private const string DefaultEventEncoderPreset = "superfast"; private const string DefaultEventEncoderPreset = "superfast";
private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls; private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0);
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IDlnaManager _dlnaManager; private readonly IDlnaManager _dlnaManager;
@ -408,6 +410,7 @@ public class DynamicHlsController : BaseJellyfinApiController
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param> /// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
/// <param name="streamOptions">Optional. The streaming options.</param> /// <param name="streamOptions">Optional. The streaming options.</param>
/// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param> /// <param name="enableAdaptiveBitrateStreaming">Enable adaptive bitrate streaming.</param>
/// <param name="enableTrickplay">Enable trickplay image playlists being added to master playlist.</param>
/// <response code="200">Video stream returned.</response> /// <response code="200">Video stream returned.</response>
/// <returns>A <see cref="FileResult"/> containing the playlist file.</returns> /// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
[HttpGet("Videos/{itemId}/master.m3u8")] [HttpGet("Videos/{itemId}/master.m3u8")]
@ -465,7 +468,8 @@ public class DynamicHlsController : BaseJellyfinApiController
[FromQuery] int? videoStreamIndex, [FromQuery] int? videoStreamIndex,
[FromQuery] EncodingContext? context, [FromQuery] EncodingContext? context,
[FromQuery] Dictionary<string, string> streamOptions, [FromQuery] Dictionary<string, string> streamOptions,
[FromQuery] bool enableAdaptiveBitrateStreaming = true) [FromQuery] bool enableAdaptiveBitrateStreaming = true,
[FromQuery] bool enableTrickplay = true)
{ {
var streamingRequest = new HlsVideoRequestDto var streamingRequest = new HlsVideoRequestDto
{ {
@ -519,7 +523,8 @@ public class DynamicHlsController : BaseJellyfinApiController
VideoStreamIndex = videoStreamIndex, VideoStreamIndex = videoStreamIndex,
Context = context ?? EncodingContext.Streaming, Context = context ?? EncodingContext.Streaming,
StreamOptions = streamOptions, StreamOptions = streamOptions,
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming,
EnableTrickplay = enableTrickplay
}; };
return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false); return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
@ -1705,16 +1710,31 @@ 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); var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
if (!state.IsOutputVideo) // opus, dts, truehd and flac (in FFmpeg 5 and older) are experimental in mp4 muxer
var strictArgs = string.Empty;
var actualOutputAudioCodec = state.ActualOutputAudioCodec;
if (string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
|| string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
|| string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)
|| (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
&& _mediaEncoder.EncoderVersion < _minFFmpegFlacInMp4))
{ {
if (EncodingHelper.IsCopyCodec(audioCodec)) strictArgs = " -strict -2";
{
return "-acodec copy -strict -2" + bitStreamArgs;
} }
if (!state.IsOutputVideo)
{
var audioTranscodeParams = string.Empty; var audioTranscodeParams = string.Empty;
audioTranscodeParams += "-acodec " + audioCodec + bitStreamArgs; // -vn to drop any video streams
audioTranscodeParams += "-vn";
if (EncodingHelper.IsCopyCodec(audioCodec))
{
return audioTranscodeParams + " -acodec copy" + bitStreamArgs + strictArgs;
}
audioTranscodeParams += " -acodec " + audioCodec + bitStreamArgs + strictArgs;
var audioBitrate = state.OutputAudioBitrate; var audioBitrate = state.OutputAudioBitrate;
var audioChannels = state.OutputAudioChannels; var audioChannels = state.OutputAudioChannels;
@ -1742,21 +1762,9 @@ public class DynamicHlsController : BaseJellyfinApiController
audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
} }
audioTranscodeParams += " -vn";
return audioTranscodeParams; return audioTranscodeParams;
} }
// dts, flac, opus and truehd are experimental in mp4 muxer
var strictArgs = string.Empty;
var actualOutputAudioCodec = state.ActualOutputAudioCodec;
if (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
|| string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
|| string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
{
strictArgs = " -strict -2";
}
if (EncodingHelper.IsCopyCodec(audioCodec)) if (EncodingHelper.IsCopyCodec(audioCodec))
{ {
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
@ -2041,9 +2049,9 @@ public class DynamicHlsController : BaseJellyfinApiController
return null; return null;
} }
var playlistFilename = Path.GetFileNameWithoutExtension(playlist); var playlistFilename = Path.GetFileNameWithoutExtension(playlist.AsSpan());
var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length); var indexString = Path.GetFileNameWithoutExtension(file.Name.AsSpan()).Slice(playlistFilename.Length);
return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture); return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
} }

View File

@ -59,7 +59,7 @@ public class HlsSegmentController : BaseJellyfinApiController
public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId) public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
{ {
// TODO: Deprecate with new iOS app // TODO: Deprecate with new iOS app
var file = segmentId + Path.GetExtension(Request.Path); var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
var transcodePath = _serverConfigurationManager.GetTranscodePath(); var transcodePath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodePath, file)); file = Path.GetFullPath(Path.Combine(transcodePath, file));
var fileDir = Path.GetDirectoryName(file); var fileDir = Path.GetDirectoryName(file);
@ -85,11 +85,12 @@ public class HlsSegmentController : BaseJellyfinApiController
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")] [SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId) public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
{ {
var file = playlistId + Path.GetExtension(Request.Path); var file = string.Concat(playlistId, Path.GetExtension(Request.Path.Value.AsSpan()));
var transcodePath = _serverConfigurationManager.GetTranscodePath(); var transcodePath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodePath, file)); file = Path.GetFullPath(Path.Combine(transcodePath, file));
var fileDir = Path.GetDirectoryName(file); var fileDir = Path.GetDirectoryName(file);
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8") if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture)
|| Path.GetExtension(file.AsSpan()).Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
{ {
return BadRequest("Invalid segment."); return BadRequest("Invalid segment.");
} }
@ -138,7 +139,7 @@ public class HlsSegmentController : BaseJellyfinApiController
[FromRoute, Required] string segmentId, [FromRoute, Required] string segmentId,
[FromRoute, Required] string segmentContainer) [FromRoute, Required] string segmentContainer)
{ {
var file = segmentId + Path.GetExtension(Request.Path); var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath(); var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file)); file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));

View File

@ -7,6 +7,7 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Mime; using System.Net.Mime;
using System.Security.Cryptography;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Attributes; using Jellyfin.Api.Attributes;
@ -78,6 +79,9 @@ public class ImageController : BaseJellyfinApiController
_appPaths = appPaths; _appPaths = appPaths;
} }
private static Stream GetFromBase64Stream(Stream inputStream)
=> new CryptoStream(inputStream, new FromBase64Transform(), CryptoStreamMode.Read);
/// <summary> /// <summary>
/// Sets the user image. /// Sets the user image.
/// </summary> /// </summary>
@ -116,8 +120,8 @@ public class ImageController : BaseJellyfinApiController
return BadRequest("Incorrect ContentType."); return BadRequest("Incorrect ContentType.");
} }
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); var stream = GetFromBase64Stream(Request.Body);
await using (memoryStream.ConfigureAwait(false)) await using (stream.ConfigureAwait(false))
{ {
// Handle image/png; charset=utf-8 // Handle image/png; charset=utf-8
var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
@ -130,7 +134,7 @@ public class ImageController : BaseJellyfinApiController
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension)); user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
await _providerManager await _providerManager
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path) .SaveImage(stream, mimeType, user.ProfileImage.Path)
.ConfigureAwait(false); .ConfigureAwait(false);
await _userManager.UpdateUserAsync(user).ConfigureAwait(false); await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
@ -176,8 +180,8 @@ public class ImageController : BaseJellyfinApiController
return BadRequest("Incorrect ContentType."); return BadRequest("Incorrect ContentType.");
} }
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); var stream = GetFromBase64Stream(Request.Body);
await using (memoryStream.ConfigureAwait(false)) await using (stream.ConfigureAwait(false))
{ {
// Handle image/png; charset=utf-8 // Handle image/png; charset=utf-8
var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
@ -190,7 +194,7 @@ public class ImageController : BaseJellyfinApiController
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension)); user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
await _providerManager await _providerManager
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path) .SaveImage(stream, mimeType, user.ProfileImage.Path)
.ConfigureAwait(false); .ConfigureAwait(false);
await _userManager.UpdateUserAsync(user).ConfigureAwait(false); await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
@ -372,12 +376,12 @@ public class ImageController : BaseJellyfinApiController
return BadRequest("Incorrect ContentType."); return BadRequest("Incorrect ContentType.");
} }
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); var stream = GetFromBase64Stream(Request.Body);
await using (memoryStream.ConfigureAwait(false)) await using (stream.ConfigureAwait(false))
{ {
// Handle image/png; charset=utf-8 // Handle image/png; charset=utf-8
var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return NoContent(); return NoContent();
@ -416,12 +420,12 @@ public class ImageController : BaseJellyfinApiController
return BadRequest("Incorrect ContentType."); return BadRequest("Incorrect ContentType.");
} }
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); var stream = GetFromBase64Stream(Request.Body);
await using (memoryStream.ConfigureAwait(false)) await using (stream.ConfigureAwait(false))
{ {
// Handle image/png; charset=utf-8 // Handle image/png; charset=utf-8
var mimeType = Request.ContentType?.Split(';').FirstOrDefault(); var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false); await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false); await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
return NoContent(); return NoContent();
@ -1792,8 +1796,8 @@ public class ImageController : BaseJellyfinApiController
return BadRequest("Incorrect ContentType."); return BadRequest("Incorrect ContentType.");
} }
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false); var stream = GetFromBase64Stream(Request.Body);
await using (memoryStream.ConfigureAwait(false)) await using (stream.ConfigureAwait(false))
{ {
var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension); var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding"); var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
@ -1803,7 +1807,7 @@ public class ImageController : BaseJellyfinApiController
var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous); var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
await using (fs.ConfigureAwait(false)) await using (fs.ConfigureAwait(false))
{ {
await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false); await stream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
} }
return NoContent(); return NoContent();
@ -1833,15 +1837,6 @@ public class ImageController : BaseJellyfinApiController
return NoContent(); return NoContent();
} }
private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
{
using var reader = new StreamReader(inputStream);
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
var bytes = Convert.FromBase64String(text);
return new MemoryStream(bytes, 0, bytes.Length, false, true);
}
private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex) private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
{ {
int? width = null; int? width = null;

View File

@ -294,8 +294,8 @@ public class LibraryController : BaseJellyfinApiController
return new AllThemeMediaResult return new AllThemeMediaResult
{ {
ThemeSongsResult = themeSongs?.Value, ThemeSongsResult = themeSongs.Value,
ThemeVideosResult = themeVideos?.Value, ThemeVideosResult = themeVideos.Value,
SoundtrackSongsResult = new ThemeMediaResult() SoundtrackSongsResult = new ThemeMediaResult()
}; };
} }
@ -490,7 +490,7 @@ public class LibraryController : BaseJellyfinApiController
baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user)); baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
parent = parent?.GetParent(); parent = parent.GetParent();
} }
return baseItemDtos; return baseItemDtos;

View File

@ -6,6 +6,7 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Mime; using System.Net.Mime;
using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -405,9 +406,8 @@ public class SubtitleController : BaseJellyfinApiController
[FromBody, Required] UploadSubtitleDto body) [FromBody, Required] UploadSubtitleDto body)
{ {
var video = (Video)_libraryManager.GetItemById(itemId); var video = (Video)_libraryManager.GetItemById(itemId);
var data = Convert.FromBase64String(body.Data); var stream = new CryptoStream(Request.Body, new FromBase64Transform(), CryptoStreamMode.Read);
var memoryStream = new MemoryStream(data, 0, data.Length, false, true); await using (stream.ConfigureAwait(false))
await using (memoryStream.ConfigureAwait(false))
{ {
await _subtitleManager.UploadSubtitle( await _subtitleManager.UploadSubtitle(
video, video,
@ -417,7 +417,7 @@ public class SubtitleController : BaseJellyfinApiController
Language = body.Language, Language = body.Language,
IsForced = body.IsForced, IsForced = body.IsForced,
IsHearingImpaired = body.IsHearingImpaired, IsHearingImpaired = body.IsHearingImpaired,
Stream = memoryStream Stream = stream
}).ConfigureAwait(false); }).ConfigureAwait(false);
_providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High); _providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);

View File

@ -10,7 +10,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using MediaBrowser.Model.System; using MediaBrowser.Model.System;
@ -26,32 +25,36 @@ namespace Jellyfin.Api.Controllers;
/// </summary> /// </summary>
public class SystemController : BaseJellyfinApiController public class SystemController : BaseJellyfinApiController
{ {
private readonly ILogger<SystemController> _logger;
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly INetworkManager _network; private readonly INetworkManager _networkManager;
private readonly ILogger<SystemController> _logger; private readonly ISystemManager _systemManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SystemController"/> class. /// Initializes a new instance of the <see cref="SystemController"/> class.
/// </summary> /// </summary>
/// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
/// <param name="appPaths">Instance of <see cref="IServerApplicationPaths"/> interface.</param>
/// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param> /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param> /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
/// <param name="network">Instance of <see cref="INetworkManager"/> interface.</param> /// <param name="networkManager">Instance of <see cref="INetworkManager"/> interface.</param>
/// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param> /// <param name="systemManager">Instance of <see cref="ISystemManager"/> interface.</param>
public SystemController( public SystemController(
IServerConfigurationManager serverConfigurationManager, ILogger<SystemController> logger,
IServerApplicationHost appHost, IServerApplicationHost appHost,
IServerApplicationPaths appPaths,
IFileSystem fileSystem, IFileSystem fileSystem,
INetworkManager network, INetworkManager networkManager,
ILogger<SystemController> logger) ISystemManager systemManager)
{ {
_appPaths = serverConfigurationManager.ApplicationPaths;
_appHost = appHost;
_fileSystem = fileSystem;
_network = network;
_logger = logger; _logger = logger;
_appHost = appHost;
_appPaths = appPaths;
_fileSystem = fileSystem;
_networkManager = networkManager;
_systemManager = systemManager;
} }
/// <summary> /// <summary>
@ -65,9 +68,7 @@ public class SystemController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult<SystemInfo> GetSystemInfo() public ActionResult<SystemInfo> GetSystemInfo()
{ => _systemManager.GetSystemInfo(Request);
return _appHost.GetSystemInfo(Request);
}
/// <summary> /// <summary>
/// Gets public information about the server. /// Gets public information about the server.
@ -77,9 +78,7 @@ public class SystemController : BaseJellyfinApiController
[HttpGet("Info/Public")] [HttpGet("Info/Public")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<PublicSystemInfo> GetPublicSystemInfo() public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
{ => _systemManager.GetPublicSystemInfo(Request);
return _appHost.GetPublicSystemInfo(Request);
}
/// <summary> /// <summary>
/// Pings the system. /// Pings the system.
@ -90,9 +89,7 @@ public class SystemController : BaseJellyfinApiController
[HttpPost("Ping", Name = "PostPingSystem")] [HttpPost("Ping", Name = "PostPingSystem")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<string> PingSystem() public ActionResult<string> PingSystem()
{ => _appHost.Name;
return _appHost.Name;
}
/// <summary> /// <summary>
/// Restarts the application. /// Restarts the application.
@ -106,7 +103,7 @@ public class SystemController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult RestartApplication() public ActionResult RestartApplication()
{ {
_appHost.Restart(); _systemManager.Restart();
return NoContent(); return NoContent();
} }
@ -122,7 +119,7 @@ public class SystemController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult ShutdownApplication() public ActionResult ShutdownApplication()
{ {
_appHost.Shutdown(); _systemManager.Shutdown();
return NoContent(); return NoContent();
} }
@ -180,7 +177,7 @@ public class SystemController : BaseJellyfinApiController
return new EndPointInfo return new EndPointInfo
{ {
IsLocal = HttpContext.IsLocal(), IsLocal = HttpContext.IsLocal(),
IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP()) IsInNetwork = _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP())
}; };
} }
@ -218,7 +215,7 @@ public class SystemController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo() public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
{ {
var result = _network.GetMacAddresses() var result = _networkManager.GetMacAddresses()
.Select(i => new WakeOnLanInfo(i)); .Select(i => new WakeOnLanInfo(i));
return Ok(result); return Ok(result);
} }

View File

@ -0,0 +1,101 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Net.Mime;
using System.Text;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers;
/// <summary>
/// Trickplay controller.
/// </summary>
[Route("")]
[Authorize]
public class TrickplayController : BaseJellyfinApiController
{
private readonly ILibraryManager _libraryManager;
private readonly ITrickplayManager _trickplayManager;
/// <summary>
/// Initializes a new instance of the <see cref="TrickplayController"/> class.
/// </summary>
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/>.</param>
/// <param name="trickplayManager">Instance of <see cref="ITrickplayManager"/>.</param>
public TrickplayController(
ILibraryManager libraryManager,
ITrickplayManager trickplayManager)
{
_libraryManager = libraryManager;
_trickplayManager = trickplayManager;
}
/// <summary>
/// Gets an image tiles playlist for trickplay.
/// </summary>
/// <param name="itemId">The item id.</param>
/// <param name="width">The width of a single tile.</param>
/// <param name="mediaSourceId">The media version id, if using an alternate version.</param>
/// <response code="200">Tiles playlist returned.</response>
/// <returns>A <see cref="FileResult"/> containing the trickplay playlist file.</returns>
[HttpGet("Videos/{itemId}/Trickplay/{width}/tiles.m3u8")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesPlaylistFile]
public async Task<ActionResult> GetTrickplayHlsPlaylist(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] int width,
[FromQuery] Guid? mediaSourceId)
{
string? playlist = await _trickplayManager.GetHlsPlaylist(mediaSourceId ?? itemId, width, User.GetToken()).ConfigureAwait(false);
if (string.IsNullOrEmpty(playlist))
{
return NotFound();
}
return Content(playlist, MimeTypes.GetMimeType("playlist.m3u8"), Encoding.UTF8);
}
/// <summary>
/// Gets a trickplay tile image.
/// </summary>
/// <param name="itemId">The item id.</param>
/// <param name="width">The width of a single tile.</param>
/// <param name="index">The index of the desired tile.</param>
/// <param name="mediaSourceId">The media version id, if using an alternate version.</param>
/// <response code="200">Tile image returned.</response>
/// <response code="200">Tile image not found at specified index.</response>
/// <returns>A <see cref="FileResult"/> containing the trickplay tiles image.</returns>
[HttpGet("Videos/{itemId}/Trickplay/{width}/{index}.jpg")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesImageFile]
public ActionResult GetTrickplayTileImage(
[FromRoute, Required] Guid itemId,
[FromRoute, Required] int width,
[FromRoute, Required] int index,
[FromQuery] Guid? mediaSourceId)
{
var item = _libraryManager.GetItemById(mediaSourceId ?? itemId);
if (item is null)
{
return NotFound();
}
var path = _trickplayManager.GetTrickplayTilePath(item, width, index);
if (System.IO.File.Exists(path))
{
return PhysicalFile(path, MediaTypeNames.Image.Jpeg);
}
return NotFound();
}
}

View File

@ -9,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Api.Extensions; using Jellyfin.Api.Extensions;
using Jellyfin.Api.Models.StreamingDtos; using Jellyfin.Api.Models.StreamingDtos;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
@ -19,6 +20,7 @@ using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
@ -46,6 +48,7 @@ public class DynamicHlsHelper
private readonly ILogger<DynamicHlsHelper> _logger; private readonly ILogger<DynamicHlsHelper> _logger;
private readonly IHttpContextAccessor _httpContextAccessor; private readonly IHttpContextAccessor _httpContextAccessor;
private readonly EncodingHelper _encodingHelper; private readonly EncodingHelper _encodingHelper;
private readonly ITrickplayManager _trickplayManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="DynamicHlsHelper"/> class. /// Initializes a new instance of the <see cref="DynamicHlsHelper"/> class.
@ -62,6 +65,7 @@ public class DynamicHlsHelper
/// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> interface.</param> /// <param name="logger">Instance of the <see cref="ILogger{DynamicHlsHelper}"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param> /// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
/// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param> /// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
/// <param name="trickplayManager">Instance of <see cref="ITrickplayManager"/>.</param>
public DynamicHlsHelper( public DynamicHlsHelper(
ILibraryManager libraryManager, ILibraryManager libraryManager,
IUserManager userManager, IUserManager userManager,
@ -74,7 +78,8 @@ public class DynamicHlsHelper
INetworkManager networkManager, INetworkManager networkManager,
ILogger<DynamicHlsHelper> logger, ILogger<DynamicHlsHelper> logger,
IHttpContextAccessor httpContextAccessor, IHttpContextAccessor httpContextAccessor,
EncodingHelper encodingHelper) EncodingHelper encodingHelper,
ITrickplayManager trickplayManager)
{ {
_libraryManager = libraryManager; _libraryManager = libraryManager;
_userManager = userManager; _userManager = userManager;
@ -88,6 +93,7 @@ public class DynamicHlsHelper
_logger = logger; _logger = logger;
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
_encodingHelper = encodingHelper; _encodingHelper = encodingHelper;
_trickplayManager = trickplayManager;
} }
/// <summary> /// <summary>
@ -200,13 +206,6 @@ public class DynamicHlsHelper
if (state.VideoStream is not null && state.VideoRequest is not null) if (state.VideoStream is not null && state.VideoRequest is not null)
{ {
// Provide a workaround for the case issue between flac and fLaC.
var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString());
if (!string.IsNullOrEmpty(flacWaPlaylist))
{
builder.Append(flacWaPlaylist);
}
var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
// Provide SDR HEVC entrance for backward compatibility. // Provide SDR HEVC entrance for backward compatibility.
@ -236,14 +235,7 @@ public class DynamicHlsHelper
} }
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate; var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup); AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
// Provide a workaround for the case issue between flac and fLaC.
flacWaPlaylist = ApplyFlacCaseWorkaround(state, sdrPlaylist.ToString());
if (!string.IsNullOrEmpty(flacWaPlaylist))
{
builder.Append(flacWaPlaylist);
}
// Restore the video codec // Restore the video codec
state.OutputVideoCodec = "copy"; state.OutputVideoCodec = "copy";
@ -274,13 +266,6 @@ public class DynamicHlsHelper
state.VideoStream.Level = originalLevel; state.VideoStream.Level = originalLevel;
var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField); var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
builder.Append(newPlaylist); builder.Append(newPlaylist);
// Provide a workaround for the case issue between flac and fLaC.
flacWaPlaylist = ApplyFlacCaseWorkaround(state, newPlaylist);
if (!string.IsNullOrEmpty(flacWaPlaylist))
{
builder.Append(flacWaPlaylist);
}
} }
} }
@ -301,6 +286,13 @@ public class DynamicHlsHelper
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup); AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
} }
if (!isLiveStream && (state.VideoRequest?.EnableTrickplay ?? false))
{
var sourceId = Guid.Parse(state.Request.MediaSourceId);
var trickplayResolutions = await _trickplayManager.GetTrickplayResolutions(sourceId).ConfigureAwait(false);
AddTrickplay(state, trickplayResolutions, builder, _httpContextAccessor.HttpContext.User);
}
return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8")); return new FileContentResult(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
} }
@ -529,6 +521,41 @@ public class DynamicHlsHelper
} }
} }
/// <summary>
/// Appends EXT-X-IMAGE-STREAM-INF playlists for each available trickplay resolution.
/// </summary>
/// <param name="state">StreamState of the current stream.</param>
/// <param name="trickplayResolutions">Dictionary of widths to corresponding tiles info.</param>
/// <param name="builder">StringBuilder to append the field to.</param>
/// <param name="user">Http user context.</param>
private void AddTrickplay(StreamState state, Dictionary<int, TrickplayInfo> trickplayResolutions, StringBuilder builder, ClaimsPrincipal user)
{
const string playlistFormat = "#EXT-X-IMAGE-STREAM-INF:BANDWIDTH={0},RESOLUTION={1}x{2},CODECS=\"jpeg\",URI=\"{3}\"";
foreach (var resolution in trickplayResolutions)
{
var width = resolution.Key;
var trickplayInfo = resolution.Value;
var url = string.Format(
CultureInfo.InvariantCulture,
"Trickplay/{0}/tiles.m3u8?MediaSourceId={1}&api_key={2}",
width.ToString(CultureInfo.InvariantCulture),
state.Request.MediaSourceId,
user.GetToken());
builder.AppendFormat(
CultureInfo.InvariantCulture,
playlistFormat,
trickplayInfo.Bandwidth.ToString(CultureInfo.InvariantCulture),
trickplayInfo.Width.ToString(CultureInfo.InvariantCulture),
trickplayInfo.Height.ToString(CultureInfo.InvariantCulture),
url);
builder.AppendLine();
}
}
/// <summary> /// <summary>
/// Get the H.26X level of the output video stream. /// Get the H.26X level of the output video stream.
/// </summary> /// </summary>
@ -767,16 +794,4 @@ public class DynamicHlsHelper
newValue.ToString(), newValue.ToString(),
StringComparison.Ordinal); StringComparison.Ordinal);
} }
private string ApplyFlacCaseWorkaround(StreamState state, string srcPlaylist)
{
if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
{
return string.Empty;
}
var newPlaylist = srcPlaylist.Replace(",flac\"", ",fLaC\"", StringComparison.Ordinal);
return newPlaylist.Contains(",fLaC\"", StringComparison.Ordinal) ? newPlaylist : string.Empty;
}
} }

View File

@ -5,7 +5,9 @@ using System.Text;
namespace Jellyfin.Api.Helpers; namespace Jellyfin.Api.Helpers;
/// <summary> /// <summary>
/// Hls Codec string helpers. /// Helpers to generate HLS codec strings according to
/// <a href="https://datatracker.ietf.org/doc/html/rfc6381#section-3.3">RFC 6381 section 3.3</a>
/// and the <a href="https://mp4ra.org">MP4 Registration Authority</a>.
/// </summary> /// </summary>
public static class HlsCodecStringHelpers public static class HlsCodecStringHelpers
{ {
@ -27,7 +29,7 @@ public static class HlsCodecStringHelpers
/// <summary> /// <summary>
/// Codec name for FLAC. /// Codec name for FLAC.
/// </summary> /// </summary>
public const string FLAC = "flac"; public const string FLAC = "fLaC";
/// <summary> /// <summary>
/// Codec name for ALAC. /// Codec name for ALAC.
@ -37,7 +39,7 @@ public static class HlsCodecStringHelpers
/// <summary> /// <summary>
/// Codec name for OPUS. /// Codec name for OPUS.
/// </summary> /// </summary>
public const string OPUS = "opus"; public const string OPUS = "Opus";
/// <summary> /// <summary>
/// Gets a MP3 codec string. /// Gets a MP3 codec string.

View File

@ -248,7 +248,7 @@ public static class StreamingHelpers
? GetOutputFileExtension(state, mediaSource) ? GetOutputFileExtension(state, mediaSource)
: ("." + state.OutputContainer); : ("." + state.OutputContainer);
state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId); state.OutputFilePath = GetOutputFilePath(state, ext, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
return state; return state;
} }
@ -421,10 +421,9 @@ public static class StreamingHelpers
/// <param name="state">The state.</param> /// <param name="state">The state.</param>
/// <param name="mediaSource">The mediaSource.</param> /// <param name="mediaSource">The mediaSource.</param>
/// <returns>System.String.</returns> /// <returns>System.String.</returns>
private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource) private static string GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
{ {
var ext = Path.GetExtension(state.RequestedUrl); var ext = Path.GetExtension(state.RequestedUrl);
if (!string.IsNullOrEmpty(ext)) if (!string.IsNullOrEmpty(ext))
{ {
return ext; return ext;
@ -463,10 +462,9 @@ public static class StreamingHelpers
return ".asf"; return ".asf";
} }
} }
else
// Try to infer based on the desired audio codec
if (!state.IsVideoRequest)
{ {
// Try to infer based on the desired audio codec
var audioCodec = state.Request.AudioCodec; var audioCodec = state.Request.AudioCodec;
if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase)) if (string.Equals("aac", audioCodec, StringComparison.OrdinalIgnoreCase))
@ -497,7 +495,7 @@ public static class StreamingHelpers
return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim(); return '.' + (idx == -1 ? mediaSource.Container : mediaSource.Container[..idx]).Trim();
} }
return null; throw new InvalidOperationException("Failed to find an appropriate file extension");
} }
/// <summary> /// <summary>
@ -514,7 +512,7 @@ public static class StreamingHelpers
var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}"; var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture); var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
var ext = outputFileExtension?.ToLowerInvariant(); var ext = outputFileExtension.ToLowerInvariant();
var folder = serverConfigurationManager.GetTranscodePath(); var folder = serverConfigurationManager.GetTranscodePath();
return Path.Combine(folder, filename + ext); return Path.Combine(folder, filename + ext);

View File

@ -538,7 +538,7 @@ public class TranscodingJobHelper : IDisposable
await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false); await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
} }
if (state.SubtitleStream.IsExternal && string.Equals(Path.GetExtension(state.SubtitleStream.Path), ".mks", StringComparison.OrdinalIgnoreCase)) if (state.SubtitleStream.IsExternal && Path.GetExtension(state.SubtitleStream.Path.AsSpan()).Equals(".mks", StringComparison.OrdinalIgnoreCase))
{ {
string subtitlePath = state.SubtitleStream.Path; string subtitlePath = state.SubtitleStream.Path;
string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal)); string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal));

View File

@ -8,8 +8,6 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0</TargetFramework> <TargetFramework>net7.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
<NoWarn>AD0001</NoWarn>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@ -122,17 +122,17 @@ public class ExceptionMiddleware
private static int GetStatusCode(Exception ex) private static int GetStatusCode(Exception ex)
{ {
switch (ex) return ex switch
{ {
case ArgumentException _: return StatusCodes.Status400BadRequest; ArgumentException => StatusCodes.Status400BadRequest,
case AuthenticationException _: return StatusCodes.Status401Unauthorized; AuthenticationException => StatusCodes.Status401Unauthorized,
case SecurityException _: return StatusCodes.Status403Forbidden; SecurityException => StatusCodes.Status403Forbidden,
case DirectoryNotFoundException _: DirectoryNotFoundException => StatusCodes.Status404NotFound,
case FileNotFoundException _: FileNotFoundException => StatusCodes.Status404NotFound,
case ResourceNotFoundException _: return StatusCodes.Status404NotFound; ResourceNotFoundException => StatusCodes.Status404NotFound,
case MethodNotAllowedException _: return StatusCodes.Status405MethodNotAllowed; MethodNotAllowedException => StatusCodes.Status405MethodNotAllowed,
default: return StatusCodes.Status500InternalServerError; _ => StatusCodes.Status500InternalServerError
} };
} }
private string NormalizeExceptionMessage(string msg) private string NormalizeExceptionMessage(string msg)

View File

@ -1,4 +1,4 @@
namespace Jellyfin.Api.Models.StreamingDtos; namespace Jellyfin.Api.Models.StreamingDtos;
/// <summary> /// <summary>
/// The video request dto. /// The video request dto.
@ -15,4 +15,9 @@ public class VideoRequestDto : StreamingRequestDto
/// Gets or sets a value indicating whether to enable subtitles in the manifest. /// Gets or sets a value indicating whether to enable subtitles in the manifest.
/// </summary> /// </summary>
public bool EnableSubtitlesInManifest { get; set; } public bool EnableSubtitlesInManifest { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to enable trickplay images.
/// </summary>
public bool EnableTrickplay { get; set; }
} }

View File

@ -0,0 +1,75 @@
using System;
using System.Text.Json.Serialization;
namespace Jellyfin.Data.Entities;
/// <summary>
/// An entity representing the metadata for a group of trickplay tiles.
/// </summary>
public class TrickplayInfo
{
/// <summary>
/// Gets or sets the id of the associated item.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[JsonIgnore]
public Guid ItemId { get; set; }
/// <summary>
/// Gets or sets width of an individual thumbnail.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public int Width { get; set; }
/// <summary>
/// Gets or sets height of an individual thumbnail.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public int Height { get; set; }
/// <summary>
/// Gets or sets amount of thumbnails per row.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public int TileWidth { get; set; }
/// <summary>
/// Gets or sets amount of thumbnails per column.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public int TileHeight { get; set; }
/// <summary>
/// Gets or sets total amount of non-black thumbnails.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public int ThumbnailCount { get; set; }
/// <summary>
/// Gets or sets interval in milliseconds between each trickplay thumbnail.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public int Interval { get; set; }
/// <summary>
/// Gets or sets peak bandwith usage in bits per second.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public int Bandwidth { get; set; }
}

View File

@ -288,6 +288,12 @@ namespace Jellyfin.Data.Entities
/// </summary> /// </summary>
public SyncPlayUserAccessType SyncPlayAccess { get; set; } public SyncPlayUserAccessType SyncPlayAccess { get; set; }
/// <summary>
/// Gets or sets the cast receiver id.
/// </summary>
[StringLength(32)]
public string? CastReceiverId { get; set; }
/// <inheritdoc /> /// <inheritdoc />
[ConcurrencyCheck] [ConcurrencyCheck]
public uint RowVersion { get; private set; } public uint RowVersion { get; private set; }

View File

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net; using System.Net;
using System.Net.Sockets; using System.Net.Sockets;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -204,7 +203,7 @@ public static partial class NetworkExtensions
{ {
var ipBlock = splitString.Current; var ipBlock = splitString.Current;
var address = IPAddress.None; var address = IPAddress.None;
if (negated && ipBlock.StartsWith<char>("!") && IPAddress.TryParse(ipBlock[1..], out var tmpAddress)) if (negated && ipBlock.StartsWith("!") && IPAddress.TryParse(ipBlock[1..], out var tmpAddress))
{ {
address = tmpAddress; address = tmpAddress;
} }
@ -231,12 +230,12 @@ public static partial class NetworkExtensions
} }
else if (address.AddressFamily == AddressFamily.InterNetwork) else if (address.AddressFamily == AddressFamily.InterNetwork)
{ {
result = new IPNetwork(address, Network.MinimumIPv4PrefixSize); result = address.Equals(IPAddress.Any) ? Network.IPv4Any : new IPNetwork(address, Network.MinimumIPv4PrefixSize);
return true; return true;
} }
else if (address.AddressFamily == AddressFamily.InterNetworkV6) else if (address.AddressFamily == AddressFamily.InterNetworkV6)
{ {
result = new IPNetwork(address, Network.MinimumIPv6PrefixSize); result = address.Equals(IPAddress.IPv6Any) ? Network.IPv6Any : new IPNetwork(address, Network.MinimumIPv6PrefixSize);
return true; return true;
} }
} }
@ -284,12 +283,15 @@ public static partial class NetworkExtensions
if (hosts.Count <= 2) if (hosts.Count <= 2)
{ {
var firstPart = hosts[0];
// Is hostname or hostname:port // Is hostname or hostname:port
if (FqdnGeneratedRegex().IsMatch(hosts[0])) if (FqdnGeneratedRegex().IsMatch(firstPart))
{ {
try try
{ {
addresses = Dns.GetHostAddresses(hosts[0]); // .NET automatically filters only supported returned addresses based on OS support.
addresses = Dns.GetHostAddresses(firstPart);
return true; return true;
} }
catch (SocketException) catch (SocketException)
@ -299,7 +301,7 @@ public static partial class NetworkExtensions
} }
// Is an IPv4 or IPv4:port // Is an IPv4 or IPv4:port
if (IPAddress.TryParse(hosts[0].AsSpan().LeftPart('/'), out var address)) if (IPAddress.TryParse(firstPart.AsSpan().LeftPart('/'), out var address))
{ {
if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled)) if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled))
|| ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled))) || ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled)))

View File

@ -15,7 +15,9 @@ using MediaBrowser.Common.Net;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
namespace Jellyfin.Networking.Manager namespace Jellyfin.Networking.Manager
{ {
@ -33,12 +35,14 @@ namespace Jellyfin.Networking.Manager
private readonly IConfigurationManager _configurationManager; private readonly IConfigurationManager _configurationManager;
private readonly IConfiguration _startupConfig;
private readonly object _networkEventLock; private readonly object _networkEventLock;
/// <summary> /// <summary>
/// Holds the published server URLs and the IPs to use them on. /// Holds the published server URLs and the IPs to use them on.
/// </summary> /// </summary>
private IReadOnlyDictionary<IPData, string> _publishedServerUrls; private IReadOnlyList<PublishedServerUriOverride> _publishedServerUrls;
private IReadOnlyList<IPNetwork> _remoteAddressFilter; private IReadOnlyList<IPNetwork> _remoteAddressFilter;
@ -76,20 +80,22 @@ namespace Jellyfin.Networking.Manager
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="NetworkManager"/> class. /// Initializes a new instance of the <see cref="NetworkManager"/> class.
/// </summary> /// </summary>
/// <param name="configurationManager">IServerConfigurationManager instance.</param> /// <param name="configurationManager">The <see cref="IConfigurationManager"/> instance.</param>
/// <param name="startupConfig">The <see cref="IConfiguration"/> instance holding startup parameters.</param>
/// <param name="logger">Logger to use for messages.</param> /// <param name="logger">Logger to use for messages.</param>
#pragma warning disable CS8618 // Non-nullable field is uninitialized. : Values are set in UpdateSettings function. Compiler doesn't yet recognise this. #pragma warning disable CS8618 // Non-nullable field is uninitialized. : Values are set in UpdateSettings function. Compiler doesn't yet recognise this.
public NetworkManager(IConfigurationManager configurationManager, ILogger<NetworkManager> logger) public NetworkManager(IConfigurationManager configurationManager, IConfiguration startupConfig, ILogger<NetworkManager> logger)
{ {
ArgumentNullException.ThrowIfNull(logger); ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(configurationManager); ArgumentNullException.ThrowIfNull(configurationManager);
_logger = logger; _logger = logger;
_configurationManager = configurationManager; _configurationManager = configurationManager;
_startupConfig = startupConfig;
_initLock = new(); _initLock = new();
_interfaces = new List<IPData>(); _interfaces = new List<IPData>();
_macAddresses = new List<PhysicalAddress>(); _macAddresses = new List<PhysicalAddress>();
_publishedServerUrls = new Dictionary<IPData, string>(); _publishedServerUrls = new List<PublishedServerUriOverride>();
_networkEventLock = new object(); _networkEventLock = new object();
_remoteAddressFilter = new List<IPNetwork>(); _remoteAddressFilter = new List<IPNetwork>();
@ -130,7 +136,7 @@ namespace Jellyfin.Networking.Manager
/// <summary> /// <summary>
/// Gets the Published server override list. /// Gets the Published server override list.
/// </summary> /// </summary>
public IReadOnlyDictionary<IPData, string> PublishedServerUrls => _publishedServerUrls; public IReadOnlyList<PublishedServerUriOverride> PublishedServerUrls => _publishedServerUrls;
/// <inheritdoc/> /// <inheritdoc/>
public void Dispose() public void Dispose()
@ -170,7 +176,6 @@ namespace Jellyfin.Networking.Manager
{ {
if (!_eventfire) if (!_eventfire)
{ {
_logger.LogDebug("Network Address Change Event.");
// As network events tend to fire one after the other only fire once every second. // As network events tend to fire one after the other only fire once every second.
_eventfire = true; _eventfire = true;
OnNetworkChange(); OnNetworkChange();
@ -193,11 +198,12 @@ namespace Jellyfin.Networking.Manager
} }
else else
{ {
InitialiseInterfaces(); InitializeInterfaces();
InitialiseLan(networkConfig); InitializeLan(networkConfig);
EnforceBindSettings(networkConfig); EnforceBindSettings(networkConfig);
} }
PrintNetworkInformation(networkConfig);
NetworkChanged?.Invoke(this, EventArgs.Empty); NetworkChanged?.Invoke(this, EventArgs.Empty);
} }
finally finally
@ -210,7 +216,7 @@ namespace Jellyfin.Networking.Manager
/// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state. /// Generate a list of all the interface ip addresses and submasks where that are in the active/unknown state.
/// Generate a list of all active mac addresses that aren't loopback addresses. /// Generate a list of all active mac addresses that aren't loopback addresses.
/// </summary> /// </summary>
private void InitialiseInterfaces() private void InitializeInterfaces()
{ {
lock (_initLock) lock (_initLock)
{ {
@ -222,7 +228,7 @@ namespace Jellyfin.Networking.Manager
try try
{ {
var nics = NetworkInterface.GetAllNetworkInterfaces() var nics = NetworkInterface.GetAllNetworkInterfaces()
.Where(i => i.SupportsMulticast && i.OperationalStatus == OperationalStatus.Up); .Where(i => i.OperationalStatus == OperationalStatus.Up);
foreach (NetworkInterface adapter in nics) foreach (NetworkInterface adapter in nics)
{ {
@ -242,34 +248,36 @@ namespace Jellyfin.Networking.Manager
{ {
if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork) if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork)
{ {
var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name); var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
interfaceObject.Index = ipProperties.GetIPv4Properties().Index; {
interfaceObject.Name = adapter.Name; Index = ipProperties.GetIPv4Properties().Index,
Name = adapter.Name,
SupportsMulticast = adapter.SupportsMulticast
};
interfaces.Add(interfaceObject); interfaces.Add(interfaceObject);
} }
else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6) else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6)
{ {
var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name); var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
interfaceObject.Index = ipProperties.GetIPv6Properties().Index; {
interfaceObject.Name = adapter.Name; Index = ipProperties.GetIPv6Properties().Index,
Name = adapter.Name,
SupportsMulticast = adapter.SupportsMulticast
};
interfaces.Add(interfaceObject); interfaces.Add(interfaceObject);
} }
} }
} }
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex) catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types
{ {
// Ignore error, and attempt to continue. // Ignore error, and attempt to continue.
_logger.LogError(ex, "Error encountered parsing interfaces."); _logger.LogError(ex, "Error encountered parsing interfaces.");
} }
} }
} }
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception ex) catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types
{ {
_logger.LogError(ex, "Error obtaining interfaces."); _logger.LogError(ex, "Error obtaining interfaces.");
} }
@ -279,14 +287,14 @@ namespace Jellyfin.Networking.Manager
{ {
_logger.LogWarning("No interface information available. Using loopback interface(s)."); _logger.LogWarning("No interface information available. Using loopback interface(s).");
if (IsIPv4Enabled && !IsIPv6Enabled) if (IsIPv4Enabled)
{ {
interfaces.Add(new IPData(IPAddress.Loopback, new IPNetwork(IPAddress.Loopback, 8), "lo")); interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo"));
} }
if (!IsIPv4Enabled && IsIPv6Enabled) if (IsIPv6Enabled)
{ {
interfaces.Add(new IPData(IPAddress.IPv6Loopback, new IPNetwork(IPAddress.IPv6Loopback, 128), "lo")); interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo"));
} }
} }
@ -299,9 +307,9 @@ namespace Jellyfin.Networking.Manager
} }
/// <summary> /// <summary>
/// Initialises internal LAN cache. /// Initializes internal LAN cache.
/// </summary> /// </summary>
private void InitialiseLan(NetworkConfiguration config) private void InitializeLan(NetworkConfiguration config)
{ {
lock (_initLock) lock (_initLock)
{ {
@ -341,10 +349,6 @@ namespace Jellyfin.Networking.Manager
_excludedSubnets = NetworkExtensions.TryParseToSubnets(subnets, out var excludedSubnets, true) _excludedSubnets = NetworkExtensions.TryParseToSubnets(subnets, out var excludedSubnets, true)
? excludedSubnets ? excludedSubnets
: new List<IPNetwork>(); : new List<IPNetwork>();
_logger.LogInformation("Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
_logger.LogInformation("Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
_logger.LogInformation("Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength));
} }
} }
@ -369,12 +373,12 @@ namespace Jellyfin.Networking.Manager
.ToHashSet(); .ToHashSet();
interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList(); interfaces = interfaces.Where(x => bindAddresses.Contains(x.Address)).ToList();
if (bindAddresses.Contains(IPAddress.Loopback)) if (bindAddresses.Contains(IPAddress.Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.Loopback)))
{ {
interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo")); interfaces.Add(new IPData(IPAddress.Loopback, Network.IPv4RFC5735Loopback, "lo"));
} }
if (bindAddresses.Contains(IPAddress.IPv6Loopback)) if (bindAddresses.Contains(IPAddress.IPv6Loopback) && !interfaces.Any(i => i.Address.Equals(IPAddress.IPv6Loopback)))
{ {
interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo")); interfaces.Add(new IPData(IPAddress.IPv6Loopback, Network.IPv6RFC4291Loopback, "lo"));
} }
@ -409,15 +413,14 @@ namespace Jellyfin.Networking.Manager
interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6); interfaces.RemoveAll(x => x.AddressFamily == AddressFamily.InterNetworkV6);
} }
_logger.LogInformation("Using bind addresses: {0}", interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address));
_interfaces = interfaces; _interfaces = interfaces;
} }
} }
/// <summary> /// <summary>
/// Initialises the remote address values. /// Initializes the remote address values.
/// </summary> /// </summary>
private void InitialiseRemote(NetworkConfiguration config) private void InitializeRemote(NetworkConfiguration config)
{ {
lock (_initLock) lock (_initLock)
{ {
@ -455,13 +458,33 @@ namespace Jellyfin.Networking.Manager
/// format is subnet=ipaddress|host|uri /// format is subnet=ipaddress|host|uri
/// when subnet = 0.0.0.0, any external address matches. /// when subnet = 0.0.0.0, any external address matches.
/// </summary> /// </summary>
private void InitialiseOverrides(NetworkConfiguration config) private void InitializeOverrides(NetworkConfiguration config)
{ {
lock (_initLock) lock (_initLock)
{ {
var publishedServerUrls = new Dictionary<IPData, string>(); var publishedServerUrls = new List<PublishedServerUriOverride>();
var overrides = config.PublishedServerUriBySubnet;
// Prefer startup configuration.
var startupOverrideKey = _startupConfig[AddressOverrideKey];
if (!string.IsNullOrEmpty(startupOverrideKey))
{
publishedServerUrls.Add(
new PublishedServerUriOverride(
new IPData(IPAddress.Any, Network.IPv4Any),
startupOverrideKey,
true,
true));
publishedServerUrls.Add(
new PublishedServerUriOverride(
new IPData(IPAddress.IPv6Any, Network.IPv6Any),
startupOverrideKey,
true,
true));
_publishedServerUrls = publishedServerUrls;
return;
}
var overrides = config.PublishedServerUriBySubnet;
foreach (var entry in overrides) foreach (var entry in overrides)
{ {
var parts = entry.Split('='); var parts = entry.Split('=');
@ -475,31 +498,70 @@ namespace Jellyfin.Networking.Manager
var identifier = parts[0]; var identifier = parts[0];
if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase)) if (string.Equals(identifier, "all", StringComparison.OrdinalIgnoreCase))
{ {
publishedServerUrls[new IPData(IPAddress.Broadcast, null)] = replacement; // Drop any other overrides in case an "all" override exists
publishedServerUrls.Clear();
publishedServerUrls.Add(
new PublishedServerUriOverride(
new IPData(IPAddress.Any, Network.IPv4Any),
replacement,
true,
true));
publishedServerUrls.Add(
new PublishedServerUriOverride(
new IPData(IPAddress.IPv6Any, Network.IPv6Any),
replacement,
true,
true));
break;
} }
else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(identifier, "external", StringComparison.OrdinalIgnoreCase))
{ {
publishedServerUrls[new IPData(IPAddress.Any, Network.IPv4Any)] = replacement; publishedServerUrls.Add(
publishedServerUrls[new IPData(IPAddress.IPv6Any, Network.IPv6Any)] = replacement; new PublishedServerUriOverride(
new IPData(IPAddress.Any, Network.IPv4Any),
replacement,
false,
true));
publishedServerUrls.Add(
new PublishedServerUriOverride(
new IPData(IPAddress.IPv6Any, Network.IPv6Any),
replacement,
false,
true));
} }
else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase)) else if (string.Equals(identifier, "internal", StringComparison.OrdinalIgnoreCase))
{ {
foreach (var lan in _lanSubnets) foreach (var lan in _lanSubnets)
{ {
var lanPrefix = lan.Prefix; var lanPrefix = lan.Prefix;
publishedServerUrls[new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength))] = replacement; publishedServerUrls.Add(
new PublishedServerUriOverride(
new IPData(lanPrefix, new IPNetwork(lanPrefix, lan.PrefixLength)),
replacement,
true,
false));
} }
} }
else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result is not null) else if (NetworkExtensions.TryParseToSubnet(identifier, out var result) && result is not null)
{ {
var data = new IPData(result.Prefix, result); var data = new IPData(result.Prefix, result);
publishedServerUrls[data] = replacement; publishedServerUrls.Add(
new PublishedServerUriOverride(
data,
replacement,
true,
true));
} }
else if (TryParseInterface(identifier, out var ifaces)) else if (TryParseInterface(identifier, out var ifaces))
{ {
foreach (var iface in ifaces) foreach (var iface in ifaces)
{ {
publishedServerUrls[iface] = replacement; publishedServerUrls.Add(
new PublishedServerUriOverride(
iface,
replacement,
true,
true));
} }
} }
else else
@ -521,7 +583,7 @@ namespace Jellyfin.Networking.Manager
} }
/// <summary> /// <summary>
/// Reloads all settings and re-initialises the instance. /// Reloads all settings and re-Initializes the instance.
/// </summary> /// </summary>
/// <param name="configuration">The <see cref="NetworkConfiguration"/> to use.</param> /// <param name="configuration">The <see cref="NetworkConfiguration"/> to use.</param>
public void UpdateSettings(object configuration) public void UpdateSettings(object configuration)
@ -531,12 +593,12 @@ namespace Jellyfin.Networking.Manager
var config = (NetworkConfiguration)configuration; var config = (NetworkConfiguration)configuration;
HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6; HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6;
InitialiseLan(config); InitializeLan(config);
InitialiseRemote(config); InitializeRemote(config);
if (string.IsNullOrEmpty(MockNetworkSettings)) if (string.IsNullOrEmpty(MockNetworkSettings))
{ {
InitialiseInterfaces(); InitializeInterfaces();
} }
else // Used in testing only. else // Used in testing only.
{ {
@ -552,8 +614,10 @@ namespace Jellyfin.Networking.Manager
var index = int.Parse(parts[1], CultureInfo.InvariantCulture); var index = int.Parse(parts[1], CultureInfo.InvariantCulture);
if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6) if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6)
{ {
var data = new IPData(address, subnet, parts[2]); var data = new IPData(address, subnet, parts[2])
data.Index = index; {
Index = index
};
interfaces.Add(data); interfaces.Add(data);
} }
} }
@ -567,7 +631,9 @@ namespace Jellyfin.Networking.Manager
} }
EnforceBindSettings(config); EnforceBindSettings(config);
InitialiseOverrides(config); InitializeOverrides(config);
PrintNetworkInformation(config, false);
} }
/// <summary> /// <summary>
@ -672,20 +738,13 @@ namespace Jellyfin.Networking.Manager
/// <inheritdoc/> /// <inheritdoc/>
public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false) public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false)
{ {
if (_interfaces.Count != 0) if (_interfaces.Count > 0 || individualInterfaces)
{ {
return _interfaces; return _interfaces;
} }
// No bind address and no exclusions, so listen on all interfaces. // No bind address and no exclusions, so listen on all interfaces.
var result = new List<IPData>(); var result = new List<IPData>();
if (individualInterfaces)
{
result.AddRange(_interfaces);
return result;
}
if (IsIPv4Enabled && IsIPv6Enabled) if (IsIPv4Enabled && IsIPv6Enabled)
{ {
// Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default // Kestrel source code shows it uses Sockets.DualMode - so this also covers IPAddress.Any by default
@ -892,31 +951,34 @@ namespace Jellyfin.Networking.Manager
bindPreference = string.Empty; bindPreference = string.Empty;
int? port = null; int? port = null;
var validPublishedServerUrls = _publishedServerUrls.Where(x => x.Key.Address.Equals(IPAddress.Any) // Only consider subnets including the source IP, prefering specific overrides
|| x.Key.Address.Equals(IPAddress.IPv6Any) List<PublishedServerUriOverride> validPublishedServerUrls;
|| x.Key.Subnet.Contains(source)) if (!isInExternalSubnet)
.DistinctBy(x => x.Key) {
.OrderBy(x => x.Key.Address.Equals(IPAddress.Any) // Only use matching internal subnets
|| x.Key.Address.Equals(IPAddress.IPv6Any)) // Prefer more specific (bigger subnet prefix) overrides
validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && x.Data.Subnet.Contains(source))
.OrderByDescending(x => x.Data.Subnet.PrefixLength)
.ToList(); .ToList();
}
// Check for user override. else
foreach (var data in validPublishedServerUrls)
{ {
if (isInExternalSubnet && (data.Key.Address.Equals(IPAddress.Any) || data.Key.Address.Equals(IPAddress.IPv6Any))) // Only use matching external subnets
{ // Prefer more specific (bigger subnet prefix) overrides
// External. validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && x.Data.Subnet.Contains(source))
bindPreference = data.Value; .OrderByDescending(x => x.Data.Subnet.PrefixLength)
break; .ToList();
} }
// Get address interface. foreach (var data in validPublishedServerUrls)
var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Key.Subnet.Contains(x.Address)); {
// Get interface matching override subnet
var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address));
if (intf?.Address is not null) if (intf?.Address is not null)
{ {
// Match IP address. // If matching interface is found, use override
bindPreference = data.Value; bindPreference = data.OverrideUri;
break; break;
} }
} }
@ -927,7 +989,7 @@ namespace Jellyfin.Networking.Manager
return false; return false;
} }
// Has it got a port defined? // Handle override specifying port
var parts = bindPreference.Split(':'); var parts = bindPreference.Split(':');
if (parts.Length > 1) if (parts.Length > 1)
{ {
@ -935,18 +997,12 @@ namespace Jellyfin.Networking.Manager
{ {
bindPreference = parts[0]; bindPreference = parts[0];
port = p; port = p;
}
}
if (port is not null)
{
_logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port); _logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port);
return true;
} }
else
{
_logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
} }
_logger.LogDebug("{Source}: Matching bind address override found: {Address}", source, bindPreference);
return true; return true;
} }
@ -1053,5 +1109,19 @@ namespace Jellyfin.Networking.Manager
_logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result); _logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result);
return true; return true;
} }
private void PrintNetworkInformation(NetworkConfiguration config, bool debug = true)
{
var logLevel = debug ? LogLevel.Debug : LogLevel.Information;
if (_logger.IsEnabled(logLevel))
{
_logger.Log(logLevel, "Defined LAN addresses: {0}", _lanSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
_logger.Log(logLevel, "Defined LAN exclusions: {0}", _excludedSubnets.Select(s => s.Prefix + "/" + s.PrefixLength));
_logger.Log(logLevel, "Using LAN addresses: {0}", _lanSubnets.Where(s => !_excludedSubnets.Contains(s)).Select(s => s.Prefix + "/" + s.PrefixLength));
_logger.Log(logLevel, "Using bind addresses: {0}", _interfaces.OrderByDescending(x => x.AddressFamily == AddressFamily.InterNetwork).Select(x => x.Address));
_logger.Log(logLevel, "Remote IP filter is {0}", config.IsRemoteIPFilterBlacklist ? "Blocklist" : "Allowlist");
_logger.Log(logLevel, "Filter list: {0}", _remoteAddressFilter.Select(s => s.Prefix + "/" + s.PrefixLength));
}
}
} }
} }

View File

@ -1,7 +1,6 @@
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.Events.Authentication; using MediaBrowser.Controller.Events.Authentication;
using MediaBrowser.Model.Activity; using MediaBrowser.Model.Activity;

View File

@ -1,5 +1,4 @@
using Jellyfin.Data.Events; using Jellyfin.Data.Events.System;
using Jellyfin.Data.Events.System;
using Jellyfin.Data.Events.Users; using Jellyfin.Data.Events.Users;
using Jellyfin.Server.Implementations.Events.Consumers.Library; using Jellyfin.Server.Implementations.Events.Consumers.Library;
using Jellyfin.Server.Implementations.Events.Consumers.Security; using Jellyfin.Server.Implementations.Events.Consumers.Security;

View File

@ -78,6 +78,11 @@ public class JellyfinDbContext : DbContext
/// </summary> /// </summary>
public DbSet<User> Users => Set<User>(); public DbSet<User> Users => Set<User>();
/// <summary>
/// Gets the <see cref="DbSet{TEntity}"/> containing the trickplay metadata.
/// </summary>
public DbSet<TrickplayInfo> TrickplayInfos => Set<TrickplayInfo>();
/*public DbSet<Artwork> Artwork => Set<Artwork>(); /*public DbSet<Artwork> Artwork => Set<Artwork>();
public DbSet<Book> Books => Set<Book>(); public DbSet<Book> Books => Set<Book>();

View File

@ -0,0 +1,681 @@
// <auto-generated />
using System;
using Jellyfin.Server.Implementations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
[DbContext(typeof(JellyfinDbContext))]
[Migration("20230626233818_AddTrickplayInfos")]
partial class AddTrickplayInfos
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.7");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("DayOfWeek")
.HasColumnType("INTEGER");
b.Property<double>("EndHour")
.HasColumnType("REAL");
b.Property<double>("StartHour")
.HasColumnType("REAL");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AccessSchedules");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<string>("ItemId")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<int>("LogSeverity")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<string>("Overview")
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("ShortOverview")
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DateCreated");
b.ToTable("ActivityLogs");
});
modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId", "ItemId", "Client", "Key")
.IsUnique();
b.ToTable("CustomItemDisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ChromecastVersion")
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<string>("DashboardTheme")
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<bool>("EnableNextVideoInfoOverlay")
.HasColumnType("INTEGER");
b.Property<int?>("IndexBy")
.HasColumnType("INTEGER");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<int>("ScrollDirection")
.HasColumnType("INTEGER");
b.Property<bool>("ShowBackdrop")
.HasColumnType("INTEGER");
b.Property<bool>("ShowSidebar")
.HasColumnType("INTEGER");
b.Property<int>("SkipBackwardLength")
.HasColumnType("INTEGER");
b.Property<int>("SkipForwardLength")
.HasColumnType("INTEGER");
b.Property<string>("TvHome")
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId", "ItemId", "Client")
.IsUnique();
b.ToTable("DisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("DisplayPreferencesId")
.HasColumnType("INTEGER");
b.Property<int>("Order")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("DisplayPreferencesId");
b.ToTable("HomeSection");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<Guid?>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("ImageInfos");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<int?>("IndexBy")
.HasColumnType("INTEGER");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<bool>("RememberIndexing")
.HasColumnType("INTEGER");
b.Property<bool>("RememberSorting")
.HasColumnType("INTEGER");
b.Property<string>("SortBy")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<int>("ViewType")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("ItemDisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<Guid?>("Permission_Permissions_Guid")
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<Guid?>("UserId")
.HasColumnType("TEXT");
b.Property<bool>("Value")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId", "Kind")
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("Permissions");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<Guid?>("Preference_Preferences_Guid")
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<Guid?>("UserId")
.HasColumnType("TEXT");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(65535)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId", "Kind")
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("Preferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AccessToken")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateLastActivity")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AccessToken")
.IsUnique();
b.ToTable("ApiKeys");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AccessToken")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("AppName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<string>("AppVersion")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateLastActivity")
.HasColumnType("TEXT");
b.Property<DateTime>("DateModified")
.HasColumnType("TEXT");
b.Property<string>("DeviceId")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("DeviceName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DeviceId");
b.HasIndex("AccessToken", "DateLastActivity");
b.HasIndex("DeviceId", "DateLastActivity");
b.HasIndex("UserId", "DeviceId");
b.ToTable("Devices");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CustomName")
.HasColumnType("TEXT");
b.Property<string>("DeviceId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DeviceId")
.IsUnique();
b.ToTable("DeviceOptions");
});
modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
{
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<int>("Width")
.HasColumnType("INTEGER");
b.Property<int>("Bandwidth")
.HasColumnType("INTEGER");
b.Property<int>("Height")
.HasColumnType("INTEGER");
b.Property<int>("Interval")
.HasColumnType("INTEGER");
b.Property<int>("ThumbnailCount")
.HasColumnType("INTEGER");
b.Property<int>("TileHeight")
.HasColumnType("INTEGER");
b.Property<int>("TileWidth")
.HasColumnType("INTEGER");
b.HasKey("ItemId", "Width");
b.ToTable("TrickplayInfos");
});
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AudioLanguagePreference")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("AuthenticationProviderId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<bool>("DisplayCollectionsView")
.HasColumnType("INTEGER");
b.Property<bool>("DisplayMissingEpisodes")
.HasColumnType("INTEGER");
b.Property<bool>("EnableAutoLogin")
.HasColumnType("INTEGER");
b.Property<bool>("EnableLocalPassword")
.HasColumnType("INTEGER");
b.Property<bool>("EnableNextEpisodeAutoPlay")
.HasColumnType("INTEGER");
b.Property<bool>("EnableUserPreferenceAccess")
.HasColumnType("INTEGER");
b.Property<bool>("HidePlayedInLatest")
.HasColumnType("INTEGER");
b.Property<long>("InternalId")
.HasColumnType("INTEGER");
b.Property<int>("InvalidLoginAttemptCount")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastActivityDate")
.HasColumnType("TEXT");
b.Property<DateTime?>("LastLoginDate")
.HasColumnType("TEXT");
b.Property<int?>("LoginAttemptsBeforeLockout")
.HasColumnType("INTEGER");
b.Property<int>("MaxActiveSessions")
.HasColumnType("INTEGER");
b.Property<int?>("MaxParentalAgeRating")
.HasColumnType("INTEGER");
b.Property<bool>("MustUpdatePassword")
.HasColumnType("INTEGER");
b.Property<string>("Password")
.HasMaxLength(65535)
.HasColumnType("TEXT");
b.Property<string>("PasswordResetProviderId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<bool>("PlayDefaultAudioTrack")
.HasColumnType("INTEGER");
b.Property<bool>("RememberAudioSelections")
.HasColumnType("INTEGER");
b.Property<bool>("RememberSubtitleSelections")
.HasColumnType("INTEGER");
b.Property<int?>("RemoteClientBitrateLimit")
.HasColumnType("INTEGER");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("SubtitleLanguagePreference")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<int>("SubtitleMode")
.HasColumnType("INTEGER");
b.Property<int>("SyncPlayAccess")
.HasColumnType("INTEGER");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT")
.UseCollation("NOCASE");
b.HasKey("Id");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("AccessSchedules")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("DisplayPreferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
{
b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
.WithMany("HomeSections")
.HasForeignKey("DisplayPreferencesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithOne("ProfileImage")
.HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("ItemDisplayPreferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Permissions")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Preferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.Navigation("HomeSections");
});
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Navigation("AccessSchedules");
b.Navigation("DisplayPreferences");
b.Navigation("ItemDisplayPreferences");
b.Navigation("Permissions");
b.Navigation("Preferences");
b.Navigation("ProfileImage");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,40 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
/// <inheritdoc />
public partial class AddTrickplayInfos : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "TrickplayInfos",
columns: table => new
{
ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
Width = table.Column<int>(type: "INTEGER", nullable: false),
Height = table.Column<int>(type: "INTEGER", nullable: false),
TileWidth = table.Column<int>(type: "INTEGER", nullable: false),
TileHeight = table.Column<int>(type: "INTEGER", nullable: false),
ThumbnailCount = table.Column<int>(type: "INTEGER", nullable: false),
Interval = table.Column<int>(type: "INTEGER", nullable: false),
Bandwidth = table.Column<int>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_TrickplayInfos", x => new { x.ItemId, x.Width });
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "TrickplayInfos");
}
}
}

View File

@ -0,0 +1,654 @@
// <auto-generated />
using System;
using Jellyfin.Server.Implementations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
[DbContext(typeof(JellyfinDbContext))]
[Migration("20230923170422_UserCastReceiver")]
partial class UserCastReceiver
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.11");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("DayOfWeek")
.HasColumnType("INTEGER");
b.Property<double>("EndHour")
.HasColumnType("REAL");
b.Property<double>("StartHour")
.HasColumnType("REAL");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AccessSchedules");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<string>("ItemId")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<int>("LogSeverity")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<string>("Overview")
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("ShortOverview")
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DateCreated");
b.ToTable("ActivityLogs");
});
modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId", "ItemId", "Client", "Key")
.IsUnique();
b.ToTable("CustomItemDisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ChromecastVersion")
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<string>("DashboardTheme")
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<bool>("EnableNextVideoInfoOverlay")
.HasColumnType("INTEGER");
b.Property<int?>("IndexBy")
.HasColumnType("INTEGER");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<int>("ScrollDirection")
.HasColumnType("INTEGER");
b.Property<bool>("ShowBackdrop")
.HasColumnType("INTEGER");
b.Property<bool>("ShowSidebar")
.HasColumnType("INTEGER");
b.Property<int>("SkipBackwardLength")
.HasColumnType("INTEGER");
b.Property<int>("SkipForwardLength")
.HasColumnType("INTEGER");
b.Property<string>("TvHome")
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId", "ItemId", "Client")
.IsUnique();
b.ToTable("DisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("DisplayPreferencesId")
.HasColumnType("INTEGER");
b.Property<int>("Order")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("DisplayPreferencesId");
b.ToTable("HomeSection");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<Guid?>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("ImageInfos");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<int?>("IndexBy")
.HasColumnType("INTEGER");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<bool>("RememberIndexing")
.HasColumnType("INTEGER");
b.Property<bool>("RememberSorting")
.HasColumnType("INTEGER");
b.Property<string>("SortBy")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<int>("ViewType")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("ItemDisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<Guid?>("Permission_Permissions_Guid")
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<Guid?>("UserId")
.HasColumnType("TEXT");
b.Property<bool>("Value")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId", "Kind")
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("Permissions");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<Guid?>("Preference_Preferences_Guid")
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<Guid?>("UserId")
.HasColumnType("TEXT");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(65535)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId", "Kind")
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("Preferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AccessToken")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateLastActivity")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AccessToken")
.IsUnique();
b.ToTable("ApiKeys");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AccessToken")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("AppName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<string>("AppVersion")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateLastActivity")
.HasColumnType("TEXT");
b.Property<DateTime>("DateModified")
.HasColumnType("TEXT");
b.Property<string>("DeviceId")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("DeviceName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DeviceId");
b.HasIndex("AccessToken", "DateLastActivity");
b.HasIndex("DeviceId", "DateLastActivity");
b.HasIndex("UserId", "DeviceId");
b.ToTable("Devices");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CustomName")
.HasColumnType("TEXT");
b.Property<string>("DeviceId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DeviceId")
.IsUnique();
b.ToTable("DeviceOptions");
});
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AudioLanguagePreference")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("AuthenticationProviderId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("CastReceiverId")
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<bool>("DisplayCollectionsView")
.HasColumnType("INTEGER");
b.Property<bool>("DisplayMissingEpisodes")
.HasColumnType("INTEGER");
b.Property<bool>("EnableAutoLogin")
.HasColumnType("INTEGER");
b.Property<bool>("EnableLocalPassword")
.HasColumnType("INTEGER");
b.Property<bool>("EnableNextEpisodeAutoPlay")
.HasColumnType("INTEGER");
b.Property<bool>("EnableUserPreferenceAccess")
.HasColumnType("INTEGER");
b.Property<bool>("HidePlayedInLatest")
.HasColumnType("INTEGER");
b.Property<long>("InternalId")
.HasColumnType("INTEGER");
b.Property<int>("InvalidLoginAttemptCount")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastActivityDate")
.HasColumnType("TEXT");
b.Property<DateTime?>("LastLoginDate")
.HasColumnType("TEXT");
b.Property<int?>("LoginAttemptsBeforeLockout")
.HasColumnType("INTEGER");
b.Property<int>("MaxActiveSessions")
.HasColumnType("INTEGER");
b.Property<int?>("MaxParentalAgeRating")
.HasColumnType("INTEGER");
b.Property<bool>("MustUpdatePassword")
.HasColumnType("INTEGER");
b.Property<string>("Password")
.HasMaxLength(65535)
.HasColumnType("TEXT");
b.Property<string>("PasswordResetProviderId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<bool>("PlayDefaultAudioTrack")
.HasColumnType("INTEGER");
b.Property<bool>("RememberAudioSelections")
.HasColumnType("INTEGER");
b.Property<bool>("RememberSubtitleSelections")
.HasColumnType("INTEGER");
b.Property<int?>("RemoteClientBitrateLimit")
.HasColumnType("INTEGER");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("SubtitleLanguagePreference")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<int>("SubtitleMode")
.HasColumnType("INTEGER");
b.Property<int>("SyncPlayAccess")
.HasColumnType("INTEGER");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT")
.UseCollation("NOCASE");
b.HasKey("Id");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("AccessSchedules")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("DisplayPreferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
{
b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
.WithMany("HomeSections")
.HasForeignKey("DisplayPreferencesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithOne("ProfileImage")
.HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("ItemDisplayPreferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Permissions")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Preferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.Navigation("HomeSections");
});
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Navigation("AccessSchedules");
b.Navigation("DisplayPreferences");
b.Navigation("ItemDisplayPreferences");
b.Navigation("Permissions");
b.Navigation("Preferences");
b.Navigation("ProfileImage");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
/// <inheritdoc />
public partial class UserCastReceiver : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "CastReceiverId",
table: "Users",
type: "TEXT",
maxLength: 32,
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CastReceiverId",
table: "Users");
}
}
}

View File

@ -1,4 +1,4 @@
// <auto-generated /> // <auto-generated />
using System; using System;
using Jellyfin.Server.Implementations; using Jellyfin.Server.Implementations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.5"); modelBuilder.HasAnnotation("ProductVersion", "7.0.11");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b => modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{ {
@ -442,6 +442,37 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("DeviceOptions"); b.ToTable("DeviceOptions");
}); });
modelBuilder.Entity("Jellyfin.Data.Entities.TrickplayInfo", b =>
{
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<int>("Width")
.HasColumnType("INTEGER");
b.Property<int>("Bandwidth")
.HasColumnType("INTEGER");
b.Property<int>("Height")
.HasColumnType("INTEGER");
b.Property<int>("Interval")
.HasColumnType("INTEGER");
b.Property<int>("ThumbnailCount")
.HasColumnType("INTEGER");
b.Property<int>("TileHeight")
.HasColumnType("INTEGER");
b.Property<int>("TileWidth")
.HasColumnType("INTEGER");
b.HasKey("ItemId", "Width");
b.ToTable("TrickplayInfos");
});
modelBuilder.Entity("Jellyfin.Data.Entities.User", b => modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -457,6 +488,10 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasMaxLength(255) .HasMaxLength(255)
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("CastReceiverId")
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<bool>("DisplayCollectionsView") b.Property<bool>("DisplayCollectionsView")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");

View File

@ -0,0 +1,18 @@
using Jellyfin.Data.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Jellyfin.Server.Implementations.ModelConfiguration
{
/// <summary>
/// FluentAPI configuration for the TrickplayInfo entity.
/// </summary>
public class TrickplayInfoConfiguration : IEntityTypeConfiguration<TrickplayInfo>
{
/// <inheritdoc/>
public void Configure(EntityTypeBuilder<TrickplayInfo> builder)
{
builder.HasKey(info => new { info.ItemId, info.Width });
}
}
}

View File

@ -49,14 +49,13 @@ namespace Jellyfin.Server.Implementations.Security
/// <summary> /// <summary>
/// Gets the authorization. /// Gets the authorization.
/// </summary> /// </summary>
/// <param name="httpReq">The HTTP req.</param> /// <param name="httpContext">The HTTP context.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns> /// <returns>Dictionary{System.StringSystem.String}.</returns>
private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpReq) private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpContext)
{ {
var auth = GetAuthorizationDictionary(httpReq); var authInfo = await GetAuthorizationInfo(httpContext.Request).ConfigureAwait(false);
var authInfo = await GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query).ConfigureAwait(false);
httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo; httpContext.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
return authInfo; return authInfo;
} }
@ -80,7 +79,6 @@ namespace Jellyfin.Server.Implementations.Security
auth.TryGetValue("Token", out token); auth.TryGetValue("Token", out token);
} }
#pragma warning disable CA1508 // string.IsNullOrEmpty(token) is always false.
if (string.IsNullOrEmpty(token)) if (string.IsNullOrEmpty(token))
{ {
token = headers["X-Emby-Token"]; token = headers["X-Emby-Token"];
@ -118,7 +116,6 @@ namespace Jellyfin.Server.Implementations.Security
// Request doesn't contain a token. // Request doesn't contain a token.
return authInfo; return authInfo;
} }
#pragma warning restore CA1508
authInfo.HasToken = true; authInfo.HasToken = true;
var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false); var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
@ -219,24 +216,7 @@ namespace Jellyfin.Server.Implementations.Security
/// <summary> /// <summary>
/// Gets the auth. /// Gets the auth.
/// </summary> /// </summary>
/// <param name="httpReq">The HTTP req.</param> /// <param name="httpReq">The HTTP request.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private static Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq)
{
var auth = httpReq.Request.Headers["X-Emby-Authorization"];
if (string.IsNullOrEmpty(auth))
{
auth = httpReq.Request.Headers[HeaderNames.Authorization];
}
return auth.Count > 0 ? GetAuthorization(auth[0]) : null;
}
/// <summary>
/// Gets the auth.
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns> /// <returns>Dictionary{System.StringSystem.String}.</returns>
private static Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq) private static Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
{ {

View File

@ -0,0 +1,474 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Implementations.Trickplay;
/// <summary>
/// ITrickplayManager implementation.
/// </summary>
public class TrickplayManager : ITrickplayManager
{
private readonly ILogger<TrickplayManager> _logger;
private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
private readonly EncodingHelper _encodingHelper;
private readonly ILibraryManager _libraryManager;
private readonly IServerConfigurationManager _config;
private readonly IImageEncoder _imageEncoder;
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IApplicationPaths _appPaths;
private static readonly SemaphoreSlim _resourcePool = new(1, 1);
private static readonly string[] _trickplayImgExtensions = { ".jpg" };
/// <summary>
/// Initializes a new instance of the <see cref="TrickplayManager"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="mediaEncoder">The media encoder.</param>
/// <param name="fileSystem">The file systen.</param>
/// <param name="encodingHelper">The encoding helper.</param>
/// <param name="libraryManager">The library manager.</param>
/// <param name="config">The server configuration manager.</param>
/// <param name="imageEncoder">The image encoder.</param>
/// <param name="dbProvider">The database provider.</param>
/// <param name="appPaths">The application paths.</param>
public TrickplayManager(
ILogger<TrickplayManager> logger,
IMediaEncoder mediaEncoder,
IFileSystem fileSystem,
EncodingHelper encodingHelper,
ILibraryManager libraryManager,
IServerConfigurationManager config,
IImageEncoder imageEncoder,
IDbContextFactory<JellyfinDbContext> dbProvider,
IApplicationPaths appPaths)
{
_logger = logger;
_mediaEncoder = mediaEncoder;
_fileSystem = fileSystem;
_encodingHelper = encodingHelper;
_libraryManager = libraryManager;
_config = config;
_imageEncoder = imageEncoder;
_dbProvider = dbProvider;
_appPaths = appPaths;
}
/// <inheritdoc />
public async Task RefreshTrickplayDataAsync(Video video, bool replace, CancellationToken cancellationToken)
{
_logger.LogDebug("Trickplay refresh for {ItemId} (replace existing: {Replace})", video.Id, replace);
var options = _config.Configuration.TrickplayOptions;
foreach (var width in options.WidthResolutions)
{
cancellationToken.ThrowIfCancellationRequested();
await RefreshTrickplayDataInternal(
video,
replace,
width,
options,
cancellationToken).ConfigureAwait(false);
}
}
private async Task RefreshTrickplayDataInternal(
Video video,
bool replace,
int width,
TrickplayOptions options,
CancellationToken cancellationToken)
{
if (!CanGenerateTrickplay(video, options.Interval))
{
return;
}
var imgTempDir = string.Empty;
var outputDir = GetTrickplayDirectory(video, width);
await _resourcePool.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (!replace && Directory.Exists(outputDir) && (await GetTrickplayResolutions(video.Id).ConfigureAwait(false)).ContainsKey(width))
{
_logger.LogDebug("Found existing trickplay files for {ItemId}. Exiting.", video.Id);
return;
}
// Extract images
// Note: Media sources under parent items exist as their own video/item as well. Only use this video stream for trickplay.
var mediaSource = video.GetMediaSources(false).Find(source => Guid.Parse(source.Id).Equals(video.Id));
if (mediaSource is null)
{
_logger.LogDebug("Found no matching media source for item {ItemId}", video.Id);
return;
}
var mediaPath = mediaSource.Path;
var mediaStream = mediaSource.VideoStream;
var container = mediaSource.Container;
_logger.LogInformation("Creating trickplay files at {Width} width, for {Path} [ID: {ItemId}]", width, mediaPath, video.Id);
imgTempDir = await _mediaEncoder.ExtractVideoImagesOnIntervalAccelerated(
mediaPath,
container,
mediaSource,
mediaStream,
width,
TimeSpan.FromMilliseconds(options.Interval),
options.EnableHwAcceleration,
options.ProcessThreads,
options.Qscale,
options.ProcessPriority,
_encodingHelper,
cancellationToken).ConfigureAwait(false);
if (string.IsNullOrEmpty(imgTempDir) || !Directory.Exists(imgTempDir))
{
throw new InvalidOperationException("Null or invalid directory from media encoder.");
}
var images = _fileSystem.GetFiles(imgTempDir, _trickplayImgExtensions, false, false)
.Select(i => i.FullName)
.OrderBy(i => i)
.ToList();
// Create tiles
var trickplayInfo = CreateTiles(images, width, options, outputDir);
// Save tiles info
try
{
if (trickplayInfo is not null)
{
trickplayInfo.ItemId = video.Id;
await SaveTrickplayInfo(trickplayInfo).ConfigureAwait(false);
_logger.LogInformation("Finished creation of trickplay files for {0}", mediaPath);
}
else
{
throw new InvalidOperationException("Null trickplay tiles info from CreateTiles.");
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error while saving trickplay tiles info.");
// Make sure no files stay in metadata folders on failure
// if tiles info wasn't saved.
Directory.Delete(outputDir, true);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Error creating trickplay images.");
}
finally
{
_resourcePool.Release();
if (!string.IsNullOrEmpty(imgTempDir))
{
Directory.Delete(imgTempDir, true);
}
}
}
/// <inheritdoc />
public TrickplayInfo CreateTiles(List<string> images, int width, TrickplayOptions options, string outputDir)
{
if (images.Count == 0)
{
throw new ArgumentException("Can't create trickplay from 0 images.");
}
var workDir = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(workDir);
var trickplayInfo = new TrickplayInfo
{
Width = width,
Interval = options.Interval,
TileWidth = options.TileWidth,
TileHeight = options.TileHeight,
ThumbnailCount = images.Count,
// Set during image generation
Height = 0,
Bandwidth = 0
};
/*
* Generate trickplay tiles from sets of thumbnails
*/
var imageOptions = new ImageCollageOptions
{
Width = trickplayInfo.TileWidth,
Height = trickplayInfo.TileHeight
};
var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
var requiredTiles = (int)Math.Ceiling((double)images.Count / thumbnailsPerTile);
for (int i = 0; i < requiredTiles; i++)
{
// Set output/input paths
var tilePath = Path.Combine(workDir, $"{i}.jpg");
imageOptions.OutputPath = tilePath;
imageOptions.InputPaths = images.GetRange(i * thumbnailsPerTile, Math.Min(thumbnailsPerTile, images.Count - (i * thumbnailsPerTile)));
// Generate image and use returned height for tiles info
var height = _imageEncoder.CreateTrickplayTile(imageOptions, options.JpegQuality, trickplayInfo.Width, trickplayInfo.Height != 0 ? trickplayInfo.Height : null);
if (trickplayInfo.Height == 0)
{
trickplayInfo.Height = height;
}
// Update bitrate
var bitrate = (int)Math.Ceiling((decimal)new FileInfo(tilePath).Length * 8 / trickplayInfo.TileWidth / trickplayInfo.TileHeight / (trickplayInfo.Interval / 1000));
trickplayInfo.Bandwidth = Math.Max(trickplayInfo.Bandwidth, bitrate);
}
/*
* Move trickplay tiles to output directory
*/
Directory.CreateDirectory(Directory.GetParent(outputDir)!.FullName);
// Replace existing tiles if they already exist
if (Directory.Exists(outputDir))
{
Directory.Delete(outputDir, true);
}
MoveDirectory(workDir, outputDir);
return trickplayInfo;
}
private bool CanGenerateTrickplay(Video video, int interval)
{
var videoType = video.VideoType;
if (videoType == VideoType.Iso || videoType == VideoType.Dvd || videoType == VideoType.BluRay)
{
return false;
}
if (video.IsPlaceHolder)
{
return false;
}
if (video.IsShortcut)
{
return false;
}
if (!video.IsCompleteMedia)
{
return false;
}
if (!video.RunTimeTicks.HasValue || video.RunTimeTicks.Value < TimeSpan.FromMilliseconds(interval).Ticks)
{
return false;
}
var libraryOptions = _libraryManager.GetLibraryOptions(video);
if (libraryOptions is null || !libraryOptions.EnableTrickplayImageExtraction)
{
return false;
}
// Can't extract images if there are no video streams
return video.GetMediaStreams().Count > 0;
}
/// <inheritdoc />
public async Task<Dictionary<int, TrickplayInfo>> GetTrickplayResolutions(Guid itemId)
{
var trickplayResolutions = new Dictionary<int, TrickplayInfo>();
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
var trickplayInfos = await dbContext.TrickplayInfos
.AsNoTracking()
.Where(i => i.ItemId.Equals(itemId))
.ToListAsync()
.ConfigureAwait(false);
foreach (var info in trickplayInfos)
{
trickplayResolutions[info.Width] = info;
}
}
return trickplayResolutions;
}
/// <inheritdoc />
public async Task SaveTrickplayInfo(TrickplayInfo info)
{
var dbContext = await _dbProvider.CreateDbContextAsync().ConfigureAwait(false);
await using (dbContext.ConfigureAwait(false))
{
var oldInfo = await dbContext.TrickplayInfos.FindAsync(info.ItemId, info.Width).ConfigureAwait(false);
if (oldInfo is not null)
{
dbContext.TrickplayInfos.Remove(oldInfo);
}
dbContext.Add(info);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
}
/// <inheritdoc />
public async Task<Dictionary<string, Dictionary<int, TrickplayInfo>>> GetTrickplayManifest(BaseItem item)
{
var trickplayManifest = new Dictionary<string, Dictionary<int, TrickplayInfo>>();
foreach (var mediaSource in item.GetMediaSources(false))
{
var mediaSourceId = Guid.Parse(mediaSource.Id);
var trickplayResolutions = await GetTrickplayResolutions(mediaSourceId).ConfigureAwait(false);
if (trickplayResolutions.Count > 0)
{
trickplayManifest[mediaSource.Id] = trickplayResolutions;
}
}
return trickplayManifest;
}
/// <inheritdoc />
public string GetTrickplayTilePath(BaseItem item, int width, int index)
{
return Path.Combine(GetTrickplayDirectory(item, width), index + ".jpg");
}
/// <inheritdoc />
public async Task<string?> GetHlsPlaylist(Guid itemId, int width, string? apiKey)
{
var trickplayResolutions = await GetTrickplayResolutions(itemId).ConfigureAwait(false);
if (trickplayResolutions is not null && trickplayResolutions.TryGetValue(width, out var trickplayInfo))
{
var builder = new StringBuilder(128);
if (trickplayInfo.ThumbnailCount > 0)
{
const string urlFormat = "Trickplay/{0}/{1}.jpg?MediaSourceId={2}&api_key={3}";
const string decimalFormat = "{0:0.###}";
var resolution = $"{trickplayInfo.Width}x{trickplayInfo.Height}";
var layout = $"{trickplayInfo.TileWidth}x{trickplayInfo.TileHeight}";
var thumbnailsPerTile = trickplayInfo.TileWidth * trickplayInfo.TileHeight;
var thumbnailDuration = trickplayInfo.Interval / 1000d;
var infDuration = thumbnailDuration * thumbnailsPerTile;
var tileCount = (int)Math.Ceiling((decimal)trickplayInfo.ThumbnailCount / thumbnailsPerTile);
builder
.AppendLine("#EXTM3U")
.Append("#EXT-X-TARGETDURATION:")
.AppendLine(tileCount.ToString(CultureInfo.InvariantCulture))
.AppendLine("#EXT-X-VERSION:7")
.AppendLine("#EXT-X-MEDIA-SEQUENCE:1")
.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD")
.AppendLine("#EXT-X-IMAGES-ONLY");
for (int i = 0; i < tileCount; i++)
{
// All tiles prior to the last must contain full amount of thumbnails (no black).
if (i == tileCount - 1)
{
thumbnailsPerTile = trickplayInfo.ThumbnailCount - (i * thumbnailsPerTile);
infDuration = thumbnailDuration * thumbnailsPerTile;
}
// EXTINF
builder
.Append("#EXTINF:")
.AppendFormat(CultureInfo.InvariantCulture, decimalFormat, infDuration)
.AppendLine(",");
// EXT-X-TILES
builder
.Append("#EXT-X-TILES:RESOLUTION=")
.Append(resolution)
.Append(",LAYOUT=")
.Append(layout)
.Append(",DURATION=")
.AppendFormat(CultureInfo.InvariantCulture, decimalFormat, thumbnailDuration)
.AppendLine();
// URL
builder
.AppendFormat(
CultureInfo.InvariantCulture,
urlFormat,
width.ToString(CultureInfo.InvariantCulture),
i.ToString(CultureInfo.InvariantCulture),
itemId.ToString("N"),
apiKey)
.AppendLine();
}
builder.AppendLine("#EXT-X-ENDLIST");
return builder.ToString();
}
}
return null;
}
private string GetTrickplayDirectory(BaseItem item, int? width = null)
{
var path = Path.Combine(item.GetInternalMetadataPath(), "trickplay");
return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path;
}
private void MoveDirectory(string source, string destination)
{
try
{
Directory.Move(source, destination);
}
catch (IOException)
{
// Cross device move requires a copy
Directory.CreateDirectory(destination);
foreach (string file in Directory.GetFiles(source))
{
File.Copy(file, Path.Join(destination, Path.GetFileName(file)), true);
}
Directory.Delete(source, true);
}
}
}

View File

@ -15,6 +15,7 @@ using MediaBrowser.Common;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Events; using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -43,6 +44,7 @@ namespace Jellyfin.Server.Implementations.Users
private readonly InvalidAuthProvider _invalidAuthProvider; private readonly InvalidAuthProvider _invalidAuthProvider;
private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider; private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider;
private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider; private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
private readonly IServerConfigurationManager _serverConfigurationManager;
private readonly IDictionary<Guid, User> _users; private readonly IDictionary<Guid, User> _users;
@ -55,13 +57,15 @@ namespace Jellyfin.Server.Implementations.Users
/// <param name="appHost">The application host.</param> /// <param name="appHost">The application host.</param>
/// <param name="imageProcessor">The image processor.</param> /// <param name="imageProcessor">The image processor.</param>
/// <param name="logger">The logger.</param> /// <param name="logger">The logger.</param>
/// <param name="serverConfigurationManager">The system config manager.</param>
public UserManager( public UserManager(
IDbContextFactory<JellyfinDbContext> dbProvider, IDbContextFactory<JellyfinDbContext> dbProvider,
IEventManager eventManager, IEventManager eventManager,
INetworkManager networkManager, INetworkManager networkManager,
IApplicationHost appHost, IApplicationHost appHost,
IImageProcessor imageProcessor, IImageProcessor imageProcessor,
ILogger<UserManager> logger) ILogger<UserManager> logger,
IServerConfigurationManager serverConfigurationManager)
{ {
_dbProvider = dbProvider; _dbProvider = dbProvider;
_eventManager = eventManager; _eventManager = eventManager;
@ -69,6 +73,7 @@ namespace Jellyfin.Server.Implementations.Users
_appHost = appHost; _appHost = appHost;
_imageProcessor = imageProcessor; _imageProcessor = imageProcessor;
_logger = logger; _logger = logger;
_serverConfigurationManager = serverConfigurationManager;
_passwordResetProviders = appHost.GetExports<IPasswordResetProvider>(); _passwordResetProviders = appHost.GetExports<IPasswordResetProvider>();
_authenticationProviders = appHost.GetExports<IAuthenticationProvider>(); _authenticationProviders = appHost.GetExports<IAuthenticationProvider>();
@ -103,7 +108,7 @@ namespace Jellyfin.Server.Implementations.Users
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @ // This is some regex that matches only on unicode "word" characters, as well as -, _ and @
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
// Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( ) // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( )
[GeneratedRegex("^[\\w\\ \\-'._@]+$")] [GeneratedRegex(@"^[\w\ \-'._@]+$")]
private static partial Regex ValidUsernameRegex(); private static partial Regex ValidUsernameRegex();
/// <inheritdoc/> /// <inheritdoc/>
@ -288,6 +293,7 @@ namespace Jellyfin.Server.Implementations.Users
public UserDto GetUserDto(User user, string? remoteEndPoint = null) public UserDto GetUserDto(User user, string? remoteEndPoint = null)
{ {
var hasPassword = GetAuthenticationProvider(user).HasPassword(user); var hasPassword = GetAuthenticationProvider(user).HasPassword(user);
var castReceiverApplications = _serverConfigurationManager.Configuration.CastReceiverApplications;
return new UserDto return new UserDto
{ {
Name = user.Username, Name = user.Username,
@ -315,7 +321,11 @@ namespace Jellyfin.Server.Implementations.Users
OrderedViews = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews), OrderedViews = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews),
GroupedFolders = user.GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders), GroupedFolders = user.GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders),
MyMediaExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes), MyMediaExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.MyMediaExcludes),
LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes) LatestItemsExcludes = user.GetPreferenceValues<Guid>(PreferenceKind.LatestItemExcludes),
CastReceiverId = string.IsNullOrEmpty(user.CastReceiverId)
? castReceiverApplications.FirstOrDefault()?.Id
: castReceiverApplications.FirstOrDefault(c => string.Equals(c.Id, user.CastReceiverId, StringComparison.Ordinal))?.Id
?? castReceiverApplications.FirstOrDefault()?.Id
}, },
Policy = new UserPolicy Policy = new UserPolicy
{ {
@ -604,6 +614,13 @@ namespace Jellyfin.Server.Implementations.Users
user.RememberSubtitleSelections = config.RememberSubtitleSelections; user.RememberSubtitleSelections = config.RememberSubtitleSelections;
user.SubtitleLanguagePreference = config.SubtitleLanguagePreference; user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
// Only set cast receiver id if it is passed in and it exists in the server config.
if (!string.IsNullOrEmpty(config.CastReceiverId)
&& _serverConfigurationManager.Configuration.CastReceiverApplications.Any(c => string.Equals(c.Id, config.CastReceiverId, StringComparison.Ordinal)))
{
user.CastReceiverId = config.CastReceiverId;
}
user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews); user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders); user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes); user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);

View File

@ -11,6 +11,7 @@ using Jellyfin.Server.Implementations.Activity;
using Jellyfin.Server.Implementations.Devices; using Jellyfin.Server.Implementations.Devices;
using Jellyfin.Server.Implementations.Events; using Jellyfin.Server.Implementations.Events;
using Jellyfin.Server.Implementations.Security; using Jellyfin.Server.Implementations.Security;
using Jellyfin.Server.Implementations.Trickplay;
using Jellyfin.Server.Implementations.Users; using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.BaseItemManager; using MediaBrowser.Controller.BaseItemManager;
@ -21,6 +22,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.Lyrics;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Trickplay;
using MediaBrowser.Model.Activity; using MediaBrowser.Model.Activity;
using MediaBrowser.Providers.Lyric; using MediaBrowser.Providers.Lyric;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@ -78,6 +80,7 @@ namespace Jellyfin.Server
serviceCollection.AddSingleton<IUserManager, UserManager>(); serviceCollection.AddSingleton<IUserManager, UserManager>();
serviceCollection.AddScoped<IDisplayPreferencesManager, DisplayPreferencesManager>(); serviceCollection.AddScoped<IDisplayPreferencesManager, DisplayPreferencesManager>();
serviceCollection.AddSingleton<IDeviceManager, DeviceManager>(); serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
serviceCollection.AddSingleton<ITrickplayManager, TrickplayManager>();
// TODO search the assemblies instead of adding them manually? // TODO search the assemblies instead of adding them manually?
serviceCollection.AddSingleton<IWebSocketListener, SessionWebSocketListener>(); serviceCollection.AddSingleton<IWebSocketListener, SessionWebSocketListener>();

View File

@ -282,7 +282,7 @@ namespace Jellyfin.Server.Extensions
AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength); AddIPAddress(config, options, subnet.Prefix, subnet.PrefixLength);
} }
} }
else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses)) else if (NetworkExtensions.TryParseHost(allowedProxies[i], out var addresses, config.EnableIPv4, config.EnableIPv6))
{ {
foreach (var address in addresses) foreach (var address in addresses)
{ {

View File

@ -3,7 +3,6 @@ using System.IO;
using System.Net; using System.Net;
using Jellyfin.Server.Helpers; using Jellyfin.Server.Helpers;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Extensions;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@ -36,7 +35,7 @@ public static class WebHostBuilderExtensions
return builder return builder
.UseKestrel((builderContext, options) => .UseKestrel((builderContext, options) =>
{ {
var addresses = appHost.NetManager.GetAllBindInterfaces(); var addresses = appHost.NetManager.GetAllBindInterfaces(true);
bool flagged = false; bool flagged = false;
foreach (var netAdd in addresses) foreach (var netAdd in addresses)

View File

@ -42,7 +42,8 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.RemoveDownloadImagesInAdvance), typeof(Routines.RemoveDownloadImagesInAdvance),
typeof(Routines.MigrateAuthenticationDb), typeof(Routines.MigrateAuthenticationDb),
typeof(Routines.FixPlaylistOwner), typeof(Routines.FixPlaylistOwner),
typeof(Routines.MigrateRatingLevels) typeof(Routines.MigrateRatingLevels),
typeof(Routines.AddDefaultCastReceivers)
}; };
/// <summary> /// <summary>

View File

@ -0,0 +1,55 @@
using System;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.System;
namespace Jellyfin.Server.Migrations.Routines;
/// <summary>
/// Migration to add the default cast receivers to the system config.
/// </summary>
public class AddDefaultCastReceivers : IMigrationRoutine
{
private readonly IServerConfigurationManager _serverConfigurationManager;
/// <summary>
/// Initializes a new instance of the <see cref="AddDefaultCastReceivers"/> class.
/// </summary>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
public AddDefaultCastReceivers(IServerConfigurationManager serverConfigurationManager)
{
_serverConfigurationManager = serverConfigurationManager;
}
/// <inheritdoc />
public Guid Id => new("34A1A1C4-5572-418E-A2F8-32CDFE2668E8");
/// <inheritdoc />
public string Name => "AddDefaultCastReceivers";
/// <inheritdoc />
public bool PerformOnNewInstall => true;
/// <inheritdoc />
public void Perform()
{
// Only add if receiver list is empty.
if (_serverConfigurationManager.Configuration.CastReceiverApplications.Length == 0)
{
_serverConfigurationManager.Configuration.CastReceiverApplications = new CastReceiverApplication[]
{
new()
{
Id = "F007D354",
Name = "Stable"
},
new()
{
Id = "6F511C87",
Name = "Unstable"
}
};
_serverConfigurationManager.SaveConfiguration();
}
}
}

View File

@ -3,7 +3,6 @@ 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.Model.Globalization; using MediaBrowser.Model.Globalization;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;

View File

@ -1,10 +1,10 @@
using System; using System;
using System.Globalization;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Mime; using System.Net.Mime;
using System.Text; using System.Text;
using Emby.Dlna.Extensions;
using Jellyfin.Api.Middleware; using Jellyfin.Api.Middleware;
using Jellyfin.MediaEncoding.Hls.Extensions; using Jellyfin.MediaEncoding.Hls.Extensions;
using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Configuration;
@ -27,7 +27,6 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.VisualBasic;
using Prometheus; using Prometheus;
namespace Jellyfin.Server namespace Jellyfin.Server
@ -120,26 +119,11 @@ namespace Jellyfin.Server
}) })
.ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate); .ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate);
services.AddHttpClient(NamedClient.Dlna, c =>
{
c.DefaultRequestHeaders.UserAgent.ParseAdd(
string.Format(
CultureInfo.InvariantCulture,
"{0}/{1} UPnP/1.0 {2}/{3}",
Environment.OSVersion.Platform,
Environment.OSVersion,
_serverApplicationHost.Name,
_serverApplicationHost.ApplicationVersionString));
c.DefaultRequestHeaders.Add("CPFN.UPNP.ORG", _serverApplicationHost.FriendlyName); // Required for UPnP DeviceArchitecture v2.0
c.DefaultRequestHeaders.Add("FriendlyName.DLNA.ORG", _serverApplicationHost.FriendlyName); // REVIEW: where does this come from?
})
.ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate);
services.AddHealthChecks() services.AddHealthChecks()
.AddCheck<DbContextFactoryHealthCheck<JellyfinDbContext>>(nameof(JellyfinDbContext)); .AddCheck<DbContextFactoryHealthCheck<JellyfinDbContext>>(nameof(JellyfinDbContext));
services.AddHlsPlaylistGenerator(); services.AddHlsPlaylistGenerator();
services.AddDlnaServices(_serverApplicationHost);
} }
/// <summary> /// <summary>

View File

@ -15,65 +15,13 @@ namespace MediaBrowser.Common.Extensions
/// </summary> /// </summary>
/// <param name="process">The process to wait for.</param> /// <param name="process">The process to wait for.</param>
/// <param name="timeout">The duration to wait before cancelling waiting for the task.</param> /// <param name="timeout">The duration to wait before cancelling waiting for the task.</param>
/// <returns>True if the task exited normally, false if the timeout elapsed before the process exited.</returns> /// <returns>A task that will complete when the process has exited, cancellation has been requested, or an error occurs.</returns>
/// <exception cref="InvalidOperationException">If <see cref="Process.EnableRaisingEvents"/> is not set to true for the process.</exception> /// <exception cref="OperationCanceledException">The timeout ended.</exception>
public static async Task<bool> WaitForExitAsync(this Process process, TimeSpan timeout) public static async Task WaitForExitAsync(this Process process, TimeSpan timeout)
{ {
using (var cancelTokenSource = new CancellationTokenSource(timeout)) using (var cancelTokenSource = new CancellationTokenSource(timeout))
{ {
return await WaitForExitAsync(process, cancelTokenSource.Token).ConfigureAwait(false); await process.WaitForExitAsync(cancelTokenSource.Token).ConfigureAwait(false);
}
}
/// <summary>
/// Asynchronously wait for the process to exit.
/// </summary>
/// <param name="process">The process to wait for.</param>
/// <param name="cancelToken">A <see cref="CancellationToken"/> to observe while waiting for the process to exit.</param>
/// <returns>True if the task exited normally, false if cancelled before the process exited.</returns>
public static async Task<bool> WaitForExitAsync(this Process process, CancellationToken cancelToken)
{
if (!process.EnableRaisingEvents)
{
throw new InvalidOperationException("EnableRisingEvents must be enabled to async wait for a task to exit.");
}
// Add an event handler for the process exit event
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
process.Exited += (_, _) => tcs.TrySetResult(true);
// Return immediately if the process has already exited
if (process.HasExitedSafe())
{
return true;
}
// Register with the cancellation token then await
using (var cancelRegistration = cancelToken.Register(() => tcs.TrySetResult(process.HasExitedSafe())))
{
return await tcs.Task.ConfigureAwait(false);
}
}
/// <summary>
/// Gets a value indicating whether the associated process has been terminated using
/// <see cref="Process.HasExited"/>. This is safe to call even if there is no operating system process
/// associated with the <see cref="Process"/>.
/// </summary>
/// <param name="process">The process to check the exit status for.</param>
/// <returns>
/// True if the operating system process referenced by the <see cref="Process"/> component has
/// terminated, or if there is no associated operating system process; otherwise, false.
/// </returns>
private static bool HasExitedSafe(this Process process)
{
try
{
return process.HasExited;
}
catch (InvalidOperationException)
{
return true;
} }
} }
} }

View File

@ -35,21 +35,15 @@ namespace MediaBrowser.Common
string SystemId { get; } string SystemId { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether this instance has pending kernel reload. /// Gets a value indicating whether this instance has pending changes requiring a restart.
/// </summary> /// </summary>
/// <value><c>true</c> if this instance has pending kernel reload; otherwise, <c>false</c>.</value> /// <value><c>true</c> if this instance has a pending restart; otherwise, <c>false</c>.</value>
bool HasPendingRestart { get; } bool HasPendingRestart { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether this instance is currently shutting down. /// Gets or sets a value indicating whether the application should restart.
/// </summary> /// </summary>
/// <value><c>true</c> if this instance is shutting down; otherwise, <c>false</c>.</value> bool ShouldRestart { get; set; }
bool IsShuttingDown { get; }
/// <summary>
/// Gets a value indicating whether the application should restart.
/// </summary>
bool ShouldRestart { get; }
/// <summary> /// <summary>
/// Gets the application version. /// Gets the application version.
@ -91,11 +85,6 @@ namespace MediaBrowser.Common
/// </summary> /// </summary>
void NotifyPendingRestart(); void NotifyPendingRestart();
/// <summary>
/// Restarts this instance.
/// </summary>
void Restart();
/// <summary> /// <summary>
/// Gets the exports. /// Gets the exports.
/// </summary> /// </summary>
@ -127,11 +116,6 @@ namespace MediaBrowser.Common
/// <returns>``0.</returns> /// <returns>``0.</returns>
T Resolve<T>(); T Resolve<T>();
/// <summary>
/// Shuts down.
/// </summary>
void Shutdown();
/// <summary> /// <summary>
/// Initializes this instance. /// Initializes this instance.
/// </summary> /// </summary>

View File

@ -38,10 +38,6 @@
<SymbolPackageFormat>snupkg</SymbolPackageFormat> <SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
</PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'"> <PropertyGroup Condition=" '$(Stability)'=='Unstable'">
<!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. --> <!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder> <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>

View File

@ -1,5 +1,4 @@
using System; using System;
using System.Linq;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;

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