update to current master to resolve merge conflict
This commit is contained in:
commit
27ceee8b6c
12
.config/dotnet-tools.json
Normal file
12
.config/dotnet-tools.json
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"dotnet-ef": {
|
||||
"version": "7.0.12",
|
||||
"commands": [
|
||||
"dotnet-ef"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
|
@ -20,18 +20,18 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
|
||||
with:
|
||||
dotnet-version: '7.0.x'
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0
|
||||
uses: github/codeql-action/init@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: +security-extended
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0
|
||||
uses: github/codeql-action/autobuild@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@2cb752a87e96af96708ab57187ab6372ee1973ab # v2.22.0
|
||||
uses: github/codeql-action/analyze@0116bc2df50751f9724a2e35ef1f24d22f90e4e1 # v2.22.3
|
||||
|
|
14
.github/workflows/commands.yml
vendored
14
.github/workflows/commands.yml
vendored
|
@ -17,14 +17,14 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
comment-id: ${{ github.event.comment.id }}
|
||||
reactions: '+1'
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
@ -43,7 +43,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- 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 }}
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
@ -51,14 +51,14 @@ jobs:
|
|||
reactions: eyes
|
||||
|
||||
- name: Checkout the latest code
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Notify as 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 }}
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
@ -93,7 +93,7 @@ jobs:
|
|||
exit ${retcode}
|
||||
|
||||
- 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() }}
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
@ -108,7 +108,7 @@ jobs:
|
|||
reactions: hooray
|
||||
|
||||
- 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() }}
|
||||
with:
|
||||
token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
|
|
8
.github/workflows/openapi.yml
vendored
8
.github/workflows/openapi.yml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
|||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
@ -39,7 +39,7 @@ jobs:
|
|||
permissions: read-all
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
|
@ -112,7 +112,7 @@ jobs:
|
|||
direction: last
|
||||
body-includes: openapi-diff-workflow-comment
|
||||
- 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 != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
|
@ -127,7 +127,7 @@ jobs:
|
|||
|
||||
</details>
|
||||
- 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 != '' }}
|
||||
with:
|
||||
issue-number: ${{ github.event.pull_request.number }}
|
||||
|
|
4
.github/workflows/repo-bump-version.yaml
vendored
4
.github/workflows/repo-bump-version.yaml
vendored
|
@ -33,7 +33,7 @@ jobs:
|
|||
yq-version: v4.9.8
|
||||
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
ref: ${{ env.TAG_BRANCH }}
|
||||
|
||||
|
@ -66,7 +66,7 @@ jobs:
|
|||
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
with:
|
||||
ref: ${{ env.TAG_BRANCH }}
|
||||
|
||||
|
|
2
.github/workflows/repo-stale.yaml
vendored
2
.github/workflows/repo-stale.yaml
vendored
|
@ -16,7 +16,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8
|
||||
- uses: actions/stale@1160a2240286f5da8ec72b1c0816ce2481aabf84 # v8.0.0
|
||||
with:
|
||||
repo-token: ${{ secrets.JF_BOT_TOKEN }}
|
||||
days-before-stale: 120
|
||||
|
|
|
@ -57,6 +57,7 @@
|
|||
- [hawken93](https://github.com/hawken93)
|
||||
- [HelloWorld017](https://github.com/HelloWorld017)
|
||||
- [ikomhoog](https://github.com/ikomhoog)
|
||||
- [iwalton3](https://github.com/iwalton3)
|
||||
- [jftuga](https://github.com/jftuga)
|
||||
- [jmshrv](https://github.com/jmshrv)
|
||||
- [joern-h](https://github.com/joern-h)
|
||||
|
@ -88,6 +89,7 @@
|
|||
- [neilsb](https://github.com/neilsb)
|
||||
- [nevado](https://github.com/nevado)
|
||||
- [Nickbert7](https://github.com/Nickbert7)
|
||||
- [nicknsy](https://github.com/nicknsy)
|
||||
- [nvllsvm](https://github.com/nvllsvm)
|
||||
- [nyanmisaka](https://github.com/nyanmisaka)
|
||||
- [OancaAndrei](https://github.com/OancaAndrei)
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<PackageVersion Include="Diacritics" Version="3.3.18" />
|
||||
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
|
||||
<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="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" />
|
||||
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
|
||||
|
@ -25,15 +25,15 @@
|
|||
<PackageVersion Include="libse" Version="3.6.13" />
|
||||
<PackageVersion Include="LrcParser" Version="2023.524.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.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.Data.Sqlite" Version="7.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.11" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.11" />
|
||||
<PackageVersion Include="Microsoft.Data.Sqlite" Version="7.0.12" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.12" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="7.0.12" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.12" />
|
||||
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.12" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" 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.DependencyInjection.Abstractions" 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" Version="7.0.11" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="7.0.12" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="7.0.12" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
|
||||
|
@ -86,8 +86,8 @@
|
|||
<PackageVersion Include="TMDbLib" Version="2.0.0" />
|
||||
<PackageVersion Include="UTF.Unknown" Version="2.5.1" />
|
||||
<PackageVersion Include="Xunit.Priority" Version="1.1.6" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.1" />
|
||||
<PackageVersion Include="xunit.runner.visualstudio" Version="2.5.3" />
|
||||
<PackageVersion Include="Xunit.SkippableFact" Version="1.4.13" />
|
||||
<PackageVersion Include="xunit" Version="2.5.1" />
|
||||
<PackageVersion Include="xunit" Version="2.5.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
|
|
@ -228,7 +228,7 @@ namespace Emby.Dlna
|
|||
try
|
||||
{
|
||||
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))
|
||||
.Where(i => i is not null)
|
||||
.ToList()!; // We just filtered out all the nulls
|
||||
|
|
69
Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs
Normal file
69
Emby.Dlna/Extensions/DlnaServiceCollectionExtensions.cs
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
|
@ -23,10 +23,8 @@ using MediaBrowser.Controller.Library;
|
|||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Plugins;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Controller.TV;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.Net;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Rssdp;
|
||||
using Rssdp.Infrastructure;
|
||||
|
@ -49,14 +47,13 @@ namespace Emby.Dlna.Main
|
|||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
private readonly IDeviceDiscovery _deviceDiscovery;
|
||||
private readonly ISocketFactory _socketFactory;
|
||||
private readonly ISsdpCommunicationsServer _communicationsServer;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly object _syncLock = new object();
|
||||
private readonly object _syncLock = new();
|
||||
private readonly bool _disabled;
|
||||
|
||||
private PlayToManager _manager;
|
||||
private SsdpDevicePublisher _publisher;
|
||||
private ISsdpCommunicationsServer _communicationsServer;
|
||||
|
||||
private bool _disposed;
|
||||
|
||||
|
@ -75,10 +72,8 @@ namespace Emby.Dlna.Main
|
|||
IMediaSourceManager mediaSourceManager,
|
||||
IDeviceDiscovery deviceDiscovery,
|
||||
IMediaEncoder mediaEncoder,
|
||||
ISocketFactory socketFactory,
|
||||
INetworkManager networkManager,
|
||||
IUserViewManager userViewManager,
|
||||
ITVSeriesManager tvSeriesManager)
|
||||
ISsdpCommunicationsServer communicationsServer,
|
||||
INetworkManager networkManager)
|
||||
{
|
||||
_config = config;
|
||||
_appHost = appHost;
|
||||
|
@ -93,37 +88,10 @@ namespace Emby.Dlna.Main
|
|||
_mediaSourceManager = mediaSourceManager;
|
||||
_deviceDiscovery = deviceDiscovery;
|
||||
_mediaEncoder = mediaEncoder;
|
||||
_socketFactory = socketFactory;
|
||||
_communicationsServer = communicationsServer;
|
||||
_networkManager = networkManager;
|
||||
_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);
|
||||
_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()
|
||||
{
|
||||
await ((DlnaManager)_dlnaManager).InitProfilesAsync().ConfigureAwait(false);
|
||||
|
@ -172,9 +127,7 @@ namespace Emby.Dlna.Main
|
|||
private void ReloadComponents()
|
||||
{
|
||||
var options = _config.GetDlnaConfiguration();
|
||||
Enabled = options.EnableServer;
|
||||
|
||||
StartSsdpHandler();
|
||||
StartDeviceDiscovery();
|
||||
|
||||
if (options.EnableServer)
|
||||
{
|
||||
|
@ -195,36 +148,11 @@ namespace Emby.Dlna.Main
|
|||
}
|
||||
}
|
||||
|
||||
private void StartSsdpHandler()
|
||||
private void StartDeviceDiscovery()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_communicationsServer is null)
|
||||
{
|
||||
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);
|
||||
}
|
||||
((DeviceDiscovery)_deviceDiscovery).Start(_communicationsServer);
|
||||
}
|
||||
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)
|
||||
{
|
||||
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.
|
||||
};
|
||||
|
||||
SetProperies(device, fullService);
|
||||
SetProperties(device, fullService);
|
||||
_publisher.AddDevice(device);
|
||||
|
||||
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.
|
||||
};
|
||||
|
||||
SetProperies(embeddedDevice, subDevice);
|
||||
SetProperties(embeddedDevice, subDevice);
|
||||
device.AddDevice(embeddedDevice);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string CreateUuid(string text)
|
||||
private static string CreateUuid(string text)
|
||||
{
|
||||
if (!Guid.TryParse(text, out var guid))
|
||||
{
|
||||
|
@ -354,15 +269,14 @@ namespace Emby.Dlna.Main
|
|||
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(':');
|
||||
|
||||
var deviceTypeNamespace = serviceParts[0].Replace('.', '-');
|
||||
|
||||
device.DeviceTypeNamespace = deviceTypeNamespace;
|
||||
device.DeviceTypeNamespace = serviceParts[0].Replace('.', '-');
|
||||
device.DeviceClass = serviceParts[1];
|
||||
device.DeviceType = serviceParts[2];
|
||||
}
|
||||
|
@ -443,20 +357,6 @@ namespace Emby.Dlna.Main
|
|||
|
||||
DisposeDevicePublisher();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -927,14 +927,11 @@ namespace Emby.Dlna.PlayTo
|
|||
|
||||
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))
|
||||
{
|
||||
return info.Value.Split(':');
|
||||
}
|
||||
if (info is not null && !string.IsNullOrWhiteSpace(info.Value))
|
||||
{
|
||||
return info.Value.Split(':');
|
||||
}
|
||||
|
||||
return new string[4];
|
||||
|
@ -1139,7 +1136,6 @@ namespace Emby.Dlna.PlayTo
|
|||
return new Device(deviceProperties, httpClientFactory, logger);
|
||||
}
|
||||
|
||||
#nullable enable
|
||||
private static DeviceIcon CreateIcon(XElement element)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(element);
|
||||
|
|
|
@ -55,41 +55,42 @@ namespace Emby.Dlna.PlayTo
|
|||
var client = _httpClientFactory.CreateClient(NamedClient.Dlna);
|
||||
using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using MemoryStream ms = new MemoryStream();
|
||||
await response.Content.CopyToAsync(ms, cancellationToken).ConfigureAwait(false);
|
||||
ms.Position = 0;
|
||||
try
|
||||
Stream stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
return await XDocument.LoadAsync(
|
||||
ms,
|
||||
LoadOptions.None,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (XmlException)
|
||||
{
|
||||
// try correcting the Xml response with common errors
|
||||
ms.Position = 0;
|
||||
using StreamReader sr = new StreamReader(ms);
|
||||
var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// find and replace unescaped ampersands (&)
|
||||
xmlString = EscapeAmpersandRegex().Replace(xmlString, "&");
|
||||
|
||||
try
|
||||
{
|
||||
// retry reading Xml
|
||||
using var xmlReader = new StringReader(xmlString);
|
||||
return await XDocument.LoadAsync(
|
||||
xmlReader,
|
||||
stream,
|
||||
LoadOptions.None,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (XmlException ex)
|
||||
catch (XmlException)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to parse response");
|
||||
_logger.LogDebug("Malformed response: {Content}\n", xmlString);
|
||||
// try correcting the Xml response with common errors
|
||||
stream.Position = 0;
|
||||
using StreamReader sr = new StreamReader(stream);
|
||||
var xmlString = await sr.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
return null;
|
||||
// find and replace unescaped ampersands (&)
|
||||
xmlString = EscapeAmpersandRegex().Replace(xmlString, "&");
|
||||
|
||||
try
|
||||
{
|
||||
// retry reading Xml
|
||||
using var xmlReader = new StringReader(xmlString);
|
||||
return await XDocument.LoadAsync(
|
||||
xmlReader,
|
||||
LoadOptions.None,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (XmlException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to parse response");
|
||||
_logger.LogDebug("Malformed response: {Content}\n", xmlString);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,9 +39,9 @@ namespace Emby.Dlna.PlayTo
|
|||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IMediaEncoder _mediaEncoder;
|
||||
|
||||
private readonly SemaphoreSlim _sessionLock = new SemaphoreSlim(1, 1);
|
||||
private readonly CancellationTokenSource _disposeCancellationTokenSource = new CancellationTokenSource();
|
||||
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)
|
||||
{
|
||||
|
|
|
@ -318,7 +318,7 @@ namespace Emby.Naming.Common
|
|||
new EpisodeExpression(@"[\._ -]()[Ee][Pp]_?([0-9]+)([^\\/]*)$"),
|
||||
// <!-- foo.E01., foo.e01. -->
|
||||
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[]
|
||||
{
|
||||
|
@ -328,7 +328,7 @@ namespace Emby.Naming.Common
|
|||
"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[]
|
||||
{
|
||||
|
@ -376,7 +376,7 @@ namespace Emby.Naming.Common
|
|||
IsNamed = true,
|
||||
SupportsAbsoluteEpisodeNumbers = false
|
||||
},
|
||||
new EpisodeExpression("[\\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\\/]*)$")
|
||||
new EpisodeExpression(@"[\/._ -]p(?:ar)?t[_. -]()([ivx]+|[0-9]+)([._ -][^\/]*)$")
|
||||
{
|
||||
SupportsAbsoluteEpisodeNumbers = true
|
||||
},
|
||||
|
@ -417,7 +417,7 @@ namespace Emby.Naming.Common
|
|||
},
|
||||
|
||||
// "1-12 episode title"
|
||||
new EpisodeExpression(@"([0-9]+)-([0-9]+)"),
|
||||
new EpisodeExpression("([0-9]+)-([0-9]+)"),
|
||||
|
||||
// "01 - blah.avi", "01-blah.avi"
|
||||
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>[0-9]+)",
|
||||
// Part if often ending of filename
|
||||
@"(?<!ch(?:apter) )(?<part>[0-9]+)$",
|
||||
"(?<!ch(?:apter) )(?<part>[0-9]+)$",
|
||||
// Sometimes named as 0001_005 (chapter_part)
|
||||
"(?<chapter>[0-9]+)_(?<part>[0-9]+)",
|
||||
// Some audiobooks are ripped from cd's, and will be named by disk number.
|
||||
|
|
|
@ -43,7 +43,7 @@ namespace Emby.Naming.ExternalFiles
|
|||
return null;
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path);
|
||||
var extension = Path.GetExtension(path.AsSpan());
|
||||
if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
&& !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
|
|
|
@ -26,19 +26,18 @@ namespace Emby.Naming.Video
|
|||
return false;
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path);
|
||||
var extension = Path.GetExtension(path.AsSpan());
|
||||
|
||||
if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
path = Path.GetFileNameWithoutExtension(path);
|
||||
var token = Path.GetExtension(path).TrimStart('.');
|
||||
var token = Path.GetExtension(Path.GetFileNameWithoutExtension(path.AsSpan())).TrimStart('.');
|
||||
|
||||
foreach (var rule in options.StubTypes)
|
||||
{
|
||||
if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase))
|
||||
if (token.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stubType = rule.StubType;
|
||||
return true;
|
||||
|
|
|
@ -61,7 +61,7 @@ namespace Emby.Photos
|
|||
item.SetImagePath(ImageType.Primary, item.Path);
|
||||
|
||||
// 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
|
||||
{
|
||||
|
|
|
@ -10,8 +10,6 @@ namespace Emby.Server.Implementations.AppBase
|
|||
/// </summary>
|
||||
public abstract class BaseApplicationPaths : IApplicationPaths
|
||||
{
|
||||
private string _dataPath;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseApplicationPaths"/> class.
|
||||
/// </summary>
|
||||
|
@ -33,7 +31,7 @@ namespace Emby.Server.Implementations.AppBase
|
|||
CachePath = cacheDirectoryPath;
|
||||
WebPath = webDirectoryPath;
|
||||
|
||||
_dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
|
||||
DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -55,7 +53,7 @@ namespace Emby.Server.Implementations.AppBase
|
|||
/// Gets the folder path to the data directory.
|
||||
/// </summary>
|
||||
/// <value>The data directory.</value>
|
||||
public string DataPath => _dataPath;
|
||||
public string DataPath { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string VirtualDataPath => "%AppDataPath%";
|
||||
|
|
|
@ -13,9 +13,7 @@ using System.Net;
|
|||
using System.Reflection;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna;
|
||||
using Emby.Dlna.Main;
|
||||
using Emby.Dlna.Ssdp;
|
||||
using Emby.Naming.Common;
|
||||
using Emby.Photos;
|
||||
using Emby.Server.Implementations.Channels;
|
||||
|
@ -58,7 +56,6 @@ using MediaBrowser.Controller.Chapters;
|
|||
using MediaBrowser.Controller.ClientEvent;
|
||||
using MediaBrowser.Controller.Collections;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
@ -82,7 +79,6 @@ using MediaBrowser.LocalMetadata.Savers;
|
|||
using MediaBrowser.MediaEncoding.BdInfo;
|
||||
using MediaBrowser.MediaEncoding.Subtitles;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
|
@ -101,7 +97,6 @@ using Microsoft.AspNetCore.Mvc;
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Prometheus.DotNetRuntime;
|
||||
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
|
||||
|
@ -133,7 +128,7 @@ namespace Emby.Server.Implementations
|
|||
/// <value>All concrete types.</value>
|
||||
private Type[] _allConcreteTypes;
|
||||
|
||||
private bool _disposed = false;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// 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 virtual bool CanLaunchWebBrowser => Environment.UserInteractive
|
||||
&& !_startupOptions.IsService
|
||||
&& (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="INetworkManager"/> singleton instance.
|
||||
/// </summary>
|
||||
public INetworkManager NetManager { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// <inheritdoc />
|
||||
public bool HasPendingRestart { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsShuttingDown { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ShouldRestart { get; private set; }
|
||||
public bool ShouldRestart { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the logger.
|
||||
|
@ -461,7 +446,7 @@ namespace Emby.Server.Implementations
|
|||
|
||||
ConfigurationManager.AddParts(GetExports<IConfigurationFactory>());
|
||||
|
||||
NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>());
|
||||
NetManager = new NetworkManager(ConfigurationManager, _startupConfig, LoggerFactory.CreateLogger<NetworkManager>());
|
||||
|
||||
// Initialize runtime stat collection
|
||||
if (ConfigurationManager.Configuration.EnableMetrics)
|
||||
|
@ -507,6 +492,8 @@ namespace Emby.Server.Implementations
|
|||
serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
|
||||
serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
|
||||
|
||||
serviceCollection.AddScoped<ISystemManager, SystemManager>();
|
||||
|
||||
serviceCollection.AddSingleton<TmdbClientManager>();
|
||||
|
||||
serviceCollection.AddSingleton(NetManager);
|
||||
|
@ -572,8 +559,6 @@ namespace Emby.Server.Implementations
|
|||
|
||||
serviceCollection.AddSingleton<ISessionManager, SessionManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IDlnaManager, DlnaManager>();
|
||||
|
||||
serviceCollection.AddSingleton<ICollectionManager, CollectionManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>();
|
||||
|
@ -585,8 +570,6 @@ namespace Emby.Server.Implementations
|
|||
|
||||
serviceCollection.AddSingleton<IUserViewManager, UserViewManager>();
|
||||
|
||||
serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>();
|
||||
|
||||
serviceCollection.AddSingleton<IChapterManager, ChapterManager>();
|
||||
|
||||
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>
|
||||
/// Gets the composable part assemblies.
|
||||
/// </summary>
|
||||
|
@ -923,49 +888,6 @@ namespace Emby.Server.Implementations
|
|||
|
||||
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/>
|
||||
public string GetSmartApiUrl(IPAddress remoteAddr)
|
||||
{
|
||||
|
@ -983,7 +905,7 @@ namespace Emby.Server.Implementations
|
|||
/// <inheritdoc/>
|
||||
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)
|
||||
{
|
||||
int? requestPort = request.Host.Port;
|
||||
|
@ -1018,7 +940,7 @@ namespace Emby.Server.Implementations
|
|||
public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true)
|
||||
{
|
||||
// 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;
|
||||
int? port = !allowHttps ? HttpPort : null;
|
||||
return GetLocalApiUrl(smart, scheme, port);
|
||||
|
|
|
@ -371,8 +371,11 @@ namespace Emby.Server.Implementations.Channels
|
|||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path));
|
||||
|
||||
await using FileStream createStream = File.Create(path);
|
||||
await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
|
||||
FileStream createStream = File.Create(path);
|
||||
await using (createStream.ConfigureAwait(false))
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -1156,7 +1159,7 @@ namespace Emby.Server.Implementations.Channels
|
|||
|
||||
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)
|
||||
|
|
|
@ -3540,10 +3540,7 @@ namespace Emby.Server.Implementations.Data
|
|||
.Append(paramName)
|
||||
.Append("))) OR ");
|
||||
|
||||
if (statement is not null)
|
||||
{
|
||||
statement.TryBind(paramName, query.PersonIds[i]);
|
||||
}
|
||||
statement?.TryBind(paramName, query.PersonIds[i]);
|
||||
}
|
||||
|
||||
clauseBuilder.Length -= Or.Length;
|
||||
|
@ -4382,7 +4379,7 @@ namespace Emby.Server.Implementations.Data
|
|||
|
||||
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) + ")");
|
||||
|
|
|
@ -22,6 +22,7 @@ using MediaBrowser.Controller.Lyrics;
|
|||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.Trickplay;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
|
@ -52,6 +53,7 @@ namespace Emby.Server.Implementations.Dto
|
|||
private readonly Lazy<ILiveTvManager> _livetvManagerFactory;
|
||||
|
||||
private readonly ILyricManager _lyricManager;
|
||||
private readonly ITrickplayManager _trickplayManager;
|
||||
|
||||
public DtoService(
|
||||
ILogger<DtoService> logger,
|
||||
|
@ -63,7 +65,8 @@ namespace Emby.Server.Implementations.Dto
|
|||
IApplicationHost appHost,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
Lazy<ILiveTvManager> livetvManagerFactory,
|
||||
ILyricManager lyricManager)
|
||||
ILyricManager lyricManager,
|
||||
ITrickplayManager trickplayManager)
|
||||
{
|
||||
_logger = logger;
|
||||
_libraryManager = libraryManager;
|
||||
|
@ -75,6 +78,7 @@ namespace Emby.Server.Implementations.Dto
|
|||
_mediaSourceManager = mediaSourceManager;
|
||||
_livetvManagerFactory = livetvManagerFactory;
|
||||
_lyricManager = lyricManager;
|
||||
_trickplayManager = trickplayManager;
|
||||
}
|
||||
|
||||
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
|
||||
|
@ -1059,6 +1063,11 @@ namespace Emby.Server.Implementations.Dto
|
|||
dto.Chapters = _itemRepo.GetChapters(item);
|
||||
}
|
||||
|
||||
if (options.ContainsField(ItemFields.Trickplay))
|
||||
{
|
||||
dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
if (video.ExtraType.HasValue)
|
||||
{
|
||||
dto.ExtraType = video.ExtraType.Value.ToString();
|
||||
|
|
|
@ -43,8 +43,6 @@
|
|||
<TargetFramework>net7.0</TargetFramework>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
|
||||
<NoWarn>AD0001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
|
|
|
@ -18,7 +18,7 @@ using Microsoft.Extensions.Logging;
|
|||
namespace Emby.Server.Implementations.EntryPoints
|
||||
{
|
||||
/// <summary>
|
||||
/// Class UdpServerEntryPoint.
|
||||
/// Class responsible for registering all UDP broadcast endpoints and their handlers.
|
||||
/// </summary>
|
||||
public sealed class UdpServerEntryPoint : IServerEntryPoint
|
||||
{
|
||||
|
@ -35,14 +35,13 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
private readonly IConfiguration _config;
|
||||
private readonly IConfigurationManager _configurationManager;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly bool _enableMultiSocketBinding;
|
||||
|
||||
/// <summary>
|
||||
/// The UDP server.
|
||||
/// </summary>
|
||||
private List<UdpServer> _udpServers;
|
||||
private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||
private bool _disposed = false;
|
||||
private readonly List<UdpServer> _udpServers;
|
||||
private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class.
|
||||
|
@ -65,7 +64,6 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
_configurationManager = configurationManager;
|
||||
_networkManager = networkManager;
|
||||
_udpServers = new List<UdpServer>();
|
||||
_enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -80,14 +78,16 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
|
||||
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);
|
||||
server.Start(_cancellationTokenSource.Token);
|
||||
_udpServers.Add(server);
|
||||
|
||||
// Add bind address specific broadcast sockets
|
||||
// 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)
|
||||
|
@ -102,9 +102,18 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
}
|
||||
else
|
||||
{
|
||||
var server = new UdpServer(_logger, _appHost, _config, IPAddress.Any, PortNumber);
|
||||
server.Start(_cancellationTokenSource.Token);
|
||||
_udpServers.Add(server);
|
||||
// Add bind address specific broadcast listeners
|
||||
// IPv6 is currently unsupported
|
||||
var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork);
|
||||
foreach (var intf in validInterfaces)
|
||||
{
|
||||
var intfAddress = intf.Address;
|
||||
_logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", intfAddress, PortNumber);
|
||||
|
||||
var server = new UdpServer(_logger, _appHost, _config, intfAddress, PortNumber);
|
||||
server.Start(_cancellationTokenSource.Token);
|
||||
_udpServers.Add(server);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (SocketException ex)
|
||||
|
@ -119,7 +128,7 @@ namespace Emby.Server.Implementations.EntryPoints
|
|||
{
|
||||
if (_disposed)
|
||||
{
|
||||
throw new ObjectDisposedException(this.GetType().Name);
|
||||
throw new ObjectDisposedException(GetType().Name);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ using MediaBrowser.Controller.Net;
|
|||
using MediaBrowser.Controller.Net.WebSocketMessages;
|
||||
using MediaBrowser.Controller.Net.WebSocketMessages.Outbound;
|
||||
using MediaBrowser.Model.Session;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.HttpServer
|
||||
|
|
|
@ -210,7 +210,6 @@ namespace Emby.Server.Implementations.IO
|
|||
|
||||
DisposeTimer();
|
||||
_disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.IO
|
|||
}
|
||||
|
||||
// unc path
|
||||
if (filePath.StartsWith("\\\\", StringComparison.Ordinal))
|
||||
if (filePath.StartsWith(@"\\", StringComparison.Ordinal))
|
||||
{
|
||||
return filePath;
|
||||
}
|
||||
|
@ -103,15 +103,17 @@ namespace Emby.Server.Implementations.IO
|
|||
return filePath;
|
||||
}
|
||||
|
||||
var filePathSpan = filePath.AsSpan();
|
||||
|
||||
// relative path
|
||||
if (firstChar == '\\')
|
||||
{
|
||||
filePath = filePath.Substring(1);
|
||||
filePathSpan = filePathSpan.Slice(1);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Path.GetFullPath(Path.Combine(folderPath, filePath));
|
||||
return Path.GetFullPath(Path.Join(folderPath, filePathSpan));
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
|
|
|
@ -46,7 +46,6 @@ using MediaBrowser.Model.IO;
|
|||
using MediaBrowser.Model.Library;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||
using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
|
||||
|
@ -839,19 +838,12 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
var path = Person.GetPath(name);
|
||||
var id = GetItemByNameId<Person>(path);
|
||||
if (GetItemById(id) is not Person item)
|
||||
if (GetItemById(id) is Person item)
|
||||
{
|
||||
item = new Person
|
||||
{
|
||||
Name = name,
|
||||
Id = id,
|
||||
DateCreated = DateTime.UtcNow,
|
||||
DateModified = DateTime.UtcNow,
|
||||
Path = path
|
||||
};
|
||||
return item;
|
||||
}
|
||||
|
||||
return item;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -1162,7 +1154,7 @@ namespace Emby.Server.Implementations.Library
|
|||
Name = Path.GetFileName(dir),
|
||||
|
||||
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 =>
|
||||
{
|
||||
try
|
||||
|
@ -2858,7 +2850,7 @@ namespace Emby.Server.Implementations.Library
|
|||
{
|
||||
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);
|
||||
|
@ -2900,9 +2892,18 @@ namespace Emby.Server.Implementations.Library
|
|||
var saveEntity = false;
|
||||
var personEntity = GetPerson(person.Name);
|
||||
|
||||
// if PresentationUniqueKey is empty it's likely a new item.
|
||||
if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey))
|
||||
if (personEntity is null)
|
||||
{
|
||||
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();
|
||||
saveEntity = true;
|
||||
}
|
||||
|
@ -3135,7 +3136,7 @@ namespace Emby.Server.Implementations.Library
|
|||
}
|
||||
|
||||
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));
|
||||
|
||||
if (!string.IsNullOrEmpty(shortcut))
|
||||
|
|
|
@ -48,15 +48,20 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
if (!string.IsNullOrEmpty(cacheKey))
|
||||
{
|
||||
FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
|
||||
try
|
||||
{
|
||||
await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
|
||||
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// _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)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
|
||||
await using FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
|
||||
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
FileStream createStream = AsyncFile.OpenWrite(cacheFilePath);
|
||||
await using (createStream.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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -625,17 +625,19 @@ namespace Emby.Server.Implementations.Library
|
|||
|
||||
if (!string.IsNullOrEmpty(cacheKey))
|
||||
{
|
||||
FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
|
||||
try
|
||||
{
|
||||
await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath);
|
||||
mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// _logger.LogDebug("Found cached media info");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception.");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await jsonStream.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaInfo is null)
|
||||
|
@ -664,8 +666,11 @@ namespace Emby.Server.Implementations.Library
|
|||
if (cacheFilePath is not null)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath));
|
||||
await using FileStream createStream = File.Create(cacheFilePath);
|
||||
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
FileStream createStream = File.Create(cacheFilePath);
|
||||
await using (createStream.ConfigureAwait(false))
|
||||
{
|
||||
await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// _logger.LogDebug("Saved media info to {0}", cacheFilePath);
|
||||
}
|
||||
|
|
|
@ -94,9 +94,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||
|
||||
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
|
||||
return null;
|
||||
|
@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
|||
|
||||
if (item is not null)
|
||||
{
|
||||
item.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase);
|
||||
item.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
item.IsInMixedFolder = true;
|
||||
}
|
||||
|
|
|
@ -263,7 +263,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
|||
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>
|
||||
|
|
|
@ -32,9 +32,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
|||
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
|
||||
return new Book
|
||||
|
@ -51,12 +51,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
|||
{
|
||||
var bookFiles = args.FileSystemChildren.Where(f =>
|
||||
{
|
||||
var fileExtension = Path.GetExtension(f.FullName)
|
||||
?? string.Empty;
|
||||
var fileExtension = Path.GetExtension(f.FullName.AsSpan());
|
||||
|
||||
return _validExtensions.Contains(
|
||||
fileExtension,
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}).ToList();
|
||||
|
||||
// Don't return a Book if there is more (or less) than one document in the directory
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Emby.Naming.Common;
|
||||
|
|
|
@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
|
|||
var resolver = new Naming.TV.EpisodeResolver(namingOptions);
|
||||
|
||||
var folderName = System.IO.Path.GetFileName(path);
|
||||
var testPath = "\\\\test\\" + folderName;
|
||||
var testPath = @"\\test\" + folderName;
|
||||
|
||||
var episodeInfo = resolver.Resolve(testPath, true);
|
||||
|
||||
|
|
|
@ -1851,7 +1851,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
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
|
||||
{
|
||||
|
@ -1860,7 +1861,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
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.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false);
|
||||
|
@ -1914,7 +1916,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
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
|
||||
{
|
||||
|
@ -1927,7 +1930,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
|
||||
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);
|
||||
|
||||
|
@ -1965,7 +1969,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV
|
|||
}
|
||||
else
|
||||
{
|
||||
await writer.WriteStartElementAsync(null, "movie", null);
|
||||
await writer.WriteStartElementAsync(null, "movie", null).ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(item.Name))
|
||||
{
|
||||
|
|
|
@ -106,8 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||
options.Content = JsonContent.Create(requestList, options: _jsonOptions);
|
||||
options.Headers.TryAddWithoutValidation("token", token);
|
||||
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 JsonSerializer.DeserializeAsync<IReadOnlyList<DayDto>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
var dailySchedules = await response.Content.ReadFromJsonAsync<IReadOnlyList<DayDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (dailySchedules is null)
|
||||
{
|
||||
return Array.Empty<ProgramInfo>();
|
||||
|
@ -122,8 +121,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||
programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions);
|
||||
|
||||
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 JsonSerializer.DeserializeAsync<IReadOnlyList<ProgramDetailsDto>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
var programDetails = await innerResponse.Content.ReadFromJsonAsync<IReadOnlyList<ProgramDetailsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (programDetails is null)
|
||||
{
|
||||
return Array.Empty<ProgramInfo>();
|
||||
|
@ -482,8 +480,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||
try
|
||||
{
|
||||
using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false);
|
||||
await using var response = await innerResponse2.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
return await JsonSerializer.DeserializeAsync<IReadOnlyList<ShowImagesDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
return await innerResponse2.Content.ReadFromJsonAsync<IReadOnlyList<ShowImagesDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
@ -510,10 +507,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||
try
|
||||
{
|
||||
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 JsonSerializer.DeserializeAsync<IReadOnlyList<HeadendsDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var root = await httpResponse.Content.ReadFromJsonAsync<IReadOnlyList<HeadendsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (root is not null)
|
||||
{
|
||||
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);
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
var root = await JsonSerializer.DeserializeAsync<TokenDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
var root = await response.Content.ReadFromJsonAsync<TokenDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (string.Equals(root?.Message, "OK", StringComparison.Ordinal))
|
||||
{
|
||||
_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);
|
||||
httpResponse.EnsureSuccessStatusCode();
|
||||
await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
using var response = httpResponse.Content;
|
||||
var root = await JsonSerializer.DeserializeAsync<LineupsDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var root = await httpResponse.Content.ReadFromJsonAsync<LineupsDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
return root?.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase)) ?? false;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
|
@ -748,8 +738,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings
|
|||
options.Headers.TryAddWithoutValidation("token", token);
|
||||
|
||||
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 JsonSerializer.DeserializeAsync<ChannelDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
var root = await httpResponse.Content.ReadFromJsonAsync<ChannelDto>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
if (root is null)
|
||||
{
|
||||
return new List<ChannelInfo>();
|
||||
|
|
|
@ -17,7 +17,6 @@ using MediaBrowser.Controller.LiveTv;
|
|||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
|
|
|
@ -9,6 +9,7 @@ using System.IO;
|
|||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -27,7 +28,6 @@ using MediaBrowser.Model.IO;
|
|||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using MediaBrowser.Model.Net;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
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);
|
||||
|
||||
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 JsonSerializer.DeserializeAsync<List<Channels>>(stream, _jsonOptions, cancellationToken)
|
||||
.ConfigureAwait(false) ?? new List<Channels>();
|
||||
|
||||
var lineup = await response.Content.ReadFromJsonAsync<IEnumerable<Channels>>(_jsonOptions, cancellationToken).ConfigureAwait(false) ?? Enumerable.Empty<Channels>();
|
||||
if (info.ImportFavoritesOnly)
|
||||
{
|
||||
lineup = lineup.Where(i => i.Favorite).ToList();
|
||||
lineup = lineup.Where(i => i.Favorite);
|
||||
}
|
||||
|
||||
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)
|
||||
.ConfigureAwait(false);
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, _jsonOptions, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
var discoverResponse = await response.Content.ReadFromJsonAsync<DiscoverResponse>(_jsonOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!string.IsNullOrEmpty(cacheKey))
|
||||
{
|
||||
|
@ -175,34 +170,37 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
||||
.GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken)
|
||||
.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>();
|
||||
await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false))
|
||||
var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
string stripedLine = StripXML(line);
|
||||
if (stripedLine.Contains("Channel", StringComparison.Ordinal))
|
||||
using var sr = new StreamReader(stream, System.Text.Encoding.UTF8);
|
||||
await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false))
|
||||
{
|
||||
LiveTvTunerStatus status;
|
||||
var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
|
||||
var name = stripedLine.Substring(0, index - 1);
|
||||
var currentChannel = stripedLine.Substring(index + 7);
|
||||
if (string.Equals(currentChannel, "none", StringComparison.Ordinal))
|
||||
string stripedLine = StripXML(line);
|
||||
if (stripedLine.Contains("Channel", StringComparison.Ordinal))
|
||||
{
|
||||
status = LiveTvTunerStatus.LiveTv;
|
||||
}
|
||||
else
|
||||
{
|
||||
status = LiveTvTunerStatus.Available;
|
||||
}
|
||||
LiveTvTunerStatus status;
|
||||
var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase);
|
||||
var name = stripedLine.Substring(0, index - 1);
|
||||
var currentChannel = stripedLine.Substring(index + 7);
|
||||
if (string.Equals(currentChannel, "none", StringComparison.Ordinal))
|
||||
{
|
||||
status = LiveTvTunerStatus.LiveTv;
|
||||
}
|
||||
else
|
||||
{
|
||||
status = LiveTvTunerStatus.Available;
|
||||
}
|
||||
|
||||
tuners.Add(new LiveTvTunerInfo
|
||||
{
|
||||
Name = name,
|
||||
SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
|
||||
ProgramName = currentChannel,
|
||||
Status = status
|
||||
});
|
||||
tuners.Add(new LiveTvTunerInfo
|
||||
{
|
||||
Name = name,
|
||||
SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber,
|
||||
ProgramName = currentChannel,
|
||||
Status = status
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,8 +44,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
|
|||
StopStreaming(socket).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public async Task<bool> CheckTunerAvailability(IPAddress remoteIP, int tuner, CancellationToken cancellationToken)
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
|
@ -22,7 +21,6 @@ using MediaBrowser.Model.Entities;
|
|||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
|
||||
|
|
|
@ -112,6 +112,8 @@
|
|||
"TaskCleanLogsDescription": "Deletes log files that are more than {0} days old.",
|
||||
"TaskRefreshPeople": "Refresh People",
|
||||
"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",
|
||||
"TaskUpdatePluginsDescription": "Downloads and installs updates for plugins that are configured to update automatically.",
|
||||
"TaskCleanTranscode": "Clean Transcode Directory",
|
||||
|
|
18
Emby.Server.Implementations/Localization/Core/fo.json
Normal file
18
Emby.Server.Implementations/Localization/Core/fo.json
Normal 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"
|
||||
}
|
|
@ -13,8 +13,8 @@
|
|||
"HeaderFavoriteArtists": "Uppáhalds Listamenn",
|
||||
"HeaderFavoriteAlbums": "Uppáhalds Plötur",
|
||||
"HeaderContinueWatching": "Halda áfram að horfa",
|
||||
"HeaderAlbumArtists": "Höfundur plötu",
|
||||
"Genres": "Tegundir",
|
||||
"HeaderAlbumArtists": "Listamaður á umslagi",
|
||||
"Genres": "Stefnur",
|
||||
"Folders": "Möppur",
|
||||
"Favorites": "Uppáhalds",
|
||||
"FailedLoginAttemptWithUserName": "{0} reyndi að auðkenna sig",
|
||||
|
@ -22,32 +22,32 @@
|
|||
"DeviceOfflineWithName": "{0} hefur aftengst",
|
||||
"Collections": "Söfn",
|
||||
"ChapterNameValue": "Kafli {0}",
|
||||
"Channels": "Stöðvar",
|
||||
"CameraImageUploadedFrom": "Ný ljósmynd frá myndavél hefur verið hlaðið upp frá {0}",
|
||||
"Channels": "Rásir",
|
||||
"CameraImageUploadedFrom": "{0} hefur hlaðið upp nýrri ljósmynd úr myndavél sinni",
|
||||
"Books": "Bækur",
|
||||
"AuthenticationSucceededWithUserName": "{0} auðkenning tókst",
|
||||
"Artists": "Listamaður",
|
||||
"AuthenticationSucceededWithUserName": "Auðkenning fyrir {0} tókst",
|
||||
"Artists": "Listamenn",
|
||||
"Application": "Forrit",
|
||||
"AppDeviceValues": "Snjallforrit: {0}, Tæki: {1}",
|
||||
"Albums": "Plötur",
|
||||
"Plugin": "Viðbót",
|
||||
"Photos": "Myndir",
|
||||
"NotificationOptionVideoPlaybackStopped": "Myndbandafspilun stöðvuð",
|
||||
"NotificationOptionVideoPlayback": "Myndbandafspilun hafin",
|
||||
"Plugin": "Viðbótarvirkni",
|
||||
"Photos": "Ljósmyndir",
|
||||
"NotificationOptionVideoPlaybackStopped": "Myndbandsafspilun stöðvuð",
|
||||
"NotificationOptionVideoPlayback": "Myndbandsafspilun hafin",
|
||||
"NotificationOptionUserLockedOut": "Notandi læstur úti",
|
||||
"NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynileg",
|
||||
"NotificationOptionPluginUpdateInstalled": "Viðbótar uppfærsla uppsett",
|
||||
"NotificationOptionPluginUninstalled": "Viðbót fjarlægð",
|
||||
"NotificationOptionPluginInstalled": "Viðbót sett upp",
|
||||
"NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynleg",
|
||||
"NotificationOptionPluginUpdateInstalled": "Uppfærslu á viðbótarvirkni lokið",
|
||||
"NotificationOptionPluginUninstalled": "Viðbótarvirkni fjarlægð",
|
||||
"NotificationOptionPluginInstalled": "Viðbótarvirkni sett upp",
|
||||
"NotificationOptionPluginError": "Bilun í viðbót",
|
||||
"NotificationOptionInstallationFailed": "Uppsetning tókst ekki",
|
||||
"NotificationOptionCameraImageUploaded": "Myndavélarmynd hlaðið upp",
|
||||
"NotificationOptionCameraImageUploaded": "Ljósmynd hlaðið upp",
|
||||
"NotificationOptionAudioPlaybackStopped": "Hljóðafspilun stöðvuð",
|
||||
"NotificationOptionAudioPlayback": "Hljóðafspilun hafin",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Uppfærsla uppsett",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Uppfærsla í boði",
|
||||
"NameSeasonUnknown": "Sería óþekkt",
|
||||
"NameSeasonNumber": "Sería {0}",
|
||||
"NameSeasonUnknown": "Þáttaröð óþekkt",
|
||||
"NameSeasonNumber": "Þáttaröð {0}",
|
||||
"MixedContent": "Blandað efni",
|
||||
"MessageServerConfigurationUpdated": "Stillingar þjóns hafa verið uppfærðar",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin þjónn hefur verið uppfærður í {0}",
|
||||
|
@ -57,24 +57,24 @@
|
|||
"User": "Notandi",
|
||||
"System": "Kerfi",
|
||||
"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",
|
||||
"MusicVideos": "Tónlistarmyndbönd",
|
||||
"Music": "Tónlist",
|
||||
"Movies": "Kvikmyndir",
|
||||
"UserDeletedWithName": "Notanda {0} hefur verið eytt",
|
||||
"UserCreatedWithName": "Notandi {0} hefur verið stofnaður",
|
||||
"TvShows": "Þættir",
|
||||
"TvShows": "Sjónvarpsþættir",
|
||||
"Sync": "Samstilla",
|
||||
"Songs": "Lög",
|
||||
"ServerNameNeedsToBeRestarted": "{0} þarf að endurræsa",
|
||||
"ServerNameNeedsToBeRestarted": "{0} þarf að vera endurræstur",
|
||||
"ScheduledTaskStartedWithName": "{0} hafin",
|
||||
"ScheduledTaskFailedWithName": "{0} mistókst",
|
||||
"PluginUpdatedWithName": "{0} var uppfært",
|
||||
"PluginUninstalledWithName": "{0} var fjarlægt",
|
||||
"PluginInstalledWithName": "{0} var sett upp",
|
||||
"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}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt",
|
||||
"UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}",
|
||||
|
@ -83,14 +83,14 @@
|
|||
"UserPasswordChangedWithName": "Lykilorði fyrir notandann {0} hefur verið breytt",
|
||||
"UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}",
|
||||
"UserOfflineFromDevice": "{0} hefur aftengst frá {1}",
|
||||
"UserLockedOutWithName": "Notanda {0} hefur verið heflaður aðgangur",
|
||||
"UserDownloadingItemWithValues": "{0} Hleður niður {1}",
|
||||
"UserLockedOutWithName": "Notandi {0} hefur verið læstur úti",
|
||||
"UserDownloadingItemWithValues": "{0} hleður niður {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",
|
||||
"ValueSpecialEpisodeName": "Sérstakt - {0}",
|
||||
"Shows": "Sýningar",
|
||||
"Playlists": "Spilunarlisti",
|
||||
"ValueSpecialEpisodeName": "Sérstaktur - {0}",
|
||||
"Shows": "Þættir",
|
||||
"Playlists": "Efnisskrár",
|
||||
"TaskRefreshChannelsDescription": "Endurhlaða upplýsingum netrása.",
|
||||
"TaskRefreshChannels": "Endurhlaða Rásir",
|
||||
"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.",
|
||||
"TaskCleanLogs": "Hreinsa færslu skrá",
|
||||
"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"
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"ServerNameNeedsToBeRestarted": "{0} ir vajadzīgs restarts",
|
||||
"NotificationOptionTaskFailed": "Plānota uzdevuma kļūme",
|
||||
"HeaderRecordingGroups": "Ierakstu Grupas",
|
||||
"HeaderRecordingGroups": "Ierakstu grupas",
|
||||
"UserPolicyUpdatedWithName": "Lietotāju politika atjaunota priekš {0}",
|
||||
"SubtitleDownloadFailureFromForItem": "Subtitru lejupielāde no {0} priekš {1} neizdevās",
|
||||
"NotificationOptionVideoPlaybackStopped": "Video atskaņošana apturēta",
|
||||
|
@ -14,7 +14,7 @@
|
|||
"Photos": "Attēli",
|
||||
"NotificationOptionUserLockedOut": "Lietotājs bloķēts",
|
||||
"LabelRunningTimeValue": "Garums: {0}",
|
||||
"Inherit": "Mantot",
|
||||
"Inherit": "Pārmantot",
|
||||
"AppDeviceValues": "Lietotne: {0}, Ierīce: {1}",
|
||||
"VersionNumber": "Versija {0}",
|
||||
"ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots jūsu multvides bibliotēkai",
|
||||
|
@ -28,7 +28,7 @@
|
|||
"UserDeletedWithName": "Lietotājs {0} ir izdzēsts",
|
||||
"UserCreatedWithName": "Lietotājs {0} ir ticis izveidots",
|
||||
"User": "Lietotājs",
|
||||
"TvShows": "TV Raidījumi",
|
||||
"TvShows": "TV raidījumi",
|
||||
"Sync": "Sinhronizācija",
|
||||
"System": "Sistēma",
|
||||
"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",
|
||||
"PluginInstalledWithName": "{0} tika uzstādīts",
|
||||
"Plugin": "Paplašinājums",
|
||||
"Playlists": "Atskaņošanas Saraksti",
|
||||
"Playlists": "Atskaņošanas saraksti",
|
||||
"MixedContent": "Jaukts saturs",
|
||||
"HomeVideos": "Mājas Video",
|
||||
"HomeVideos": "Mājas video",
|
||||
"HeaderNextUp": "Nākamais",
|
||||
"ChapterNameValue": "Nodaļa {0}",
|
||||
"ChapterNameValue": "{0}. nodaļa",
|
||||
"Application": "Lietotne",
|
||||
"NotificationOptionServerRestartRequired": "Vajadzīgs servera restarts",
|
||||
"NotificationOptionPluginUpdateInstalled": "Paplašinājuma atjauninājums uzstādīts",
|
||||
|
@ -56,14 +56,14 @@
|
|||
"NotificationOptionApplicationUpdateInstalled": "Lietotnes atjauninājums uzstādīts",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Lietotnes atjauninājums pieejams",
|
||||
"NewVersionIsAvailable": "Lejupielādei ir pieejama jauna Jellyfin Server versija.",
|
||||
"NameSeasonUnknown": "Nezināma Sezona",
|
||||
"NameSeasonNumber": "Sezona {0}",
|
||||
"NameSeasonUnknown": "Nezināma sezona",
|
||||
"NameSeasonNumber": "{0}. sezona",
|
||||
"NameInstallFailed": "{0} instalācija neizdevās",
|
||||
"MusicVideos": "Mūzikas video",
|
||||
"Music": "Mūzika",
|
||||
"Movies": "Filmas",
|
||||
"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}",
|
||||
"MessageApplicationUpdated": "Jellyfin Server ir ticis atjaunots",
|
||||
"Latest": "Jaunākais",
|
||||
|
@ -71,57 +71,57 @@
|
|||
"ItemRemovedWithName": "{0} tika noņemts no bibliotēkas",
|
||||
"ItemAddedWithName": "{0} tika pievienots bibliotēkai",
|
||||
"HeaderLiveTV": "Tiešraides TV",
|
||||
"HeaderContinueWatching": "Turpināt Skatīšanos",
|
||||
"HeaderAlbumArtists": "Albumu Izpildītāji",
|
||||
"HeaderContinueWatching": "Turpināt skatīšanos",
|
||||
"HeaderAlbumArtists": "Albumu izpildītāji",
|
||||
"Genres": "Žanri",
|
||||
"Folders": "Mapes",
|
||||
"Favorites": "Favorīti",
|
||||
"FailedLoginAttemptWithUserName": "Neizdevies pieslēgšanās mēģinājums no {0}",
|
||||
"DeviceOnlineWithName": "{0} ir pievienojies",
|
||||
"DeviceOfflineWithName": "{0} ir atvienojies",
|
||||
"Favorites": "Izlase",
|
||||
"FailedLoginAttemptWithUserName": "Neizdevies ieiešanas mēģinājums no {0}",
|
||||
"DeviceOnlineWithName": "Savienojums ar {0} ir izveidots",
|
||||
"DeviceOfflineWithName": "Savienojums ar {0} ir pārtraukts",
|
||||
"Collections": "Kolekcijas",
|
||||
"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",
|
||||
"Artists": "Izpildītāji",
|
||||
"Albums": "Albumi",
|
||||
"ProviderValue": "Provider: {0}",
|
||||
"HeaderFavoriteSongs": "Dziesmu Favorīti",
|
||||
"HeaderFavoriteShows": "Raidījumu Favorīti",
|
||||
"HeaderFavoriteEpisodes": "Episožu Favorīti",
|
||||
"HeaderFavoriteArtists": "Izpildītāju Favorīti",
|
||||
"HeaderFavoriteAlbums": "Albumu Favorīti",
|
||||
"TaskCleanCacheDescription": "Nodzēš keša datnes, kas vairs nav sistēmai vajadzīgas.",
|
||||
"TaskRefreshChapterImages": "Izvilkt Nodaļu Attēlus",
|
||||
"HeaderFavoriteSongs": "Dziesmu izlase",
|
||||
"HeaderFavoriteShows": "Raidījumu izlase",
|
||||
"HeaderFavoriteEpisodes": "Sēriju izlase",
|
||||
"HeaderFavoriteArtists": "Izpildītāju izlase",
|
||||
"HeaderFavoriteAlbums": "Albumu izlase",
|
||||
"TaskCleanCacheDescription": "Nodzēš kešatmiņas datnes, kas vairs nav sistēmai vajadzīgas.",
|
||||
"TaskRefreshChapterImages": "Izvilkt nodaļu attēlus",
|
||||
"TasksApplicationCategory": "Lietotne",
|
||||
"TasksLibraryCategory": "Bibliotēka",
|
||||
"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.",
|
||||
"TaskRefreshChannels": "Atjaunot Kanālus",
|
||||
"TaskCleanTranscodeDescription": "Izdzēš trans-kodēšanas datnes, kas ir vecākas par vienu dienu.",
|
||||
"TaskCleanTranscode": "Iztīrīt Trans-kodēšanas Mapi",
|
||||
"TaskRefreshChannels": "Atjaunot kanālus",
|
||||
"TaskCleanTranscodeDescription": "Izdzēš transkodēšanas datnes, kas ir senākas par vienu dienu.",
|
||||
"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.",
|
||||
"TaskUpdatePlugins": "Atjaunot Paplašinājumus",
|
||||
"TaskUpdatePlugins": "Atjaunot paplašinājumus",
|
||||
"TaskRefreshPeopleDescription": "Atjauno metadatus aktieriem un direktoriem jūsu multivides bibliotēkā.",
|
||||
"TaskRefreshPeople": "Atjaunot Cilvēkus",
|
||||
"TaskCleanLogsDescription": "Nodzēš log datnes, kas ir vairāk par {0} dienām vecas.",
|
||||
"TaskCleanLogs": "Iztīrīt Logdatņu Mapi",
|
||||
"TaskRefreshPeople": "Atjaunot cilvēkus",
|
||||
"TaskCleanLogsDescription": "Nodzēš logdatnes, kas ir senākas par {0} dienām.",
|
||||
"TaskCleanLogs": "Iztīrīt logdatņu mapi",
|
||||
"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.",
|
||||
"TaskCleanCache": "Iztīrīt Kešošanas Mapi",
|
||||
"TasksChannelsCategory": "Interneta Kanāli",
|
||||
"TaskCleanCache": "Iztīrīt kešatmiņas mapi",
|
||||
"TasksChannelsCategory": "Interneta kanāli",
|
||||
"TasksMaintenanceCategory": "Apkope",
|
||||
"Forced": "Piespiests",
|
||||
"Forced": "Piespiedu",
|
||||
"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",
|
||||
"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",
|
||||
"External": "Ārējais",
|
||||
"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."
|
||||
}
|
||||
|
|
|
@ -121,5 +121,7 @@
|
|||
"TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്കാൻ ചെയ്തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്തതിന് ശേഷം ഈ ടാസ്ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
|
||||
"TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക",
|
||||
"HearingImpaired": "കേൾവി തകരാറുകൾ",
|
||||
"External": "പുറമേയുള്ള"
|
||||
"External": "പുറമേയുള്ള",
|
||||
"TaskKeyframeExtractorDescription": "കൂടുതൽ കൃത്യമായ HLS പ്ലേലിസ്റ്റുകൾ സൃഷ്ടിക്കുന്നതിന് വീഡിയോ ഫയലുകളിൽ നിന്ന് കീഫ്രെയിമുകൾ എക്സ്ട്രാക്റ്റ് ചെയ്യുന്നു. ഈ പ്രവർത്തനം പൂർത്തിയാവാൻ കുറച്ചധികം സമയം എടുത്തേക്കാം.",
|
||||
"TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ"
|
||||
}
|
||||
|
|
1
Emby.Server.Implementations/Localization/Core/si.json
Normal file
1
Emby.Server.Implementations/Localization/Core/si.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -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.",
|
||||
"TaskKeyframeExtractor": "Extraktor kľúčových snímkov",
|
||||
"External": "Externé",
|
||||
"HearingImpaired": "Sluchovo Postihnutý"
|
||||
"HearingImpaired": "Sluchovo postihnutí"
|
||||
}
|
||||
|
|
|
@ -71,25 +71,28 @@ namespace Emby.Server.Implementations.Localization
|
|||
string countryCode = resource.Substring(RatingsPath.Length, 2);
|
||||
var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
await using 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 foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
|
||||
var stream = _assembly.GetManifestResourceStream(resource);
|
||||
await using (stream!.ConfigureAwait(false)) // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
using var reader = new StreamReader(stream!);
|
||||
await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string[] parts = line.Split(',');
|
||||
if (parts.Length == 2
|
||||
&& int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
var name = parts[0];
|
||||
dict.Add(name, new ParentalRating(name, value));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
|
||||
string[] parts = line.Split(',');
|
||||
if (parts.Length == 2
|
||||
&& int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
var name = parts[0];
|
||||
dict.Add(name, new ParentalRating(name, value));
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -222,7 +222,7 @@ namespace Emby.Server.Implementations.MediaEncoder
|
|||
{
|
||||
var deadImages = images
|
||||
.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();
|
||||
|
||||
foreach (var image in deadImages)
|
||||
|
|
|
@ -1,12 +1,15 @@
|
|||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Net.Sockets;
|
||||
using MediaBrowser.Model.Net;
|
||||
|
||||
namespace Emby.Server.Implementations.Net
|
||||
{
|
||||
/// <summary>
|
||||
/// Factory class to create different kinds of sockets.
|
||||
/// </summary>
|
||||
public class SocketFactory : ISocketFactory
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
@ -29,7 +32,7 @@ namespace Emby.Server.Implementations.Net
|
|||
}
|
||||
catch
|
||||
{
|
||||
socket?.Dispose();
|
||||
socket.Dispose();
|
||||
|
||||
throw;
|
||||
}
|
||||
|
@ -38,7 +41,8 @@ namespace Emby.Server.Implementations.Net
|
|||
/// <inheritdoc />
|
||||
public Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(bindInterface.Address);
|
||||
var interfaceAddress = bindInterface.Address;
|
||||
ArgumentNullException.ThrowIfNull(interfaceAddress);
|
||||
|
||||
if (localPort < 0)
|
||||
{
|
||||
|
@ -49,13 +53,13 @@ namespace Emby.Server.Implementations.Net
|
|||
try
|
||||
{
|
||||
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
socket.Bind(new IPEndPoint(bindInterface.Address, localPort));
|
||||
socket.Bind(new IPEndPoint(interfaceAddress, localPort));
|
||||
|
||||
return socket;
|
||||
}
|
||||
catch
|
||||
{
|
||||
socket?.Dispose();
|
||||
socket.Dispose();
|
||||
|
||||
throw;
|
||||
}
|
||||
|
@ -82,22 +86,31 @@ namespace Emby.Server.Implementations.Net
|
|||
|
||||
try
|
||||
{
|
||||
var interfaceIndex = bindInterface.Index;
|
||||
var interfaceIndexSwapped = (int)IPAddress.HostToNetworkOrder(interfaceIndex);
|
||||
|
||||
socket.MulticastLoopback = false;
|
||||
socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
|
||||
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true);
|
||||
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive);
|
||||
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastInterface, interfaceIndexSwapped);
|
||||
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex));
|
||||
socket.Bind(new IPEndPoint(multicastAddress, localPort));
|
||||
|
||||
if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS())
|
||||
{
|
||||
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress));
|
||||
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;
|
||||
}
|
||||
catch
|
||||
{
|
||||
socket?.Dispose();
|
||||
socket.Dispose();
|
||||
|
||||
throw;
|
||||
}
|
||||
|
|
|
@ -327,9 +327,9 @@ namespace Emby.Server.Implementations.Playlists
|
|||
// 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
|
||||
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();
|
||||
foreach (var child in item.GetLinkedChildren())
|
||||
|
@ -362,8 +362,7 @@ namespace Emby.Server.Implementations.Playlists
|
|||
string text = new WplContent().ToText(playlist);
|
||||
File.WriteAllText(playlistPath, text);
|
||||
}
|
||||
|
||||
if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
|
||||
else if (extension.Equals(".zpl", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var playlist = new ZplPlaylist();
|
||||
foreach (var child in item.GetLinkedChildren())
|
||||
|
@ -396,8 +395,7 @@ namespace Emby.Server.Implementations.Playlists
|
|||
string text = new ZplContent().ToText(playlist);
|
||||
File.WriteAllText(playlistPath, text);
|
||||
}
|
||||
|
||||
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
|
||||
else if (extension.Equals(".m3u", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var playlist = new M3uPlaylist
|
||||
{
|
||||
|
@ -428,8 +426,7 @@ namespace Emby.Server.Implementations.Playlists
|
|||
string text = new M3uContent().ToText(playlist);
|
||||
File.WriteAllText(playlistPath, text);
|
||||
}
|
||||
|
||||
if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
|
||||
else if (extension.Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var playlist = new M3uPlaylist();
|
||||
playlist.IsExtended = true;
|
||||
|
@ -458,8 +455,7 @@ namespace Emby.Server.Implementations.Playlists
|
|||
string text = new M3uContent().ToText(playlist);
|
||||
File.WriteAllText(playlistPath, text);
|
||||
}
|
||||
|
||||
if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
|
||||
else if (extension.Equals(".pls", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var playlist = new PlsPlaylist();
|
||||
foreach (var child in item.GetLinkedChildren())
|
||||
|
|
|
@ -386,11 +386,11 @@ namespace Emby.Server.Implementations.Plugins
|
|||
var url = new Uri(packageInfo.ImageUrl);
|
||||
imagePath = Path.Join(path, url.Segments[^1]);
|
||||
|
||||
await using var fileStream = AsyncFile.OpenWrite(imagePath);
|
||||
|
||||
var fileStream = AsyncFile.OpenWrite(imagePath);
|
||||
Stream? downloadStream = null;
|
||||
try
|
||||
{
|
||||
await using var downloadStream = await HttpClientFactory
|
||||
downloadStream = await HttpClientFactory
|
||||
.CreateClient(NamedClient.Default)
|
||||
.GetStreamAsync(url)
|
||||
.ConfigureAwait(false);
|
||||
|
@ -402,6 +402,14 @@ namespace Emby.Server.Implementations.Plugins
|
|||
_logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath);
|
||||
imagePath = string.Empty;
|
||||
}
|
||||
finally
|
||||
{
|
||||
await fileStream.DisposeAsync().ConfigureAwait(false);
|
||||
if (downloadStream is not null)
|
||||
{
|
||||
await downloadStream.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var manifest = new PluginManifest
|
||||
|
@ -421,7 +429,7 @@ namespace Emby.Server.Implementations.Plugins
|
|||
ImagePath = imagePath
|
||||
};
|
||||
|
||||
if (!await ReconcileManifest(manifest, path))
|
||||
if (!await ReconcileManifest(manifest, path).ConfigureAwait(false))
|
||||
{
|
||||
// An error occurred during reconciliation and saving could be undesirable.
|
||||
return false;
|
||||
|
@ -458,7 +466,7 @@ namespace Emby.Server.Implementations.Plugins
|
|||
}
|
||||
|
||||
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();
|
||||
|
||||
if (!Equals(localManifest.Id, manifest.Id))
|
||||
|
|
|
@ -115,7 +115,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
|
|||
{
|
||||
try
|
||||
{
|
||||
previouslyFailedImages = File.ReadAllText(failHistoryPath)
|
||||
previouslyFailedImages = (await File.ReadAllTextAsync(failHistoryPath, cancellationToken).ConfigureAwait(false))
|
||||
.Split('|', StringSplitOptions.RemoveEmptyEntries)
|
||||
.ToList();
|
||||
}
|
||||
|
@ -156,7 +156,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks
|
|||
}
|
||||
|
||||
string text = string.Join('|', previouslyFailedImages);
|
||||
File.WriteAllText(failHistoryPath, text);
|
||||
await File.WriteAllTextAsync(failHistoryPath, text, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
|
|
104
Emby.Server.Implementations/SystemManager.cs
Normal file
104
Emby.Server.Implementations/SystemManager.cs
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -27,9 +27,9 @@ namespace Emby.Server.Implementations.Udp
|
|||
|
||||
private readonly byte[] _receiveBuffer = new byte[8192];
|
||||
|
||||
private Socket _udpSocket;
|
||||
private IPEndPoint _endpoint;
|
||||
private bool _disposed = false;
|
||||
private readonly Socket _udpSocket;
|
||||
private readonly IPEndPoint _endpoint;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UdpServer" /> class.
|
||||
|
@ -52,7 +52,10 @@ namespace Emby.Server.Implementations.Udp
|
|||
|
||||
_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);
|
||||
}
|
||||
|
||||
|
@ -74,6 +77,7 @@ namespace Emby.Server.Implementations.Udp
|
|||
|
||||
try
|
||||
{
|
||||
_logger.LogDebug("Sending AutoDiscovery response");
|
||||
await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (SocketException ex)
|
||||
|
@ -99,7 +103,8 @@ namespace Emby.Server.Implementations.Udp
|
|||
{
|
||||
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);
|
||||
if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
|
@ -112,7 +117,7 @@ namespace Emby.Server.Implementations.Udp
|
|||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Don't throw
|
||||
_logger.LogDebug("Broadcast socket operation cancelled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -125,9 +130,8 @@ namespace Emby.Server.Implementations.Udp
|
|||
return;
|
||||
}
|
||||
|
||||
_udpSocket?.Dispose();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
_udpSocket.Dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -504,8 +504,7 @@ namespace Emby.Server.Implementations.Updates
|
|||
|
||||
private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken)
|
||||
{
|
||||
var extension = Path.GetExtension(package.SourceUrl);
|
||||
if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
|
||||
if (!Path.GetExtension(package.SourceUrl.AsSpan()).Equals(".zip", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl);
|
||||
return;
|
||||
|
@ -521,10 +520,9 @@ namespace Emby.Server.Implementations.Updates
|
|||
|
||||
// CA5351: Do Not Use Broken Cryptographic Algorithms
|
||||
#pragma warning disable CA5351
|
||||
using var md5 = MD5.Create();
|
||||
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))
|
||||
{
|
||||
_logger.LogError(
|
||||
|
@ -557,7 +555,7 @@ namespace Emby.Server.Implementations.Updates
|
|||
reader.ExtractToDirectory(targetDir, true);
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ using System.IO;
|
|||
using System.Net.Mime;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna;
|
||||
using Emby.Dlna.Main;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Controller.Dlna;
|
||||
|
@ -33,12 +32,19 @@ public class DlnaServerController : BaseJellyfinApiController
|
|||
/// Initializes a new instance of the <see cref="DlnaServerController"/> class.
|
||||
/// </summary>
|
||||
/// <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;
|
||||
_contentDirectory = DlnaEntryPoint.Current.ContentDirectory;
|
||||
_connectionManager = DlnaEntryPoint.Current.ConnectionManager;
|
||||
_mediaReceiverRegistrar = DlnaEntryPoint.Current.MediaReceiverRegistrar;
|
||||
_contentDirectory = contentDirectory;
|
||||
_connectionManager = connectionManager;
|
||||
_mediaReceiverRegistrar = mediaReceiverRegistrar;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -45,6 +45,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||
private const string DefaultEventEncoderPreset = "superfast";
|
||||
private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
|
||||
|
||||
private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0);
|
||||
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IDlnaManager _dlnaManager;
|
||||
|
@ -408,6 +410,7 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||
/// <param name="context">Optional. The <see cref="EncodingContext"/>.</param>
|
||||
/// <param name="streamOptions">Optional. The streaming options.</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>
|
||||
/// <returns>A <see cref="FileResult"/> containing the playlist file.</returns>
|
||||
[HttpGet("Videos/{itemId}/master.m3u8")]
|
||||
|
@ -465,7 +468,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||
[FromQuery] int? videoStreamIndex,
|
||||
[FromQuery] EncodingContext? context,
|
||||
[FromQuery] Dictionary<string, string> streamOptions,
|
||||
[FromQuery] bool enableAdaptiveBitrateStreaming = true)
|
||||
[FromQuery] bool enableAdaptiveBitrateStreaming = true,
|
||||
[FromQuery] bool enableTrickplay = true)
|
||||
{
|
||||
var streamingRequest = new HlsVideoRequestDto
|
||||
{
|
||||
|
@ -519,7 +523,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||
VideoStreamIndex = videoStreamIndex,
|
||||
Context = context ?? EncodingContext.Streaming,
|
||||
StreamOptions = streamOptions,
|
||||
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming
|
||||
EnableAdaptiveBitrateStreaming = enableAdaptiveBitrateStreaming,
|
||||
EnableTrickplay = enableTrickplay
|
||||
};
|
||||
|
||||
return await _dynamicHlsHelper.GetMasterHlsPlaylist(TranscodingJobType, streamingRequest, enableAdaptiveBitrateStreaming).ConfigureAwait(false);
|
||||
|
@ -1705,16 +1710,31 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||
var audioCodec = _encodingHelper.GetAudioEncoder(state);
|
||||
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
|
||||
|
||||
// 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))
|
||||
{
|
||||
strictArgs = " -strict -2";
|
||||
}
|
||||
|
||||
if (!state.IsOutputVideo)
|
||||
{
|
||||
if (EncodingHelper.IsCopyCodec(audioCodec))
|
||||
{
|
||||
return "-acodec copy -strict -2" + bitStreamArgs;
|
||||
}
|
||||
|
||||
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 audioChannels = state.OutputAudioChannels;
|
||||
|
@ -1742,21 +1762,9 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||
audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
audioTranscodeParams += " -vn";
|
||||
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))
|
||||
{
|
||||
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
||||
|
@ -2041,9 +2049,9 @@ public class DynamicHlsController : BaseJellyfinApiController
|
|||
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);
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ public class HlsSegmentController : BaseJellyfinApiController
|
|||
public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
|
||||
{
|
||||
// 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();
|
||||
file = Path.GetFullPath(Path.Combine(transcodePath, 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")]
|
||||
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();
|
||||
file = Path.GetFullPath(Path.Combine(transcodePath, 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.");
|
||||
}
|
||||
|
@ -138,7 +139,7 @@ public class HlsSegmentController : BaseJellyfinApiController
|
|||
[FromRoute, Required] string segmentId,
|
||||
[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();
|
||||
|
||||
file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
|
||||
|
|
|
@ -7,6 +7,7 @@ using System.Globalization;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
|
@ -78,6 +79,9 @@ public class ImageController : BaseJellyfinApiController
|
|||
_appPaths = appPaths;
|
||||
}
|
||||
|
||||
private static Stream GetFromBase64Stream(Stream inputStream)
|
||||
=> new CryptoStream(inputStream, new FromBase64Transform(), CryptoStreamMode.Read);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the user image.
|
||||
/// </summary>
|
||||
|
@ -116,8 +120,8 @@ public class ImageController : BaseJellyfinApiController
|
|||
return BadRequest("Incorrect ContentType.");
|
||||
}
|
||||
|
||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||
await using (memoryStream.ConfigureAwait(false))
|
||||
var stream = GetFromBase64Stream(Request.Body);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
// Handle image/png; charset=utf-8
|
||||
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));
|
||||
|
||||
await _providerManager
|
||||
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
|
||||
.SaveImage(stream, mimeType, user.ProfileImage.Path)
|
||||
.ConfigureAwait(false);
|
||||
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
||||
|
||||
|
@ -176,8 +180,8 @@ public class ImageController : BaseJellyfinApiController
|
|||
return BadRequest("Incorrect ContentType.");
|
||||
}
|
||||
|
||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||
await using (memoryStream.ConfigureAwait(false))
|
||||
var stream = GetFromBase64Stream(Request.Body);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
// Handle image/png; charset=utf-8
|
||||
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));
|
||||
|
||||
await _providerManager
|
||||
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
|
||||
.SaveImage(stream, mimeType, user.ProfileImage.Path)
|
||||
.ConfigureAwait(false);
|
||||
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
||||
|
||||
|
@ -372,12 +376,12 @@ public class ImageController : BaseJellyfinApiController
|
|||
return BadRequest("Incorrect ContentType.");
|
||||
}
|
||||
|
||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||
await using (memoryStream.ConfigureAwait(false))
|
||||
var stream = GetFromBase64Stream(Request.Body);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
// Handle image/png; charset=utf-8
|
||||
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);
|
||||
|
||||
return NoContent();
|
||||
|
@ -416,12 +420,12 @@ public class ImageController : BaseJellyfinApiController
|
|||
return BadRequest("Incorrect ContentType.");
|
||||
}
|
||||
|
||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||
await using (memoryStream.ConfigureAwait(false))
|
||||
var stream = GetFromBase64Stream(Request.Body);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
// Handle image/png; charset=utf-8
|
||||
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);
|
||||
|
||||
return NoContent();
|
||||
|
@ -1792,8 +1796,8 @@ public class ImageController : BaseJellyfinApiController
|
|||
return BadRequest("Incorrect ContentType.");
|
||||
}
|
||||
|
||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||
await using (memoryStream.ConfigureAwait(false))
|
||||
var stream = GetFromBase64Stream(Request.Body);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
|
||||
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);
|
||||
await using (fs.ConfigureAwait(false))
|
||||
{
|
||||
await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
|
||||
await stream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
|
@ -1833,15 +1837,6 @@ public class ImageController : BaseJellyfinApiController
|
|||
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)
|
||||
{
|
||||
int? width = null;
|
||||
|
|
|
@ -294,8 +294,8 @@ public class LibraryController : BaseJellyfinApiController
|
|||
|
||||
return new AllThemeMediaResult
|
||||
{
|
||||
ThemeSongsResult = themeSongs?.Value,
|
||||
ThemeVideosResult = themeVideos?.Value,
|
||||
ThemeSongsResult = themeSongs.Value,
|
||||
ThemeVideosResult = themeVideos.Value,
|
||||
SoundtrackSongsResult = new ThemeMediaResult()
|
||||
};
|
||||
}
|
||||
|
@ -490,7 +490,7 @@ public class LibraryController : BaseJellyfinApiController
|
|||
|
||||
baseItemDtos.Add(_dtoService.GetBaseItemDto(parent, dtoOptions, user));
|
||||
|
||||
parent = parent?.GetParent();
|
||||
parent = parent.GetParent();
|
||||
}
|
||||
|
||||
return baseItemDtos;
|
||||
|
|
|
@ -6,6 +6,7 @@ using System.Globalization;
|
|||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
@ -405,9 +406,8 @@ public class SubtitleController : BaseJellyfinApiController
|
|||
[FromBody, Required] UploadSubtitleDto body)
|
||||
{
|
||||
var video = (Video)_libraryManager.GetItemById(itemId);
|
||||
var data = Convert.FromBase64String(body.Data);
|
||||
var memoryStream = new MemoryStream(data, 0, data.Length, false, true);
|
||||
await using (memoryStream.ConfigureAwait(false))
|
||||
var stream = new CryptoStream(Request.Body, new FromBase64Transform(), CryptoStreamMode.Read);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
await _subtitleManager.UploadSubtitle(
|
||||
video,
|
||||
|
@ -417,7 +417,7 @@ public class SubtitleController : BaseJellyfinApiController
|
|||
Language = body.Language,
|
||||
IsForced = body.IsForced,
|
||||
IsHearingImpaired = body.IsHearingImpaired,
|
||||
Stream = memoryStream
|
||||
Stream = stream
|
||||
}).ConfigureAwait(false);
|
||||
_providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ using MediaBrowser.Common.Configuration;
|
|||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.System;
|
||||
|
@ -26,32 +25,36 @@ namespace Jellyfin.Api.Controllers;
|
|||
/// </summary>
|
||||
public class SystemController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly ILogger<SystemController> _logger;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly INetworkManager _network;
|
||||
private readonly ILogger<SystemController> _logger;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly ISystemManager _systemManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SystemController"/> class.
|
||||
/// </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="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="network">Instance of <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="systemManager">Instance of <see cref="ISystemManager"/> interface.</param>
|
||||
public SystemController(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<SystemController> logger,
|
||||
IServerApplicationHost appHost,
|
||||
IServerApplicationPaths appPaths,
|
||||
IFileSystem fileSystem,
|
||||
INetworkManager network,
|
||||
ILogger<SystemController> logger)
|
||||
INetworkManager networkManager,
|
||||
ISystemManager systemManager)
|
||||
{
|
||||
_appPaths = serverConfigurationManager.ApplicationPaths;
|
||||
_appHost = appHost;
|
||||
_fileSystem = fileSystem;
|
||||
_network = network;
|
||||
_logger = logger;
|
||||
_appHost = appHost;
|
||||
_appPaths = appPaths;
|
||||
_fileSystem = fileSystem;
|
||||
_networkManager = networkManager;
|
||||
_systemManager = systemManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -65,9 +68,7 @@ public class SystemController : BaseJellyfinApiController
|
|||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public ActionResult<SystemInfo> GetSystemInfo()
|
||||
{
|
||||
return _appHost.GetSystemInfo(Request);
|
||||
}
|
||||
=> _systemManager.GetSystemInfo(Request);
|
||||
|
||||
/// <summary>
|
||||
/// Gets public information about the server.
|
||||
|
@ -77,9 +78,7 @@ public class SystemController : BaseJellyfinApiController
|
|||
[HttpGet("Info/Public")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
|
||||
{
|
||||
return _appHost.GetPublicSystemInfo(Request);
|
||||
}
|
||||
=> _systemManager.GetPublicSystemInfo(Request);
|
||||
|
||||
/// <summary>
|
||||
/// Pings the system.
|
||||
|
@ -90,9 +89,7 @@ public class SystemController : BaseJellyfinApiController
|
|||
[HttpPost("Ping", Name = "PostPingSystem")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<string> PingSystem()
|
||||
{
|
||||
return _appHost.Name;
|
||||
}
|
||||
=> _appHost.Name;
|
||||
|
||||
/// <summary>
|
||||
/// Restarts the application.
|
||||
|
@ -106,7 +103,7 @@ public class SystemController : BaseJellyfinApiController
|
|||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public ActionResult RestartApplication()
|
||||
{
|
||||
_appHost.Restart();
|
||||
_systemManager.Restart();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
|
@ -122,7 +119,7 @@ public class SystemController : BaseJellyfinApiController
|
|||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public ActionResult ShutdownApplication()
|
||||
{
|
||||
_appHost.Shutdown();
|
||||
_systemManager.Shutdown();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
|
@ -180,7 +177,7 @@ public class SystemController : BaseJellyfinApiController
|
|||
return new EndPointInfo
|
||||
{
|
||||
IsLocal = HttpContext.IsLocal(),
|
||||
IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP())
|
||||
IsInNetwork = _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP())
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -218,7 +215,7 @@ public class SystemController : BaseJellyfinApiController
|
|||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
|
||||
{
|
||||
var result = _network.GetMacAddresses()
|
||||
var result = _networkManager.GetMacAddresses()
|
||||
.Select(i => new WakeOnLanInfo(i));
|
||||
return Ok(result);
|
||||
}
|
||||
|
|
101
Jellyfin.Api/Controllers/TrickplayController.cs
Normal file
101
Jellyfin.Api/Controllers/TrickplayController.cs
Normal 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();
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ using System.Threading;
|
|||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Extensions;
|
||||
using Jellyfin.Api.Models.StreamingDtos;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
|
@ -19,6 +20,7 @@ using MediaBrowser.Controller.Devices;
|
|||
using MediaBrowser.Controller.Dlna;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Trickplay;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Net;
|
||||
|
@ -46,6 +48,7 @@ public class DynamicHlsHelper
|
|||
private readonly ILogger<DynamicHlsHelper> _logger;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly EncodingHelper _encodingHelper;
|
||||
private readonly ITrickplayManager _trickplayManager;
|
||||
|
||||
/// <summary>
|
||||
/// 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="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
|
||||
/// <param name="encodingHelper">Instance of <see cref="EncodingHelper"/>.</param>
|
||||
/// <param name="trickplayManager">Instance of <see cref="ITrickplayManager"/>.</param>
|
||||
public DynamicHlsHelper(
|
||||
ILibraryManager libraryManager,
|
||||
IUserManager userManager,
|
||||
|
@ -74,7 +78,8 @@ public class DynamicHlsHelper
|
|||
INetworkManager networkManager,
|
||||
ILogger<DynamicHlsHelper> logger,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
EncodingHelper encodingHelper)
|
||||
EncodingHelper encodingHelper,
|
||||
ITrickplayManager trickplayManager)
|
||||
{
|
||||
_libraryManager = libraryManager;
|
||||
_userManager = userManager;
|
||||
|
@ -88,6 +93,7 @@ public class DynamicHlsHelper
|
|||
_logger = logger;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_encodingHelper = encodingHelper;
|
||||
_trickplayManager = trickplayManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -200,13 +206,6 @@ public class DynamicHlsHelper
|
|||
|
||||
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();
|
||||
|
||||
// Provide SDR HEVC entrance for backward compatibility.
|
||||
|
@ -236,14 +235,7 @@ public class DynamicHlsHelper
|
|||
}
|
||||
|
||||
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
|
||||
var sdrPlaylist = 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);
|
||||
}
|
||||
AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
|
||||
|
||||
// Restore the video codec
|
||||
state.OutputVideoCodec = "copy";
|
||||
|
@ -274,13 +266,6 @@ public class DynamicHlsHelper
|
|||
state.VideoStream.Level = originalLevel;
|
||||
var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
|
||||
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);
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
/// Get the H.26X level of the output video stream.
|
||||
/// </summary>
|
||||
|
@ -767,16 +794,4 @@ public class DynamicHlsHelper
|
|||
newValue.ToString(),
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,9 @@ using System.Text;
|
|||
namespace Jellyfin.Api.Helpers;
|
||||
|
||||
/// <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>
|
||||
public static class HlsCodecStringHelpers
|
||||
{
|
||||
|
@ -27,7 +29,7 @@ public static class HlsCodecStringHelpers
|
|||
/// <summary>
|
||||
/// Codec name for FLAC.
|
||||
/// </summary>
|
||||
public const string FLAC = "flac";
|
||||
public const string FLAC = "fLaC";
|
||||
|
||||
/// <summary>
|
||||
/// Codec name for ALAC.
|
||||
|
@ -37,7 +39,7 @@ public static class HlsCodecStringHelpers
|
|||
/// <summary>
|
||||
/// Codec name for OPUS.
|
||||
/// </summary>
|
||||
public const string OPUS = "opus";
|
||||
public const string OPUS = "Opus";
|
||||
|
||||
/// <summary>
|
||||
/// Gets a MP3 codec string.
|
||||
|
|
|
@ -248,7 +248,7 @@ public static class StreamingHelpers
|
|||
? GetOutputFileExtension(state, mediaSource)
|
||||
: ("." + state.OutputContainer);
|
||||
|
||||
state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
|
||||
state.OutputFilePath = GetOutputFilePath(state, ext, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
@ -421,10 +421,9 @@ public static class StreamingHelpers
|
|||
/// <param name="state">The state.</param>
|
||||
/// <param name="mediaSource">The mediaSource.</param>
|
||||
/// <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);
|
||||
|
||||
if (!string.IsNullOrEmpty(ext))
|
||||
{
|
||||
return ext;
|
||||
|
@ -463,10 +462,9 @@ public static class StreamingHelpers
|
|||
return ".asf";
|
||||
}
|
||||
}
|
||||
|
||||
// Try to infer based on the desired audio codec
|
||||
if (!state.IsVideoRequest)
|
||||
else
|
||||
{
|
||||
// Try to infer based on the desired audio codec
|
||||
var audioCodec = state.Request.AudioCodec;
|
||||
|
||||
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 null;
|
||||
throw new InvalidOperationException("Failed to find an appropriate file extension");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -514,7 +512,7 @@ public static class StreamingHelpers
|
|||
var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
|
||||
|
||||
var filename = data.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
var ext = outputFileExtension?.ToLowerInvariant();
|
||||
var ext = outputFileExtension.ToLowerInvariant();
|
||||
var folder = serverConfigurationManager.GetTranscodePath();
|
||||
|
||||
return Path.Combine(folder, filename + ext);
|
||||
|
|
|
@ -538,7 +538,7 @@ public class TranscodingJobHelper : IDisposable
|
|||
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 subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal));
|
||||
|
|
|
@ -8,8 +8,6 @@
|
|||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 -->
|
||||
<NoWarn>AD0001</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
@ -122,17 +122,17 @@ public class ExceptionMiddleware
|
|||
|
||||
private static int GetStatusCode(Exception ex)
|
||||
{
|
||||
switch (ex)
|
||||
return ex switch
|
||||
{
|
||||
case ArgumentException _: return StatusCodes.Status400BadRequest;
|
||||
case AuthenticationException _: return StatusCodes.Status401Unauthorized;
|
||||
case SecurityException _: return StatusCodes.Status403Forbidden;
|
||||
case DirectoryNotFoundException _:
|
||||
case FileNotFoundException _:
|
||||
case ResourceNotFoundException _: return StatusCodes.Status404NotFound;
|
||||
case MethodNotAllowedException _: return StatusCodes.Status405MethodNotAllowed;
|
||||
default: return StatusCodes.Status500InternalServerError;
|
||||
}
|
||||
ArgumentException => StatusCodes.Status400BadRequest,
|
||||
AuthenticationException => StatusCodes.Status401Unauthorized,
|
||||
SecurityException => StatusCodes.Status403Forbidden,
|
||||
DirectoryNotFoundException => StatusCodes.Status404NotFound,
|
||||
FileNotFoundException => StatusCodes.Status404NotFound,
|
||||
ResourceNotFoundException => StatusCodes.Status404NotFound,
|
||||
MethodNotAllowedException => StatusCodes.Status405MethodNotAllowed,
|
||||
_ => StatusCodes.Status500InternalServerError
|
||||
};
|
||||
}
|
||||
|
||||
private string NormalizeExceptionMessage(string msg)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
namespace Jellyfin.Api.Models.StreamingDtos;
|
||||
namespace Jellyfin.Api.Models.StreamingDtos;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public bool EnableSubtitlesInManifest { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to enable trickplay images.
|
||||
/// </summary>
|
||||
public bool EnableTrickplay { get; set; }
|
||||
}
|
||||
|
|
75
Jellyfin.Data/Entities/TrickplayInfo.cs
Normal file
75
Jellyfin.Data/Entities/TrickplayInfo.cs
Normal 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; }
|
||||
}
|
|
@ -288,6 +288,12 @@ namespace Jellyfin.Data.Entities
|
|||
/// </summary>
|
||||
public SyncPlayUserAccessType SyncPlayAccess { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the cast receiver id.
|
||||
/// </summary>
|
||||
[StringLength(32)]
|
||||
public string? CastReceiverId { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[ConcurrencyCheck]
|
||||
public uint RowVersion { get; private set; }
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text.RegularExpressions;
|
||||
|
@ -204,7 +203,7 @@ public static partial class NetworkExtensions
|
|||
{
|
||||
var ipBlock = splitString.Current;
|
||||
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;
|
||||
}
|
||||
|
@ -231,12 +230,12 @@ public static partial class NetworkExtensions
|
|||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -284,12 +283,15 @@ public static partial class NetworkExtensions
|
|||
|
||||
if (hosts.Count <= 2)
|
||||
{
|
||||
var firstPart = hosts[0];
|
||||
|
||||
// Is hostname or hostname:port
|
||||
if (FqdnGeneratedRegex().IsMatch(hosts[0]))
|
||||
if (FqdnGeneratedRegex().IsMatch(firstPart))
|
||||
{
|
||||
try
|
||||
{
|
||||
addresses = Dns.GetHostAddresses(hosts[0]);
|
||||
// .NET automatically filters only supported returned addresses based on OS support.
|
||||
addresses = Dns.GetHostAddresses(firstPart);
|
||||
return true;
|
||||
}
|
||||
catch (SocketException)
|
||||
|
@ -299,7 +301,7 @@ public static partial class NetworkExtensions
|
|||
}
|
||||
|
||||
// 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))
|
||||
|| ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled)))
|
||||
|
|
|
@ -15,7 +15,9 @@ using MediaBrowser.Common.Net;
|
|||
using MediaBrowser.Model.Net;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using static MediaBrowser.Controller.Extensions.ConfigurationExtensions;
|
||||
|
||||
namespace Jellyfin.Networking.Manager
|
||||
{
|
||||
|
@ -33,12 +35,14 @@ namespace Jellyfin.Networking.Manager
|
|||
|
||||
private readonly IConfigurationManager _configurationManager;
|
||||
|
||||
private readonly IConfiguration _startupConfig;
|
||||
|
||||
private readonly object _networkEventLock;
|
||||
|
||||
/// <summary>
|
||||
/// Holds the published server URLs and the IPs to use them on.
|
||||
/// </summary>
|
||||
private IReadOnlyDictionary<IPData, string> _publishedServerUrls;
|
||||
private IReadOnlyList<PublishedServerUriOverride> _publishedServerUrls;
|
||||
|
||||
private IReadOnlyList<IPNetwork> _remoteAddressFilter;
|
||||
|
||||
|
@ -76,20 +80,22 @@ namespace Jellyfin.Networking.Manager
|
|||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="NetworkManager"/> class.
|
||||
/// </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>
|
||||
#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(configurationManager);
|
||||
|
||||
_logger = logger;
|
||||
_configurationManager = configurationManager;
|
||||
_startupConfig = startupConfig;
|
||||
_initLock = new();
|
||||
_interfaces = new List<IPData>();
|
||||
_macAddresses = new List<PhysicalAddress>();
|
||||
_publishedServerUrls = new Dictionary<IPData, string>();
|
||||
_publishedServerUrls = new List<PublishedServerUriOverride>();
|
||||
_networkEventLock = new object();
|
||||
_remoteAddressFilter = new List<IPNetwork>();
|
||||
|
||||
|
@ -130,7 +136,7 @@ namespace Jellyfin.Networking.Manager
|
|||
/// <summary>
|
||||
/// Gets the Published server override list.
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<IPData, string> PublishedServerUrls => _publishedServerUrls;
|
||||
public IReadOnlyList<PublishedServerUriOverride> PublishedServerUrls => _publishedServerUrls;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
|
@ -170,7 +176,6 @@ namespace Jellyfin.Networking.Manager
|
|||
{
|
||||
if (!_eventfire)
|
||||
{
|
||||
_logger.LogDebug("Network Address Change Event.");
|
||||
// As network events tend to fire one after the other only fire once every second.
|
||||
_eventfire = true;
|
||||
OnNetworkChange();
|
||||
|
@ -193,11 +198,12 @@ namespace Jellyfin.Networking.Manager
|
|||
}
|
||||
else
|
||||
{
|
||||
InitialiseInterfaces();
|
||||
InitialiseLan(networkConfig);
|
||||
InitializeInterfaces();
|
||||
InitializeLan(networkConfig);
|
||||
EnforceBindSettings(networkConfig);
|
||||
}
|
||||
|
||||
PrintNetworkInformation(networkConfig);
|
||||
NetworkChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
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 active mac addresses that aren't loopback addresses.
|
||||
/// </summary>
|
||||
private void InitialiseInterfaces()
|
||||
private void InitializeInterfaces()
|
||||
{
|
||||
lock (_initLock)
|
||||
{
|
||||
|
@ -222,7 +228,7 @@ namespace Jellyfin.Networking.Manager
|
|||
try
|
||||
{
|
||||
var nics = NetworkInterface.GetAllNetworkInterfaces()
|
||||
.Where(i => i.SupportsMulticast && i.OperationalStatus == OperationalStatus.Up);
|
||||
.Where(i => i.OperationalStatus == OperationalStatus.Up);
|
||||
|
||||
foreach (NetworkInterface adapter in nics)
|
||||
{
|
||||
|
@ -242,34 +248,36 @@ namespace Jellyfin.Networking.Manager
|
|||
{
|
||||
if (IsIPv4Enabled && info.Address.AddressFamily == AddressFamily.InterNetwork)
|
||||
{
|
||||
var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name);
|
||||
interfaceObject.Index = ipProperties.GetIPv4Properties().Index;
|
||||
interfaceObject.Name = adapter.Name;
|
||||
var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
|
||||
{
|
||||
Index = ipProperties.GetIPv4Properties().Index,
|
||||
Name = adapter.Name,
|
||||
SupportsMulticast = adapter.SupportsMulticast
|
||||
};
|
||||
|
||||
interfaces.Add(interfaceObject);
|
||||
}
|
||||
else if (IsIPv6Enabled && info.Address.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
{
|
||||
var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name);
|
||||
interfaceObject.Index = ipProperties.GetIPv6Properties().Index;
|
||||
interfaceObject.Name = adapter.Name;
|
||||
var interfaceObject = new IPData(info.Address, new IPNetwork(info.Address, info.PrefixLength), adapter.Name)
|
||||
{
|
||||
Index = ipProperties.GetIPv6Properties().Index,
|
||||
Name = adapter.Name,
|
||||
SupportsMulticast = adapter.SupportsMulticast
|
||||
};
|
||||
|
||||
interfaces.Add(interfaceObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma warning disable CA1031 // Do not catch general exception types
|
||||
catch (Exception ex)
|
||||
#pragma warning restore CA1031 // Do not catch general exception types
|
||||
{
|
||||
// Ignore error, and attempt to continue.
|
||||
_logger.LogError(ex, "Error encountered parsing interfaces.");
|
||||
}
|
||||
}
|
||||
}
|
||||
#pragma warning disable CA1031 // Do not catch general exception types
|
||||
catch (Exception ex)
|
||||
#pragma warning restore CA1031 // Do not catch general exception types
|
||||
{
|
||||
_logger.LogError(ex, "Error obtaining interfaces.");
|
||||
}
|
||||
|
@ -279,14 +287,14 @@ namespace Jellyfin.Networking.Manager
|
|||
{
|
||||
_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>
|
||||
/// Initialises internal LAN cache.
|
||||
/// Initializes internal LAN cache.
|
||||
/// </summary>
|
||||
private void InitialiseLan(NetworkConfiguration config)
|
||||
private void InitializeLan(NetworkConfiguration config)
|
||||
{
|
||||
lock (_initLock)
|
||||
{
|
||||
|
@ -341,10 +349,6 @@ namespace Jellyfin.Networking.Manager
|
|||
_excludedSubnets = NetworkExtensions.TryParseToSubnets(subnets, out var excludedSubnets, true)
|
||||
? excludedSubnets
|
||||
: 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();
|
||||
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"));
|
||||
}
|
||||
|
||||
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"));
|
||||
}
|
||||
|
@ -409,15 +413,14 @@ namespace Jellyfin.Networking.Manager
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialises the remote address values.
|
||||
/// Initializes the remote address values.
|
||||
/// </summary>
|
||||
private void InitialiseRemote(NetworkConfiguration config)
|
||||
private void InitializeRemote(NetworkConfiguration config)
|
||||
{
|
||||
lock (_initLock)
|
||||
{
|
||||
|
@ -455,13 +458,33 @@ namespace Jellyfin.Networking.Manager
|
|||
/// format is subnet=ipaddress|host|uri
|
||||
/// when subnet = 0.0.0.0, any external address matches.
|
||||
/// </summary>
|
||||
private void InitialiseOverrides(NetworkConfiguration config)
|
||||
private void InitializeOverrides(NetworkConfiguration config)
|
||||
{
|
||||
lock (_initLock)
|
||||
{
|
||||
var publishedServerUrls = new Dictionary<IPData, string>();
|
||||
var overrides = config.PublishedServerUriBySubnet;
|
||||
var publishedServerUrls = new List<PublishedServerUriOverride>();
|
||||
|
||||
// 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)
|
||||
{
|
||||
var parts = entry.Split('=');
|
||||
|
@ -475,31 +498,70 @@ namespace Jellyfin.Networking.Manager
|
|||
var identifier = parts[0];
|
||||
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))
|
||||
{
|
||||
publishedServerUrls[new IPData(IPAddress.Any, Network.IPv4Any)] = replacement;
|
||||
publishedServerUrls[new IPData(IPAddress.IPv6Any, Network.IPv6Any)] = replacement;
|
||||
publishedServerUrls.Add(
|
||||
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))
|
||||
{
|
||||
foreach (var lan in _lanSubnets)
|
||||
{
|
||||
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)
|
||||
{
|
||||
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))
|
||||
{
|
||||
foreach (var iface in ifaces)
|
||||
{
|
||||
publishedServerUrls[iface] = replacement;
|
||||
publishedServerUrls.Add(
|
||||
new PublishedServerUriOverride(
|
||||
iface,
|
||||
replacement,
|
||||
true,
|
||||
true));
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -521,7 +583,7 @@ namespace Jellyfin.Networking.Manager
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reloads all settings and re-initialises the instance.
|
||||
/// Reloads all settings and re-Initializes the instance.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The <see cref="NetworkConfiguration"/> to use.</param>
|
||||
public void UpdateSettings(object configuration)
|
||||
|
@ -531,12 +593,12 @@ namespace Jellyfin.Networking.Manager
|
|||
var config = (NetworkConfiguration)configuration;
|
||||
HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6;
|
||||
|
||||
InitialiseLan(config);
|
||||
InitialiseRemote(config);
|
||||
InitializeLan(config);
|
||||
InitializeRemote(config);
|
||||
|
||||
if (string.IsNullOrEmpty(MockNetworkSettings))
|
||||
{
|
||||
InitialiseInterfaces();
|
||||
InitializeInterfaces();
|
||||
}
|
||||
else // Used in testing only.
|
||||
{
|
||||
|
@ -552,8 +614,10 @@ namespace Jellyfin.Networking.Manager
|
|||
var index = int.Parse(parts[1], CultureInfo.InvariantCulture);
|
||||
if (address.AddressFamily == AddressFamily.InterNetwork || address.AddressFamily == AddressFamily.InterNetworkV6)
|
||||
{
|
||||
var data = new IPData(address, subnet, parts[2]);
|
||||
data.Index = index;
|
||||
var data = new IPData(address, subnet, parts[2])
|
||||
{
|
||||
Index = index
|
||||
};
|
||||
interfaces.Add(data);
|
||||
}
|
||||
}
|
||||
|
@ -567,7 +631,9 @@ namespace Jellyfin.Networking.Manager
|
|||
}
|
||||
|
||||
EnforceBindSettings(config);
|
||||
InitialiseOverrides(config);
|
||||
InitializeOverrides(config);
|
||||
|
||||
PrintNetworkInformation(config, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -672,20 +738,13 @@ namespace Jellyfin.Networking.Manager
|
|||
/// <inheritdoc/>
|
||||
public IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false)
|
||||
{
|
||||
if (_interfaces.Count != 0)
|
||||
if (_interfaces.Count > 0 || individualInterfaces)
|
||||
{
|
||||
return _interfaces;
|
||||
}
|
||||
|
||||
// No bind address and no exclusions, so listen on all interfaces.
|
||||
var result = new List<IPData>();
|
||||
|
||||
if (individualInterfaces)
|
||||
{
|
||||
result.AddRange(_interfaces);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (IsIPv4Enabled && IsIPv6Enabled)
|
||||
{
|
||||
// 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;
|
||||
int? port = null;
|
||||
|
||||
var validPublishedServerUrls = _publishedServerUrls.Where(x => x.Key.Address.Equals(IPAddress.Any)
|
||||
|| x.Key.Address.Equals(IPAddress.IPv6Any)
|
||||
|| x.Key.Subnet.Contains(source))
|
||||
.DistinctBy(x => x.Key)
|
||||
.OrderBy(x => x.Key.Address.Equals(IPAddress.Any)
|
||||
|| x.Key.Address.Equals(IPAddress.IPv6Any))
|
||||
// Only consider subnets including the source IP, prefering specific overrides
|
||||
List<PublishedServerUriOverride> validPublishedServerUrls;
|
||||
if (!isInExternalSubnet)
|
||||
{
|
||||
// Only use matching internal subnets
|
||||
// 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();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only use matching external subnets
|
||||
// Prefer more specific (bigger subnet prefix) overrides
|
||||
validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && x.Data.Subnet.Contains(source))
|
||||
.OrderByDescending(x => x.Data.Subnet.PrefixLength)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
// Check for user override.
|
||||
foreach (var data in validPublishedServerUrls)
|
||||
{
|
||||
if (isInExternalSubnet && (data.Key.Address.Equals(IPAddress.Any) || data.Key.Address.Equals(IPAddress.IPv6Any)))
|
||||
{
|
||||
// External.
|
||||
bindPreference = data.Value;
|
||||
break;
|
||||
}
|
||||
|
||||
// Get address interface.
|
||||
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)
|
||||
{
|
||||
// Match IP address.
|
||||
bindPreference = data.Value;
|
||||
// If matching interface is found, use override
|
||||
bindPreference = data.OverrideUri;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -927,7 +989,7 @@ namespace Jellyfin.Networking.Manager
|
|||
return false;
|
||||
}
|
||||
|
||||
// Has it got a port defined?
|
||||
// Handle override specifying port
|
||||
var parts = bindPreference.Split(':');
|
||||
if (parts.Length > 1)
|
||||
{
|
||||
|
@ -935,18 +997,12 @@ namespace Jellyfin.Networking.Manager
|
|||
{
|
||||
bindPreference = parts[0];
|
||||
port = p;
|
||||
_logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (port is not null)
|
||||
{
|
||||
_logger.LogDebug("{Source}: Matching bind address override found: {Address}:{Port}", source, bindPreference, port);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -1053,5 +1109,19 @@ namespace Jellyfin.Networking.Manager
|
|||
_logger.LogDebug("{Source}: Using first external interface as bind address: {Result}", source, result);
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
using System.Globalization;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using Jellyfin.Data.Events;
|
||||
using MediaBrowser.Controller.Events;
|
||||
using MediaBrowser.Controller.Events.Authentication;
|
||||
using MediaBrowser.Model.Activity;
|
||||
|
|
|
@ -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.Server.Implementations.Events.Consumers.Library;
|
||||
using Jellyfin.Server.Implementations.Events.Consumers.Security;
|
||||
|
|
|
@ -78,6 +78,11 @@ public class JellyfinDbContext : DbContext
|
|||
/// </summary>
|
||||
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<Book> Books => Set<Book>();
|
||||
|
|
681
Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs
generated
Normal file
681
Jellyfin.Server.Implementations/Migrations/20230626233818_AddTrickplayInfos.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
654
Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs
generated
Normal file
654
Jellyfin.Server.Implementations/Migrations/20230923170422_UserCastReceiver.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// <auto-generated />
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Jellyfin.Server.Implementations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.5");
|
||||
modelBuilder.HasAnnotation("ProductVersion", "7.0.11");
|
||||
|
||||
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
|
||||
{
|
||||
|
@ -442,6 +442,37 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||
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")
|
||||
|
@ -457,6 +488,10 @@ namespace Jellyfin.Server.Implementations.Migrations
|
|||
.HasMaxLength(255)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("CastReceiverId")
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<bool>("DisplayCollectionsView")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
}
|
|
@ -49,14 +49,13 @@ namespace Jellyfin.Server.Implementations.Security
|
|||
/// <summary>
|
||||
/// Gets the authorization.
|
||||
/// </summary>
|
||||
/// <param name="httpReq">The HTTP req.</param>
|
||||
/// <param name="httpContext">The HTTP context.</param>
|
||||
/// <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 GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query).ConfigureAwait(false);
|
||||
var authInfo = await GetAuthorizationInfo(httpContext.Request).ConfigureAwait(false);
|
||||
|
||||
httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
|
||||
httpContext.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
|
||||
return authInfo;
|
||||
}
|
||||
|
||||
|
@ -80,7 +79,6 @@ namespace Jellyfin.Server.Implementations.Security
|
|||
auth.TryGetValue("Token", out token);
|
||||
}
|
||||
|
||||
#pragma warning disable CA1508 // string.IsNullOrEmpty(token) is always false.
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
token = headers["X-Emby-Token"];
|
||||
|
@ -118,7 +116,6 @@ namespace Jellyfin.Server.Implementations.Security
|
|||
// Request doesn't contain a token.
|
||||
return authInfo;
|
||||
}
|
||||
#pragma warning restore CA1508
|
||||
|
||||
authInfo.HasToken = true;
|
||||
var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
|
@ -219,24 +216,7 @@ namespace Jellyfin.Server.Implementations.Security
|
|||
/// <summary>
|
||||
/// Gets the auth.
|
||||
/// </summary>
|
||||
/// <param name="httpReq">The HTTP req.</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>
|
||||
/// <param name="httpReq">The HTTP request.</param>
|
||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||
private static Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
|
||||
{
|
||||
|
|
474
Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
Normal file
474
Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ using MediaBrowser.Common;
|
|||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Drawing;
|
||||
using MediaBrowser.Controller.Events;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
@ -43,6 +44,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
private readonly InvalidAuthProvider _invalidAuthProvider;
|
||||
private readonly DefaultAuthenticationProvider _defaultAuthenticationProvider;
|
||||
private readonly DefaultPasswordResetProvider _defaultPasswordResetProvider;
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
|
||||
private readonly IDictionary<Guid, User> _users;
|
||||
|
||||
|
@ -55,13 +57,15 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
/// <param name="appHost">The application host.</param>
|
||||
/// <param name="imageProcessor">The image processor.</param>
|
||||
/// <param name="logger">The logger.</param>
|
||||
/// <param name="serverConfigurationManager">The system config manager.</param>
|
||||
public UserManager(
|
||||
IDbContextFactory<JellyfinDbContext> dbProvider,
|
||||
IEventManager eventManager,
|
||||
INetworkManager networkManager,
|
||||
IApplicationHost appHost,
|
||||
IImageProcessor imageProcessor,
|
||||
ILogger<UserManager> logger)
|
||||
ILogger<UserManager> logger,
|
||||
IServerConfigurationManager serverConfigurationManager)
|
||||
{
|
||||
_dbProvider = dbProvider;
|
||||
_eventManager = eventManager;
|
||||
|
@ -69,6 +73,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
_appHost = appHost;
|
||||
_imageProcessor = imageProcessor;
|
||||
_logger = logger;
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
|
||||
_passwordResetProviders = appHost.GetExports<IPasswordResetProvider>();
|
||||
_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 @
|
||||
// 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 ( )
|
||||
[GeneratedRegex("^[\\w\\ \\-'._@]+$")]
|
||||
[GeneratedRegex(@"^[\w\ \-'._@]+$")]
|
||||
private static partial Regex ValidUsernameRegex();
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
@ -288,6 +293,7 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
public UserDto GetUserDto(User user, string? remoteEndPoint = null)
|
||||
{
|
||||
var hasPassword = GetAuthenticationProvider(user).HasPassword(user);
|
||||
var castReceiverApplications = _serverConfigurationManager.Configuration.CastReceiverApplications;
|
||||
return new UserDto
|
||||
{
|
||||
Name = user.Username,
|
||||
|
@ -315,7 +321,11 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
OrderedViews = user.GetPreferenceValues<Guid>(PreferenceKind.OrderedViews),
|
||||
GroupedFolders = user.GetPreferenceValues<Guid>(PreferenceKind.GroupedFolders),
|
||||
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
|
||||
{
|
||||
|
@ -604,6 +614,13 @@ namespace Jellyfin.Server.Implementations.Users
|
|||
user.RememberSubtitleSelections = config.RememberSubtitleSelections;
|
||||
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.GroupedFolders, config.GroupedFolders);
|
||||
user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
|
||||
|
|
|
@ -11,6 +11,7 @@ using Jellyfin.Server.Implementations.Activity;
|
|||
using Jellyfin.Server.Implementations.Devices;
|
||||
using Jellyfin.Server.Implementations.Events;
|
||||
using Jellyfin.Server.Implementations.Security;
|
||||
using Jellyfin.Server.Implementations.Trickplay;
|
||||
using Jellyfin.Server.Implementations.Users;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.BaseItemManager;
|
||||
|
@ -21,6 +22,7 @@ using MediaBrowser.Controller.Library;
|
|||
using MediaBrowser.Controller.Lyrics;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Controller.Security;
|
||||
using MediaBrowser.Controller.Trickplay;
|
||||
using MediaBrowser.Model.Activity;
|
||||
using MediaBrowser.Providers.Lyric;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
@ -78,6 +80,7 @@ namespace Jellyfin.Server
|
|||
serviceCollection.AddSingleton<IUserManager, UserManager>();
|
||||
serviceCollection.AddScoped<IDisplayPreferencesManager, DisplayPreferencesManager>();
|
||||
serviceCollection.AddSingleton<IDeviceManager, DeviceManager>();
|
||||
serviceCollection.AddSingleton<ITrickplayManager, TrickplayManager>();
|
||||
|
||||
// TODO search the assemblies instead of adding them manually?
|
||||
serviceCollection.AddSingleton<IWebSocketListener, SessionWebSocketListener>();
|
||||
|
|
|
@ -282,7 +282,7 @@ namespace Jellyfin.Server.Extensions
|
|||
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)
|
||||
{
|
||||
|
|
|
@ -3,7 +3,6 @@ using System.IO;
|
|||
using System.Net;
|
||||
using Jellyfin.Server.Helpers;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller.Extensions;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
@ -36,7 +35,7 @@ public static class WebHostBuilderExtensions
|
|||
return builder
|
||||
.UseKestrel((builderContext, options) =>
|
||||
{
|
||||
var addresses = appHost.NetManager.GetAllBindInterfaces();
|
||||
var addresses = appHost.NetManager.GetAllBindInterfaces(true);
|
||||
|
||||
bool flagged = false;
|
||||
foreach (var netAdd in addresses)
|
||||
|
|
|
@ -42,7 +42,8 @@ namespace Jellyfin.Server.Migrations
|
|||
typeof(Routines.RemoveDownloadImagesInAdvance),
|
||||
typeof(Routines.MigrateAuthenticationDb),
|
||||
typeof(Routines.FixPlaylistOwner),
|
||||
typeof(Routines.MigrateRatingLevels)
|
||||
typeof(Routines.MigrateRatingLevels),
|
||||
typeof(Routines.AddDefaultCastReceivers)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@ using System.Globalization;
|
|||
using System.IO;
|
||||
using Emby.Server.Implementations.Data;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Model.Globalization;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using Emby.Dlna.Extensions;
|
||||
using Jellyfin.Api.Middleware;
|
||||
using Jellyfin.MediaEncoding.Hls.Extensions;
|
||||
using Jellyfin.Networking.Configuration;
|
||||
|
@ -27,7 +27,6 @@ using Microsoft.Extensions.Configuration;
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.VisualBasic;
|
||||
using Prometheus;
|
||||
|
||||
namespace Jellyfin.Server
|
||||
|
@ -120,26 +119,11 @@ namespace Jellyfin.Server
|
|||
})
|
||||
.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()
|
||||
.AddCheck<DbContextFactoryHealthCheck<JellyfinDbContext>>(nameof(JellyfinDbContext));
|
||||
|
||||
services.AddHlsPlaylistGenerator();
|
||||
services.AddDlnaServices(_serverApplicationHost);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
@ -15,65 +15,13 @@ namespace MediaBrowser.Common.Extensions
|
|||
/// </summary>
|
||||
/// <param name="process">The process to wait for.</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>
|
||||
/// <exception cref="InvalidOperationException">If <see cref="Process.EnableRaisingEvents"/> is not set to true for the process.</exception>
|
||||
public static async Task<bool> WaitForExitAsync(this Process process, TimeSpan timeout)
|
||||
/// <returns>A task that will complete when the process has exited, cancellation has been requested, or an error occurs.</returns>
|
||||
/// <exception cref="OperationCanceledException">The timeout ended.</exception>
|
||||
public static async Task WaitForExitAsync(this Process process, TimeSpan timeout)
|
||||
{
|
||||
using (var cancelTokenSource = new CancellationTokenSource(timeout))
|
||||
{
|
||||
return await WaitForExitAsync(process, 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;
|
||||
await process.WaitForExitAsync(cancelTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,21 +35,15 @@ namespace MediaBrowser.Common
|
|||
string SystemId { get; }
|
||||
|
||||
/// <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>
|
||||
/// <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; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is currently shutting down.
|
||||
/// Gets or sets a value indicating whether the application should restart.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance is shutting down; otherwise, <c>false</c>.</value>
|
||||
bool IsShuttingDown { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the application should restart.
|
||||
/// </summary>
|
||||
bool ShouldRestart { get; }
|
||||
bool ShouldRestart { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the application version.
|
||||
|
@ -91,11 +85,6 @@ namespace MediaBrowser.Common
|
|||
/// </summary>
|
||||
void NotifyPendingRestart();
|
||||
|
||||
/// <summary>
|
||||
/// Restarts this instance.
|
||||
/// </summary>
|
||||
void Restart();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the exports.
|
||||
/// </summary>
|
||||
|
@ -127,11 +116,6 @@ namespace MediaBrowser.Common
|
|||
/// <returns>``0.</returns>
|
||||
T Resolve<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Shuts down.
|
||||
/// </summary>
|
||||
void Shutdown();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes this instance.
|
||||
/// </summary>
|
||||
|
|
|
@ -38,10 +38,6 @@
|
|||
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<CodeAnalysisTreatWarningsAsErrors>false</CodeAnalysisTreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
|
||||
<!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
|
||||
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user