aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/codeql-analysis.yml8
-rw-r--r--.github/workflows/commands.yml4
-rw-r--r--.github/workflows/openapi.yml4
-rw-r--r--.github/workflows/repo-bump-version.yaml4
-rw-r--r--Directory.Packages.props11
-rw-r--r--Dockerfile2
-rw-r--r--Dockerfile.arm2
-rw-r--r--Dockerfile.arm642
-rw-r--r--Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs20
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs136
-rw-r--r--Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs11
-rw-r--r--Emby.Server.Implementations/IO/FileRefresher.cs9
-rw-r--r--Emby.Server.Implementations/IO/LibraryMonitor.cs68
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs37
-rw-r--r--Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs11
-rw-r--r--Emby.Server.Implementations/Library/IgnorePatterns.cs4
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs27
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs9
-rw-r--r--Emby.Server.Implementations/Localization/Core/as.json44
-rw-r--r--Emby.Server.Implementations/Localization/Core/chr.json52
-rw-r--r--Emby.Server.Implementations/Localization/Core/da.json48
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/kn.json122
-rw-r--r--Emby.Server.Implementations/Localization/Core/ms.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/zu.json11
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs5
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManager.cs22
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs146
-rw-r--r--Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs119
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs155
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs7
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs13
-rw-r--r--Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs3
-rw-r--r--Jellyfin.Data/Enums/PersonKind.cs36
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs10
-rw-r--r--Jellyfin.Server/CoreAppHost.cs6
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs1
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs3
-rw-r--r--Jellyfin.Server/Program.cs79
-rw-r--r--MediaBrowser.Common/IApplicationHost.cs9
-rw-r--r--MediaBrowser.Common/Plugins/IPluginManager.cs5
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs42
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs9
-rw-r--r--MediaBrowser.Controller/Playlists/IPlaylistManager.cs6
-rw-r--r--MediaBrowser.Controller/Resolvers/IItemResolver.cs2
-rw-r--r--MediaBrowser.Controller/Resolvers/ItemResolver.cs6
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs14
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs1
-rw-r--r--MediaBrowser.Model/IO/IFileSystem.cs9
-rw-r--r--debian/jellyfin.init1
-rw-r--r--fedora/jellyfin.spec1
-rw-r--r--jellyfin.ruleset16
-rw-r--r--src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj6
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaEncoder.cs7
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaHelper.cs8
-rw-r--r--src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs4
-rw-r--r--src/Jellyfin.Drawing/Jellyfin.Drawing.csproj6
-rw-r--r--src/Jellyfin.Extensions/Jellyfin.Extensions.csproj4
-rw-r--r--src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj4
-rw-r--r--src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj4
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs1
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs3
64 files changed, 753 insertions, 678 deletions
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 14ba3995d..5dc38e188 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -20,18 +20,18 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+ uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
- 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@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # v2.21.8
+ uses: github/codeql-action/init@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # v2.21.8
+ uses: github/codeql-action/autobuild@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # v2.21.8
+ uses: github/codeql-action/analyze@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9
diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml
index 02aeefdbd..ba7883a73 100644
--- a/.github/workflows/commands.yml
+++ b/.github/workflows/commands.yml
@@ -24,7 +24,7 @@ jobs:
reactions: '+1'
- name: Checkout the latest code
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+ uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
@@ -51,7 +51,7 @@ jobs:
reactions: eyes
- name: Checkout the latest code
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+ uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
with:
token: ${{ secrets.JF_BOT_TOKEN }}
fetch-depth: 0
diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml
index 72f1b1d1f..693f98d16 100644
--- a/.github/workflows/openapi.yml
+++ b/.github/workflows/openapi.yml
@@ -14,7 +14,7 @@ jobs:
permissions: read-all
steps:
- name: Checkout repository
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+ uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
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@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
+ uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
with:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
diff --git a/.github/workflows/repo-bump-version.yaml b/.github/workflows/repo-bump-version.yaml
index 1713b484d..0ba68dda3 100644
--- a/.github/workflows/repo-bump-version.yaml
+++ b/.github/workflows/repo-bump-version.yaml
@@ -33,7 +33,7 @@ jobs:
yq-version: v4.9.8
- name: Checkout Repository
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
+ uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
with:
ref: ${{ env.TAG_BRANCH }}
@@ -66,7 +66,7 @@ jobs:
NEXT_VERSION: ${{ github.event.inputs.NEXT_VERSION }}
steps:
- name: Checkout Repository
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4
+ uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608 # v4.1.0
with:
ref: ${{ env.TAG_BRANCH }}
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 5351d5aa6..8cf3eae2a 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -19,6 +19,8 @@
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.2" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
+ <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" />
+ <PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="3.6.13" />
<PackageVersion Include="LrcParser" Version="2023.524.0" />
@@ -64,14 +66,13 @@
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
- <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.2" />
+ <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.0" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
- <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.6" />
+ <PackageVersion Include="SkiaSharp" Version="2.88.5" />
+ <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.5" />
+ <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" />
<PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" />
- <PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.6" />
- <PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" />
- <PackageVersion Include="SkiaSharp" Version="2.88.6" />
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
diff --git a/Dockerfile b/Dockerfile
index e51d285e1..9be319311 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,7 +4,7 @@
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=7.0
-FROM node:lts-alpine as web-builder
+FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
diff --git a/Dockerfile.arm b/Dockerfile.arm
index 46a3e9b99..e8ec6398e 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -5,7 +5,7 @@
ARG DOTNET_VERSION=7.0
-FROM node:lts-alpine as web-builder
+FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index 4f9d5e1fd..83137ee89 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -5,7 +5,7 @@
ARG DOTNET_VERSION=7.0
-FROM node:lts-alpine as web-builder
+FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index a4deeddb7..a2f38c8c2 100644
--- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
+++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
@@ -8,7 +8,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
@@ -19,14 +18,8 @@ namespace Emby.Server.Implementations.AppBase
/// </summary>
public abstract class BaseConfigurationManager : IConfigurationManager
{
- private readonly IFileSystem _fileSystem;
-
- private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
-
- /// <summary>
- /// The _configuration sync lock.
- /// </summary>
- private readonly object _configurationSyncLock = new object();
+ private readonly ConcurrentDictionary<string, object> _configurations = new();
+ private readonly object _configurationSyncLock = new();
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
@@ -42,12 +35,13 @@ namespace Emby.Server.Implementations.AppBase
/// <param name="applicationPaths">The application paths.</param>
/// <param name="loggerFactory">The logger factory.</param>
/// <param name="xmlSerializer">The XML serializer.</param>
- /// <param name="fileSystem">The file system.</param>
- protected BaseConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem)
+ protected BaseConfigurationManager(
+ IApplicationPaths applicationPaths,
+ ILoggerFactory loggerFactory,
+ IXmlSerializer xmlSerializer)
{
CommonApplicationPaths = applicationPaths;
XmlSerializer = xmlSerializer;
- _fileSystem = fileSystem;
Logger = loggerFactory.CreateLogger<BaseConfigurationManager>();
UpdateCachePath();
@@ -272,7 +266,7 @@ namespace Emby.Server.Implementations.AppBase
{
var file = Path.Combine(path, Guid.NewGuid().ToString());
File.WriteAllText(file, string.Empty);
- _fileSystem.DeleteFile(file);
+ File.Delete(file);
}
private string GetConfigurationFile(string key)
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 8b13ccada..8518b1352 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -12,7 +12,6 @@ using System.Linq;
using System.Net;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
-using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna;
using Emby.Dlna.Main;
@@ -102,6 +101,7 @@ 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;
@@ -112,7 +112,7 @@ namespace Emby.Server.Implementations
/// <summary>
/// Class CompositionRoot.
/// </summary>
- public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
+ public abstract class ApplicationHost : IServerApplicationHost, IDisposable
{
/// <summary>
/// The disposable parts.
@@ -120,14 +120,12 @@ namespace Emby.Server.Implementations
private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
private readonly DeviceId _deviceId;
- private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer;
private readonly IStartupOptions _startupOptions;
private readonly IPluginManager _pluginManager;
private List<Type> _creatingInstances;
- private ISessionManager _sessionManager;
/// <summary>
/// Gets or sets all concrete types.
@@ -154,10 +152,8 @@ namespace Emby.Server.Implementations
LoggerFactory = loggerFactory;
_startupOptions = options;
_startupConfig = startupConfig;
- _fileSystemManager = new ManagedFileSystem(LoggerFactory.CreateLogger<ManagedFileSystem>(), applicationPaths);
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
- _fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler(_fileSystemManager));
_deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
@@ -165,13 +161,15 @@ namespace Emby.Server.Implementations
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
_xmlSerializer = new MyXmlSerializer();
- ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
+ ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer);
_pluginManager = new PluginManager(
LoggerFactory.CreateLogger<PluginManager>(),
this,
ConfigurationManager.Configuration,
ApplicationPaths.PluginsPath,
ApplicationVersion);
+
+ _disposableParts.TryAdd((PluginManager)_pluginManager, byte.MinValue);
}
/// <summary>
@@ -204,6 +202,9 @@ namespace Emby.Server.Implementations
/// <inheritdoc />
public bool IsShuttingDown { get; private set; }
+ /// <inheritdoc />
+ public bool ShouldRestart { get; private set; }
+
/// <summary>
/// Gets the logger.
/// </summary>
@@ -406,11 +407,9 @@ namespace Emby.Server.Implementations
/// <summary>
/// Runs the startup tasks.
/// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
/// <returns><see cref="Task" />.</returns>
- public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
+ public async Task RunStartupTasksAsync()
{
- cancellationToken.ThrowIfCancellationRequested();
Logger.LogInformation("Running startup tasks");
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
@@ -424,8 +423,6 @@ namespace Emby.Server.Implementations
var entryPoints = GetExports<IServerEntryPoint>();
- cancellationToken.ThrowIfCancellationRequested();
-
var stopWatch = new Stopwatch();
stopWatch.Start();
@@ -435,8 +432,6 @@ namespace Emby.Server.Implementations
Logger.LogInformation("Core startup complete");
CoreStartupHasCompleted = true;
- cancellationToken.ThrowIfCancellationRequested();
-
stopWatch.Restart();
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
@@ -509,7 +504,9 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton(_pluginManager);
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
- serviceCollection.AddSingleton(_fileSystemManager);
+ serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
+ serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
+
serviceCollection.AddSingleton<TmdbClientManager>();
serviceCollection.AddSingleton(NetManager);
@@ -633,8 +630,6 @@ namespace Emby.Server.Implementations
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
await localizationManager.LoadAll().ConfigureAwait(false);
- _sessionManager = Resolve<ISessionManager>();
-
SetStaticProperties();
FindParts();
@@ -685,7 +680,7 @@ namespace Emby.Server.Implementations
BaseItem.ProviderManager = Resolve<IProviderManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
BaseItem.ItemRepository = Resolve<IItemRepository>();
- BaseItem.FileSystem = _fileSystemManager;
+ BaseItem.FileSystem = Resolve<IFileSystem>();
BaseItem.UserDataManager = Resolve<IUserDataManager>();
BaseItem.ChannelManager = Resolve<IChannelManager>();
Video.LiveTvManager = Resolve<ILiveTvManager>();
@@ -855,38 +850,24 @@ namespace Emby.Server.Implementations
}
}
- /// <summary>
- /// Restarts this instance.
- /// </summary>
+ /// <inheritdoc />
public void Restart()
{
- if (IsShuttingDown)
- {
- return;
- }
-
- IsShuttingDown = true;
- _pluginManager.UnloadAssemblies();
+ ShouldRestart = true;
+ Shutdown();
+ }
+ /// <inheritdoc />
+ public void Shutdown()
+ {
Task.Run(async () =>
{
- try
- {
- await _sessionManager.SendServerRestartNotification(CancellationToken.None).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error sending server restart notification");
- }
-
- Logger.LogInformation("Calling RestartInternal");
-
- RestartInternal();
+ await Task.Delay(100).ConfigureAwait(false);
+ IsShuttingDown = true;
+ Resolve<IHostApplicationLifetime>().StopApplication();
});
}
- protected abstract void RestartInternal();
-
/// <summary>
/// Gets the composable part assemblies.
/// </summary>
@@ -1065,30 +1046,6 @@ namespace Emby.Server.Implementations
}.ToString().TrimEnd('/');
}
- /// <inheritdoc />
- public async Task Shutdown()
- {
- if (IsShuttingDown)
- {
- return;
- }
-
- IsShuttingDown = true;
-
- try
- {
- await _sessionManager.SendServerShutdownNotification(CancellationToken.None).ConfigureAwait(false);
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error sending server shutdown notification");
- }
-
- ShutdownInternal();
- }
-
- protected abstract void ShutdownInternal();
-
public IEnumerable<Assembly> GetApiPluginAssemblies()
{
var assemblies = _allConcreteTypes
@@ -1152,52 +1109,5 @@ namespace Emby.Server.Implementations
_disposed = true;
}
-
- public async ValueTask DisposeAsync()
- {
- await DisposeAsyncCore().ConfigureAwait(false);
- Dispose(false);
- GC.SuppressFinalize(this);
- }
-
- /// <summary>
- /// Used to perform asynchronous cleanup of managed resources or for cascading calls to <see cref="DisposeAsync"/>.
- /// </summary>
- /// <returns>A ValueTask.</returns>
- protected virtual async ValueTask DisposeAsyncCore()
- {
- var type = GetType();
-
- Logger.LogInformation("Disposing {Type}", type.Name);
-
- foreach (var (part, _) in _disposableParts)
- {
- var partType = part.GetType();
- if (partType == type)
- {
- continue;
- }
-
- Logger.LogInformation("Disposing {Type}", partType.Name);
-
- try
- {
- part.Dispose();
- }
- catch (Exception ex)
- {
- Logger.LogError(ex, "Error disposing {Type}", partType.Name);
- }
- }
-
- if (_sessionManager is not null)
- {
- // used for closing websockets
- foreach (var session in _sessionManager.Sessions)
- {
- await session.DisposeAsync().ConfigureAwait(false);
- }
- }
- }
}
}
diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
index 6b8b1a620..0ee43ce0a 100644
--- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
+++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
@@ -7,7 +7,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
@@ -22,11 +21,13 @@ namespace Emby.Server.Implementations.Configuration
/// Initializes a new instance of the <see cref="ServerConfigurationManager" /> class.
/// </summary>
/// <param name="applicationPaths">The application paths.</param>
- /// <param name="loggerFactory">The paramref name="loggerFactory" factory.</param>
+ /// <param name="loggerFactory">The logger factory.</param>
/// <param name="xmlSerializer">The XML serializer.</param>
- /// <param name="fileSystem">The file system.</param>
- public ServerConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem)
- : base(applicationPaths, loggerFactory, xmlSerializer, fileSystem)
+ public ServerConfigurationManager(
+ IApplicationPaths applicationPaths,
+ ILoggerFactory loggerFactory,
+ IXmlSerializer xmlSerializer)
+ : base(applicationPaths, loggerFactory, xmlSerializer)
{
UpdateMetadataPath();
}
diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs
index 0ad81b653..15b1836eb 100644
--- a/Emby.Server.Implementations/IO/FileRefresher.cs
+++ b/Emby.Server.Implementations/IO/FileRefresher.cs
@@ -85,7 +85,7 @@ namespace Emby.Server.Implementations.IO
}
}
- public void ResetPath(string path, string affectedFile)
+ public void ResetPath(string path, string? affectedFile)
{
lock (_timerLock)
{
@@ -148,13 +148,6 @@ namespace Emby.Server.Implementations.IO
{
item.ChangedExternally();
}
- catch (IOException ex)
- {
- // For now swallow and log.
- // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable)
- // Should we remove it from it's parent?
- _logger.LogError(ex, "Error refreshing {Name}", item.Name);
- }
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing {Name}", item.Name);
diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs
index f67a02be8..dde38906f 100644
--- a/Emby.Server.Implementations/IO/LibraryMonitor.cs
+++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -160,7 +158,7 @@ namespace Emby.Server.Implementations.IO
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
- private void OnLibraryManagerItemRemoved(object sender, ItemChangeEventArgs e)
+ private void OnLibraryManagerItemRemoved(object? sender, ItemChangeEventArgs e)
{
if (e.Parent is AggregateFolder)
{
@@ -173,7 +171,7 @@ namespace Emby.Server.Implementations.IO
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="ItemChangeEventArgs"/> instance containing the event data.</param>
- private void OnLibraryManagerItemAdded(object sender, ItemChangeEventArgs e)
+ private void OnLibraryManagerItemAdded(object? sender, ItemChangeEventArgs e)
{
if (e.Parent is AggregateFolder)
{
@@ -189,19 +187,28 @@ namespace Emby.Server.Implementations.IO
/// <param name="path">The path.</param>
/// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns>
/// <exception cref="ArgumentNullException"><paramref name="path"/> is <c>null</c>.</exception>
- private static bool ContainsParentFolder(IEnumerable<string> lst, string path)
+ private static bool ContainsParentFolder(IReadOnlyList<string> lst, ReadOnlySpan<char> path)
{
- ArgumentException.ThrowIfNullOrEmpty(path);
+ if (path.IsEmpty)
+ {
+ throw new ArgumentException("Path can't be empty", nameof(path));
+ }
path = path.TrimEnd(Path.DirectorySeparatorChar);
- return lst.Any(str =>
+ foreach (var str in lst)
{
// this should be a little quicker than examining each actual parent folder...
- var compare = str.TrimEnd(Path.DirectorySeparatorChar);
+ var compare = str.AsSpan().TrimEnd(Path.DirectorySeparatorChar);
- return path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar);
- });
+ if (path.Equals(compare, StringComparison.OrdinalIgnoreCase)
+ || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar))
+ {
+ return true;
+ }
+ }
+
+ return false;
}
/// <summary>
@@ -349,21 +356,19 @@ namespace Emby.Server.Implementations.IO
{
ArgumentException.ThrowIfNullOrEmpty(path);
- var monitorPath = !IgnorePatterns.ShouldIgnore(path);
+ if (IgnorePatterns.ShouldIgnore(path))
+ {
+ return;
+ }
// Ignore certain files, If the parent of an ignored path has a change event, ignore that too
- if (_tempIgnoredPaths.Keys.Any(i =>
+ foreach (var i in _tempIgnoredPaths.Keys)
{
- if (_fileSystem.AreEqual(i, path))
- {
- _logger.LogDebug("Ignoring change to {Path}", path);
- return true;
- }
-
- if (_fileSystem.ContainsSubPath(i, path))
+ if (_fileSystem.AreEqual(i, path)
+ || _fileSystem.ContainsSubPath(i, path))
{
_logger.LogDebug("Ignoring change to {Path}", path);
- return true;
+ return;
}
// Go up a level
@@ -371,20 +376,11 @@ namespace Emby.Server.Implementations.IO
if (!string.IsNullOrEmpty(parent) && _fileSystem.AreEqual(parent, path))
{
_logger.LogDebug("Ignoring change to {Path}", path);
- return true;
+ return;
}
-
- return false;
- }))
- {
- monitorPath = false;
}
- if (monitorPath)
- {
- // Avoid implicitly captured closure
- CreateRefresher(path);
- }
+ CreateRefresher(path);
}
private void CreateRefresher(string path)
@@ -417,7 +413,8 @@ namespace Emby.Server.Implementations.IO
}
// They are siblings. Rebase the refresher to the parent folder.
- if (string.Equals(parentPath, Path.GetDirectoryName(refresher.Path), StringComparison.Ordinal))
+ if (parentPath is not null
+ && Path.GetDirectoryName(refresher.Path.AsSpan()).Equals(parentPath, StringComparison.Ordinal))
{
refresher.ResetPath(parentPath, path);
return;
@@ -430,8 +427,13 @@ namespace Emby.Server.Implementations.IO
}
}
- private void OnNewRefresherCompleted(object sender, EventArgs e)
+ private void OnNewRefresherCompleted(object? sender, EventArgs e)
{
+ if (sender is null)
+ {
+ return;
+ }
+
var refresher = (FileRefresher)sender;
DisposeRefresher(refresher);
}
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 0ba4a488b..18b00ce0b 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -15,10 +15,6 @@ namespace Emby.Server.Implementations.IO
/// </summary>
public class ManagedFileSystem : IFileSystem
{
- private readonly ILogger<ManagedFileSystem> _logger;
-
- private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
- private readonly string _tempPath;
private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows();
private static readonly char[] _invalidPathCharacters =
{
@@ -29,23 +25,24 @@ namespace Emby.Server.Implementations.IO
(char)31, ':', '*', '?', '\\', '/'
};
+ private readonly ILogger<ManagedFileSystem> _logger;
+ private readonly List<IShortcutHandler> _shortcutHandlers;
+ private readonly string _tempPath;
+
/// <summary>
/// Initializes a new instance of the <see cref="ManagedFileSystem"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger"/> instance to use.</param>
/// <param name="applicationPaths">The <see cref="IApplicationPaths"/> instance to use.</param>
+ /// <param name="shortcutHandlers">the <see cref="IShortcutHandler"/>'s to use.</param>
public ManagedFileSystem(
ILogger<ManagedFileSystem> logger,
- IApplicationPaths applicationPaths)
+ IApplicationPaths applicationPaths,
+ IEnumerable<IShortcutHandler> shortcutHandlers)
{
_logger = logger;
_tempPath = applicationPaths.TempDirectory;
- }
-
- /// <inheritdoc />
- public virtual void AddShortcutHandler(IShortcutHandler handler)
- {
- _shortcutHandlers.Add(handler);
+ _shortcutHandlers = shortcutHandlers.ToList();
}
/// <summary>
@@ -486,24 +483,10 @@ namespace Emby.Server.Implementations.IO
}
/// <inheritdoc />
- public virtual string NormalizePath(string path)
- {
- ArgumentException.ThrowIfNullOrEmpty(path);
-
- if (path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase))
- {
- return path;
- }
-
- return Path.TrimEndingDirectorySeparator(path);
- }
-
- /// <inheritdoc />
public virtual bool AreEqual(string path1, string path2)
{
- return string.Equals(
- NormalizePath(path1),
- NormalizePath(path2),
+ return Path.TrimEndingDirectorySeparator(path1).Equals(
+ Path.TrimEndingDirectorySeparator(path2),
_isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
}
diff --git a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
index c2aab3879..5776c7a7c 100644
--- a/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
+++ b/Emby.Server.Implementations/IO/MbLinkShortcutHandler.cs
@@ -8,24 +8,17 @@ namespace Emby.Server.Implementations.IO
{
public class MbLinkShortcutHandler : IShortcutHandler
{
- private readonly IFileSystem _fileSystem;
-
- public MbLinkShortcutHandler(IFileSystem fileSystem)
- {
- _fileSystem = fileSystem;
- }
-
public string Extension => ".mblink";
public string? Resolve(string shortcutPath)
{
ArgumentException.ThrowIfNullOrEmpty(shortcutPath);
- if (string.Equals(Path.GetExtension(shortcutPath), ".mblink", StringComparison.OrdinalIgnoreCase))
+ if (Path.GetExtension(shortcutPath.AsSpan()).Equals(".mblink", StringComparison.OrdinalIgnoreCase))
{
var path = File.ReadAllText(shortcutPath);
- return _fileSystem.NormalizePath(path);
+ return Path.TrimEndingDirectorySeparator(path);
}
return null;
diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs
index 5384c04b3..cf6fc1845 100644
--- a/Emby.Server.Implementations/Library/IgnorePatterns.cs
+++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs
@@ -89,6 +89,10 @@ namespace Emby.Server.Implementations.Library
// bts sync files
"**/*.bts",
"**/*.sync",
+
+ // zfs
+ "**/.zfs/**",
+ "**/.zfs"
};
private static readonly GlobOptions _globOptions = new GlobOptions
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 808cedd67..b0a4a4151 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -608,7 +608,7 @@ namespace Emby.Server.Implementations.Library
var originalList = paths.ToList();
var list = originalList.Where(i => i.IsDirectory)
- .Select(i => _fileSystem.NormalizePath(i.FullName))
+ .Select(i => Path.TrimEndingDirectorySeparator(i.FullName))
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
index 9026160ff..b77c6b204 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -25,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
private readonly NamingOptions _namingOptions;
private readonly IDirectoryService _directoryService;
- private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ private static readonly string[] _ignoreFiles = new[]
{
"folder",
"thumb",
@@ -56,7 +54,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>Trailer.</returns>
- protected override Photo Resolve(ItemResolveArgs args)
+ protected override Photo? Resolve(ItemResolveArgs args)
{
if (!args.IsDirectory)
{
@@ -68,10 +66,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
if (IsImageFile(args.Path, _imageProcessor))
{
- var filename = Path.GetFileNameWithoutExtension(args.Path);
+ var filename = Path.GetFileNameWithoutExtension(args.Path.AsSpan());
// Make sure the image doesn't belong to a video file
- var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path));
+ var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path)
+ ?? throw new InvalidOperationException("Path can't be a root directory."));
foreach (var file in files)
{
@@ -92,32 +91,32 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null;
}
- internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, string imageFilename)
+ internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, ReadOnlySpan<char> imageFilename)
{
return VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename);
}
- internal static bool IsOwnedByResolvedMedia(string file, string imageFilename)
+ internal static bool IsOwnedByResolvedMedia(ReadOnlySpan<char> file, ReadOnlySpan<char> imageFilename)
=> imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
{
ArgumentNullException.ThrowIfNull(path);
- var filename = Path.GetFileNameWithoutExtension(path);
-
- if (_ignoreFiles.Contains(filename))
+ var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
+ if (!imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
return false;
}
- if (_ignoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1))
+ var filename = Path.GetFileNameWithoutExtension(path);
+
+ if (_ignoreFiles.Any(i => filename.StartsWith(i, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
- string extension = Path.GetExtension(path).TrimStart('.');
- return imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase);
+ return true;
}
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index df9101f48..341782d9d 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -94,14 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
{
var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine);
- if (string.IsNullOrWhiteSpace(channel.Id))
- {
- channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
- }
- else
- {
- channel.Id = channelIdPrefix + channel.Id.GetMD5().ToString("N", CultureInfo.InvariantCulture);
- }
+ channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
channel.Path = trimmedLine;
channels.Add(channel);
diff --git a/Emby.Server.Implementations/Localization/Core/as.json b/Emby.Server.Implementations/Localization/Core/as.json
index 0967ef424..7c7dd26e9 100644
--- a/Emby.Server.Implementations/Localization/Core/as.json
+++ b/Emby.Server.Implementations/Localization/Core/as.json
@@ -1 +1,43 @@
-{}
+{
+ "Albums": "এলবাম",
+ "Application": "আবেদন",
+ "AppDeviceValues": "এপ্‌: {0}, ডিভাইচ: {1}",
+ "Artists": "শিল্পী",
+ "Channels": "চেনেলস",
+ "Default": "ডিফল্ট",
+ "AuthenticationSucceededWithUserName": "{0} সফলভাবে প্রমাণিত",
+ "Books": "পুস্তক",
+ "Movies": "চলচ্চিত্ৰ",
+ "CameraImageUploadedFrom": "একটি নতুন ক্যামেরা চিত্র আপলোড করা হয়েছে {0}",
+ "Collections": "সংগ্রহ",
+ "HeaderFavoriteShows": "প্রিয় শোসমূহ",
+ "Latest": "শেহতীয়া",
+ "MessageApplicationUpdated": "জেলিফিন চাইভাৰ আপডেট কৰা হৈছে",
+ "MixedContent": "মিশ্ৰিত সমগ্ৰতা",
+ "NewVersionIsAvailable": "ডাউনলোড কৰিবলৈ জেলিফিন চাইভাৰৰ এটা নতুন সংস্কৰণ উপলব্ধ আছে.",
+ "NotificationOptionCameraImageUploaded": "কেমেৰাৰ চিত্ৰ আপল'ড কৰা হ'ল",
+ "External": "বাহ্যিক",
+ "Favorites": "পছন্দসই",
+ "Folders": "ফোল্ডাৰ",
+ "Forced": "বলপূর্বক",
+ "Genres": "শ্রেণী",
+ "HeaderAlbumArtists": "অ্যালবাম শিল্পী",
+ "HeaderContinueWatching": "দেখা চালিয়ে যান",
+ "FailedLoginAttemptWithUserName": "লগইন ব্যর্থ চেষ্টা কৰা হৈছে থেকে {0}",
+ "HeaderFavoriteAlbums": "প্রিয় অ্যালবামসমূহ",
+ "HeaderFavoriteArtists": "প্রিয় শিল্পীসমূহ",
+ "HeaderFavoriteEpisodes": "প্রিয় পর্বসমূহ",
+ "HeaderFavoriteSongs": "প্ৰিয় গীত",
+ "HeaderLiveTV": "প্ৰতিবেদন টিভি",
+ "HeaderNextUp": "পৰৱৰ্তী অংশ",
+ "HeaderRecordingGroups": "অলংকৰণ গোষ্ঠীসমূহ",
+ "HearingImpaired": "শ্ৰবণ অক্ষম",
+ "HomeVideos": "ঘৰৰ ভিডিঅ'সমূহ",
+ "Inherit": "উত্তপ্ত কৰা",
+ "MessageServerConfigurationUpdated": "চাইভাৰ কনফিগাৰেশ্যন আপডেট কৰা হৈছে",
+ "NotificationOptionApplicationUpdateAvailable": "অ্যাপ্লিকেশ্যন আপডেট উপলব্ধ",
+ "NotificationOptionApplicationUpdateInstalled": "অ্যাপ্লিকেশ্যন আপডেট ইনষ্টল কৰা হ'ল",
+ "NotificationOptionAudioPlayback": "অডিঅ' প্লেবেক আৰম্ভ হ'ল",
+ "NotificationOptionAudioPlaybackStopped": "অডিঅ' প্লেবেক আঁতৰ হ'ল",
+ "NotificationOptionInstallationFailed": "ইনষ্টলেশ্যন ব্যৰ্থতা"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/chr.json b/Emby.Server.Implementations/Localization/Core/chr.json
new file mode 100644
index 000000000..85d1f4c88
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/chr.json
@@ -0,0 +1,52 @@
+{
+ "ChapterNameValue": "Didanedi {0}",
+ "HeaderAlbumArtists": "Didanidanolisgisgi",
+ "HeaderFavoriteAlbums": "Dvganidi didanidisgisgi",
+ "HeaderLiveTV": "Anigadi didanidisgosgi",
+ "HeaderRecordingGroups": "Didanisquodiisgisgi",
+ "HomeVideos": "Diganadi dinagadisgisgi",
+ "Inherit": "Anigwe",
+ "MessageApplicationUpdatedTo": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe anigadi {0}",
+ "MixedContent": "Ganinidi dininoladisgisgi",
+ "Movies": "Anidvnisgisgi",
+ "MusicVideos": "Danodisgisgi didanidisgosgi",
+ "NotificationOptionAudioPlayback": "Didanidigwe diganuyisgisgi anigadi",
+ "NotificationOptionInstallationFailed": "Diudvdi anadvnatisgisgi",
+ "NotificationOptionPluginUninstalled": "Ditsigvhnidv anawvdisgisgi",
+ "Albums": "Anigawidaniyv",
+ "Application": "Didanvyi",
+ "Artists": "Dinidaniyi",
+ "AuthenticationSucceededWithUserName": "{0} Sesoquonisdi nagadani",
+ "Books": "Didanedi",
+ "CameraImageUploadedFrom": "Anigawidaniyv nasgi didagwalanvyi {0}",
+ "Channels": "Diganadasgi",
+ "Collections": "Diganadisgi",
+ "Default": "Dinadi",
+ "DeviceOfflineWithName": "{0} Aniyvolehvi nasgi",
+ "External": "Amohdi",
+ "Favorites": "Nvdayelvdisgi",
+ "Folders": "Didanididisgi",
+ "Forced": "Ganedi",
+ "Genres": "Diganadisgi",
+ "HeaderContinueWatching": "Uwoditsu asdanidisgisgi",
+ "HeaderFavoriteArtists": "Dvganidi dinidanolisgisgi",
+ "HeaderFavoriteEpisodes": "Dvganidi didanidilisgadisgisgi",
+ "HeaderFavoriteShows": "Dvganidi didanididanolisgisgi)",
+ "HeaderFavoriteSongs": "Dvganidi danodisgisgi",
+ "HeaderNextUp": "Anidvli uwodoli",
+ "HearingImpaired": "Anitsunidi talunidisgisgi",
+ "ItemAddedWithName": "{0} Dinigwe anididanidisgi",
+ "Latest": "Uwodoli",
+ "MessageApplicationUpdated": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe",
+ "MessageServerConfigurationUpdated": "Sedanidvdi anigadi diganidinonvhi",
+ "Music": "Danodisgisgi",
+ "NameSeasonUnknown": "Tsunita anidvdisgi",
+ "NewVersionIsAvailable": "Danodigwe anigadi Jellyfin Server tsadanidigwe adisdi uwodvdi diganidinonvhi.",
+ "NotificationOptionApplicationUpdateAvailable": "Disisdi tsadanidigwe udvdi",
+ "NotificationOptionApplicationUpdateInstalled": "Disisdi tsadanidigwe digawvdi",
+ "NotificationOptionAudioPlaybackStopped": "Didanidigwe diganuyisgisgi digawvdi",
+ "NotificationOptionCameraImageUploaded": "Asdayi adininisgisgi diganuyisgisgi",
+ "NotificationOptionNewLibraryContent": "Danodisgisgi anigadi digawvdi",
+ "NotificationOptionPluginError": "Ditsigvhnidv anadvnatisgisgi",
+ "NotificationOptionPluginInstalled": "Ditsigvhnidv digawvdi"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index 1b6eecdcf..837172a5b 100644
--- a/Emby.Server.Implementations/Localization/Core/da.json
+++ b/Emby.Server.Implementations/Localization/Core/da.json
@@ -15,13 +15,13 @@
"Favorites": "Favoritter",
"Folders": "Mapper",
"Genres": "Genrer",
- "HeaderAlbumArtists": "Albums kunstnere",
+ "HeaderAlbumArtists": "Albumkunstnere",
"HeaderContinueWatching": "Fortsæt afspilning",
- "HeaderFavoriteAlbums": "Favorit albummer",
- "HeaderFavoriteArtists": "Favorit kunstnere",
- "HeaderFavoriteEpisodes": "Favorit afsnit",
- "HeaderFavoriteShows": "Favorit serier",
- "HeaderFavoriteSongs": "Favorit sange",
+ "HeaderFavoriteAlbums": "Favoritalbummer",
+ "HeaderFavoriteArtists": "Favoritkunstnere",
+ "HeaderFavoriteEpisodes": "Yndlingsafsnit",
+ "HeaderFavoriteShows": "Yndlingsserier",
+ "HeaderFavoriteSongs": "Yndlingssange",
"HeaderLiveTV": "Live-TV",
"HeaderNextUp": "Næste",
"HeaderRecordingGroups": "Optagelsesgrupper",
@@ -34,8 +34,8 @@
"Latest": "Seneste",
"MessageApplicationUpdated": "Jellyfin Server er blevet opdateret",
"MessageApplicationUpdatedTo": "Jellyfin Server er blevet opdateret til {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Server konfiguration sektion {0} er blevet opdateret",
- "MessageServerConfigurationUpdated": "Server konfigurationen er blevet opdateret",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfiguration sektion {0} er blevet opdateret",
+ "MessageServerConfigurationUpdated": "Serverkonfigurationen er blevet opdateret",
"MixedContent": "Blandet indhold",
"Movies": "Film",
"Music": "Musik",
@@ -51,7 +51,7 @@
"NotificationOptionCameraImageUploaded": "Kamerabillede uploadet",
"NotificationOptionInstallationFailed": "Installationen mislykkedes",
"NotificationOptionNewLibraryContent": "Nyt indhold tilføjet",
- "NotificationOptionPluginError": "Plugin fejl",
+ "NotificationOptionPluginError": "Plugin-fejl",
"NotificationOptionPluginInstalled": "Plugin blev installeret",
"NotificationOptionPluginUninstalled": "Plugin blev afinstalleret",
"NotificationOptionPluginUpdateInstalled": "Opdatering til plugin blev installeret",
@@ -92,26 +92,26 @@
"ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
"ValueSpecialEpisodeName": "Special - {0}",
"VersionNumber": "Version {0}",
- "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfigurationen.",
+ "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
"TaskDownloadMissingSubtitles": "Hent manglende undertekster",
"TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.",
"TaskUpdatePlugins": "Opdater Plugins",
- "TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gamle.",
- "TaskCleanLogs": "Ryd Log mappe",
- "TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdateret metadata.",
- "TaskRefreshLibrary": "Scan Medie Bibliotek",
- "TaskCleanCacheDescription": "Sletter cache filer som systemet ikke længere bruger.",
- "TaskCleanCache": "Ryd Cache mappe",
- "TasksChannelsCategory": "Internet Kanaler",
+ "TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.",
+ "TaskCleanLogs": "Ryd Log-mappe",
+ "TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.",
+ "TaskRefreshLibrary": "Scan Mediebibliotek",
+ "TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.",
+ "TaskCleanCache": "Ryd Cache-mappe",
+ "TasksChannelsCategory": "Internetkanaler",
"TasksApplicationCategory": "Applikation",
"TasksLibraryCategory": "Bibliotek",
"TasksMaintenanceCategory": "Vedligeholdelse",
- "TaskRefreshChapterImages": "Udtræk kapitel billeder",
- "TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.",
- "TaskRefreshChannelsDescription": "Opdater internet kanal information.",
+ "TaskRefreshChapterImages": "Udtræk kapitelbilleder",
+ "TaskRefreshChapterImagesDescription": "Laver miniaturebilleder for videoer, der har kapitler.",
+ "TaskRefreshChannelsDescription": "Opdaterer information for internetkanal.",
"TaskRefreshChannels": "Opdater Kanaler",
- "TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end 1 dag gammel.",
- "TaskCleanTranscode": "Tøm Transcode mappen",
+ "TaskCleanTranscodeDescription": "Fjerner transcode-filer, som er mere end 1 dag gammel.",
+ "TaskCleanTranscode": "Tøm Transcode-mappen",
"TaskRefreshPeople": "Opdater Personer",
"TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.",
"TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.",
@@ -121,8 +121,8 @@
"Default": "Standard",
"TaskOptimizeDatabaseDescription": "Komprimerer databasen og frigør plads. Denne handling køres efter at have scannet mediebiblioteket, eller efter at have lavet ændringer til databasen, for at højne ydeevnen.",
"TaskOptimizeDatabase": "Optimér database",
- "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan tage lang tid.",
- "TaskKeyframeExtractor": "Nøglebillede udtræk",
+ "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS-playlister. Denne opgave kan tage lang tid.",
+ "TaskKeyframeExtractor": "Udtræk af nøglebillede",
"External": "Ekstern",
"HearingImpaired": "Hørehæmmet"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index f5636a0af..4c56f789d 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -3,9 +3,9 @@
"AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
"Application": "Aplicación",
"Artists": "Artistas",
- "AuthenticationSucceededWithUserName": "{0} identificado correctamente",
+ "AuthenticationSucceededWithUserName": "{0} autenticado correctamente",
"Books": "Libros",
- "CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}",
+ "CameraImageUploadedFrom": "Se ha subido una nueva imagen por cámara desde {0}",
"Channels": "Canales",
"ChapterNameValue": "Capítulo {0}",
"Collections": "Colecciones",
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index 4877bcd7a..a2b429dcd 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -105,8 +105,8 @@
"TaskRefreshPeople": "Actualiser les acteurs",
"TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
- "TaskRefreshLibraryDescription": "Scanne votre médiathèque pour trouver les nouveaux fichiers et actualise les métadonnées.",
- "TaskRefreshLibrary": "Scanner la médiathèque",
+ "TaskRefreshLibraryDescription": "Analyser sa médiathèque pour trouver les nouveaux fichiers et actualiser les métadonnées.",
+ "TaskRefreshLibrary": "Analyser la médiathèque",
"TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.",
"TaskRefreshChapterImages": "Extraire les images de chapitre",
"TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",
diff --git a/Emby.Server.Implementations/Localization/Core/kn.json b/Emby.Server.Implementations/Localization/Core/kn.json
index 3c8c38ed4..5e2b3756b 100644
--- a/Emby.Server.Implementations/Localization/Core/kn.json
+++ b/Emby.Server.Implementations/Localization/Core/kn.json
@@ -3,5 +3,125 @@
"TaskOptimizeDatabase": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಆಪ್ಟಿಮೈಜ್ ಮಾಡಿ",
"TaskOptimizeDatabaseDescription": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಕಾಂಪ್ಯಾಕ್ಟ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮುಕ್ತ ಜಾಗವನ್ನು ಮೊಟಕುಗೊಳಿಸುತ್ತದೆ. ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿದ ನಂತರ ಈ ಕಾರ್ಯವನ್ನು ನಡೆಸುವುದು ಅಥವಾ ಡೇಟಾಬೇಸ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಸೂಚಿಸುವ ಇತರ ಬದಲಾವಣೆಗಳನ್ನು ಮಾಡುವುದರಿಂದ ಕಾರ್ಯಕ್ಷಮತೆಯನ್ನು ಸುಧಾರಿಸಬಹುದು.",
"TaskKeyframeExtractor": "ಕೀಫ್ರೇಮ್ ಎಕ್ಸ್‌ಟ್ರಾಕ್ಟರ್",
- "TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್‌ಗಳಿಂದ ಕೀಫ್ರೇಮ್‌ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು."
+ "TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್‌ಗಳಿಂದ ಕೀಫ್ರೇಮ್‌ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು.",
+ "ValueHasBeenAddedToLibrary": "{0} ಅನ್ನು ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಗೆ ಸೇರಿಸಲಾಗಿದೆ",
+ "ValueSpecialEpisodeName": "ವಿಶೇಷ - {0}",
+ "TasksLibraryCategory": "ಸಮೊಹ",
+ "TasksApplicationCategory": "ಅಪ್ಲಿಕೇಶನ್",
+ "TasksChannelsCategory": "ಇಂಟರ್ನೆಟ್ ಚಾನೆಲ್ಗಳು",
+ "TaskCleanCache": "ಕ್ಲೀನ್ ಕ್ಯಾಶ ಡೈರೆಕ್ಟರಿ",
+ "TaskCleanCacheDescription": "ಸಿಸ್ಟಮ್‌ಗೆ ಇನ್ನು ಮುಂದೆ ಅಗತ್ಯವಿಲ್ಲದ ಸಂಗ್ರಹ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+ "TaskRefreshLibrary": "ಸ್ಕ್ಯಾನ್ ಮೀಡಿಯಾ ಲೈಬ್ರರಿ",
+ "UserOfflineFromDevice": "{1} ನಿಂದ {0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
+ "Albums": "ಸಂಪುಟ",
+ "Application": "ಅಪ್ಲಿಕೇಶನ್",
+ "AppDeviceValues": "ಅಪ್ಲಿಕೇಶನ್: {0}, ಸಾಧನ: {1}",
+ "Artists": "ಕಲಾವಿದರು",
+ "AuthenticationSucceededWithUserName": "{0} ಯಶಸ್ವಿಯಾಗಿ ದೃಢೀಕರಿಸಲಾಗಿದೆ",
+ "Books": "ಪುಸ್ತಕಗಳು",
+ "ChapterNameValue": "ಅಧ್ಯಾಯ {0}",
+ "Collections": "ಸಂಗ್ರಹಣೆಗಳು",
+ "Default": "ಪೂರ್ವನಿಯೋಜಿತ",
+ "DeviceOfflineWithName": "{0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
+ "DeviceOnlineWithName": "{0} ಸಂಪರ್ಕಗೊಂಡಿದೆ",
+ "External": "ಹೊರಗಿನ",
+ "FailedLoginAttemptWithUserName": "{0} ರಿಂದ ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ",
+ "Favorites": "ಮೆಚ್ಚಿನವುಗಳು",
+ "Folders": "ಫೋಲ್ಡರ್‌ಗಳು",
+ "Forced": "ಬಲವಂತವಾಗಿ",
+ "Genres": "ಪ್ರಕಾರಗಳು",
+ "HeaderContinueWatching": "ನೋಡುವುದನ್ನು ಮುಂದುವರಿಸಿ",
+ "HeaderFavoriteAlbums": "ಮೆಚ್ಚಿನ ಸಂಪುಟಗಳು",
+ "HeaderFavoriteArtists": "ಮೆಚ್ಚಿನ ಕಲಾವಿದರು",
+ "HeaderFavoriteShows": "ಮೆಚ್ಚಿನ ಪ್ರದರ್ಶನಗಳು",
+ "HeaderFavoriteSongs": "ಮೆಚ್ಚಿನ ಹಾಡುಗಳು",
+ "HeaderLiveTV": "ನೇರ ದೂರದರ್ಶನ",
+ "HeaderNextUp": "ಮುಂದೆ",
+ "HeaderRecordingGroups": "ರೆಕಾರ್ಡಿಂಗ್ ಗುಂಪುಗಳು",
+ "MessageApplicationUpdated": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+ "CameraImageUploadedFrom": "ಹೊಸ ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು {0} ನಿಂದ ಅಪ್‌ಲೋಡ್ ಮಾಡಲಾಗಿದೆ",
+ "Channels": "ಮೂಲಗಳು",
+ "HeaderAlbumArtists": "ಸಂಪುಟ ಕಲಾವಿದರು",
+ "HeaderFavoriteEpisodes": "ಮೆಚ್ಚಿನ ಸಂಚಿಕೆಗಳು",
+ "HearingImpaired": "ಮೂಗ",
+ "ItemAddedWithName": "{0} ಅನ್ನು ಸಂಕಲನಕ್ಕೆ ಸೇರಿಸಲಾಗಿದೆ",
+ "MessageApplicationUpdatedTo": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ",
+ "MessageNamedServerConfigurationUpdatedWithValue": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ವಿಭಾಗ {0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+ "NewVersionIsAvailable": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್‌ನ ಹೊಸ ಆವೃತ್ತಿಯು ಡೌನ್‌ಲೋಡ್‌ಗೆ ಲಭ್ಯವಿದೆ.",
+ "NotificationOptionAudioPlayback": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ",
+ "NotificationOptionCameraImageUploaded": "ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು ಅಪ್ಲೋಡ್ ಮಾಡಲಾಗಿದೆ",
+ "NotificationOptionPluginUninstalled": "ಪ್ಲಗಿನ್ ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ",
+ "NotificationOptionUserLockedOut": "ಬಳಕೆದಾರರು ಲಾಕ್ ಔಟ್ ಆಗಿದ್ದಾರೆ",
+ "NotificationOptionVideoPlaybackStopped": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ",
+ "PluginUninstalledWithName": "{0} ಅನ್ನು ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ",
+ "ScheduledTaskFailedWithName": "{0} ವಿಫಲವಾಗಿದೆ",
+ "ScheduledTaskStartedWithName": "{0} ಪ್ರಾರಂಭವಾಯಿತು",
+ "ServerNameNeedsToBeRestarted": "{0} ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಬೇಕಾಗಿದೆ",
+ "UserCreatedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ರಚಿಸಲಾಗಿದೆ",
+ "UserLockedOutWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಲಾಕ್ ಮಾಡಲಾಗಿದೆ",
+ "UserOnlineFromDevice": "{1} ನಿಂದ {0} ಆನ್‌ಲೈನ್‌ನಲ್ಲಿದೆ",
+ "UserPasswordChangedWithName": "{0} ಬಳಕೆದಾರರಿಗಾಗಿ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ಬದಲಾಯಿಸಲಾಗಿದೆ",
+ "UserPolicyUpdatedWithName": "ಬಳಕೆದಾರರ ನೀತಿಯನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ",
+ "UserStartedPlayingItemWithValues": "{2} ರಂದು {0} ಆಡುತ್ತಿದೆ {1}",
+ "UserStoppedPlayingItemWithValues": "{0} ಅವರು {1} ಅನ್ನು {2} ನಲ್ಲಿ ಆಡುವುದನ್ನು ಮುಗಿಸಿದ್ದಾರೆ",
+ "VersionNumber": "ಆವೃತ್ತಿ {0}",
+ "TasksMaintenanceCategory": "ನಿರ್ವಹಣೆ",
+ "TaskCleanActivityLog": "ಕ್ಲೀನ್ ಚಟುವಟಿಕೆ ಲಾಗ್",
+ "TaskCleanActivityLogDescription": "ಕಾನ್ಫಿಗರ್ ಮಾಡಿದ ವಯಸ್ಸಿಗಿಂತ ಹಳೆಯದಾದ ಚಟುವಟಿಕೆ ಲಾಗ್ ನಮೂದುಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+ "TaskRefreshChapterImages": "ಅಧ್ಯಾಯ ಚಿತ್ರಗಳನ್ನು ಹೊರತೆಗೆಯಿರಿ",
+ "TaskRefreshChapterImagesDescription": "ಅಧ್ಯಾಯಗಳನ್ನು ಹೊಂದಿರುವ ವೀಡಿಯೊಗಳಿಗಾಗಿ ಥಂಬ್‌ನೇಲ್‌ಗಳನ್ನು ರಚಿಸುತ್ತದೆ.",
+ "TaskRefreshLibraryDescription": "ಹೊಸ ಫೈಲ್‌ಗಳಿಗಾಗಿ ನಿಮ್ಮ ಮೀಡಿಯಾ ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮೆಟಾಡೇಟಾವನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ.",
+ "TaskCleanLogsDescription": "{0} ದಿನಗಳಿಗಿಂತ ಹಳೆಯದಾದ ಲಾಗ್ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+ "TaskUpdatePluginsDescription": "ಸ್ವಯಂಚಾಲಿತವಾಗಿ ನವೀಕರಿಸಲು ಕಾನ್ಫಿಗರ್ ಮಾಡಲಾದ ಪ್ಲಗಿನ್‌ಗಳಿಗಾಗಿ ನವೀಕರಣಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಸ್ಥಾಪಿಸುತ್ತದೆ.",
+ "TaskCleanTranscodeDescription": "ಒಂದು ದಿನಕ್ಕಿಂತ ಹಳೆಯದಾದ ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+ "TaskDownloadMissingSubtitles": "ಕಾಣೆಯಾದ ಉಪಶೀರ್ಷಿಕೆಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಿ",
+ "Shows": "ಧಾರವಾಹಿಗಳು",
+ "Songs": "ಹಾಡುಗಳು",
+ "StartupEmbyServerIsLoading": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಲೋಡ್ ಆಗುತ್ತಿದೆ. ದಯವಿಟ್ಟು ಸ್ವಲ್ಪ ಸಮಯದ ನಂತರ ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.",
+ "UserDeletedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಅಳಿಸಲಾಗಿದೆ",
+ "UserDownloadingItemWithValues": "{0} ಡೌನ್‌ಲೋಡ್ ಆಗುತ್ತಿದೆ {1}",
+ "SubtitleDownloadFailureFromForItem": "ಉಪಶೀರ್ಷಿಕೆಗಳು {0} ನಿಂದ {1} ಗಾಗಿ ಡೌನ್‌ಲೋಡ್ ಮಾಡಲು ವಿಫಲವಾಗಿವೆ",
+ "Sync": "ಹೊಂದಿಕೆ",
+ "System": "ವ್ಯವಸ್ಥೆ",
+ "TvShows": "ದೂರದರ್ಶನ ಕಾರ್ಯಕ್ರಮಗಳು",
+ "Undefined": "ವ್ಯಾಖ್ಯಾನಿಸಲಾಗಿಲ್ಲ",
+ "User": "ಬಳಕೆದಾರ",
+ "HomeVideos": "ಮುಖಪುಟ ವೀಡಿಯೊಗಳು",
+ "Inherit": "ಪಾರಂಪರ್ಯವಾಗಿ",
+ "ItemRemovedWithName": "{0} ಅನ್ನು ಸಂಕಲನದಿಂದ ತೆಗೆದುಹಾಕಲಾಗಿದೆ",
+ "LabelIpAddressValue": "IP ವಿಳಾಸ: {0}",
+ "LabelRunningTimeValue": "ಅವಧಿ: {0}",
+ "Latest": "ಹೊಸದಾದ",
+ "MessageServerConfigurationUpdated": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+ "MixedContent": "ಮಿಶ್ರ ವಿಷಯ",
+ "Movies": "ಚಲನಚಿತ್ರಗಳು",
+ "Music": "ಸಂಗೀತ",
+ "MusicVideos": "ಸಂಗೀತ ವೀಡಿಯೊಗಳು",
+ "NameInstallFailed": "{0} ಸ್ಥಾಪನೆ ವಿಫಲವಾಗಿದೆ",
+ "NameSeasonNumber": "ಸೀಸನ್ {0}",
+ "NameSeasonUnknown": "ಸೀಸನ್ ತಿಳಿದಿಲ್ಲ",
+ "NotificationOptionApplicationUpdateAvailable": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣ ಲಭ್ಯವಿದೆ",
+ "NotificationOptionApplicationUpdateInstalled": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
+ "NotificationOptionAudioPlaybackStopped": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ",
+ "NotificationOptionInstallationFailed": "ಸ್ಥಾಪನ ವೈಫಲ್ಯ",
+ "NotificationOptionNewLibraryContent": "ಹೊಸ ವಿಷಯವನ್ನು ಒಳಗೊಂಡಿದೆ",
+ "NotificationOptionPluginError": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
+ "NotificationOptionPluginInstalled": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
+ "NotificationOptionPluginUpdateInstalled": "ಪ್ಲಗಿನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
+ "NotificationOptionServerRestartRequired": "ಸರ್ವರ್ ಮರುಪ್ರಾರಂಭದ ಅಗತ್ಯವಿದೆ",
+ "NotificationOptionTaskFailed": "ನಿಗದಿತ ಕಾರ್ಯ ವೈಫಲ್ಯ",
+ "NotificationOptionVideoPlayback": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ",
+ "Photos": "ಚಿತ್ರಗಳು",
+ "Playlists": "ಪ್ಲೇಪಟ್ಟಿಗಳು",
+ "Plugin": "ಪ್ಲಗಿನ್",
+ "PluginInstalledWithName": "{0} ಅನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
+ "PluginUpdatedWithName": "{0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+ "ProviderValue": "ಒದಗಿಸುವವರು: {0}",
+ "TaskCleanLogs": "ಕ್ಲೀನ್ ಲಾಗ್ ಡೈರೆಕ್ಟರಿ",
+ "TaskRefreshPeople": "ಜನರನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
+ "TaskRefreshPeopleDescription": "ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಯಲ್ಲಿ ನಟರು ಮತ್ತು ನಿರ್ದೇಶಕರಿಗಾಗಿ ಮೆಟಾಡೇಟಾವನ್ನು ನವೀಕರಿಸಿ.",
+ "TaskUpdatePlugins": "ಪ್ಲಗಿನ್‌ಗಳನ್ನು ನವೀಕರಿಸಿ",
+ "TaskCleanTranscode": "ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಡೈರೆಕ್ಟರಿಯನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ",
+ "TaskRefreshChannels": "ಚಾನಲ್‌ಗಳನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
+ "TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index 904443bea..a07222975 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -1,5 +1,5 @@
{
- "Albums": "Albums",
+ "Albums": "Album",
"AppDeviceValues": "Apl: {0}, Peranti: {1}",
"Application": "Aplikasi",
"Artists": "Artis-artis",
diff --git a/Emby.Server.Implementations/Localization/Core/zu.json b/Emby.Server.Implementations/Localization/Core/zu.json
index b5f4b920f..aa056d449 100644
--- a/Emby.Server.Implementations/Localization/Core/zu.json
+++ b/Emby.Server.Implementations/Localization/Core/zu.json
@@ -25,5 +25,14 @@
"Channels": "Amashaneli",
"Books": "Izincwadi",
"Artists": "Abadlali",
- "Albums": "Ama-albhamu"
+ "Albums": "Ama-albhamu",
+ "CameraImageUploadedFrom": "Kulandelayo lwesithonjana sekhamera selithunyelwe kusuka ku {0}",
+ "HeaderFavoriteArtists": "Abasethi Abathandekayo",
+ "HeaderFavoriteEpisodes": "Izilimi Ezithandekayo",
+ "HeaderFavoriteShows": "Izisho Ezithandekayo",
+ "External": "Kwezifungo",
+ "FailedLoginAttemptWithUserName": "Ukushayiswa kwesithombe sokungena okungekho {0}",
+ "HeaderContinueWatching": "Buyela Ukubona",
+ "HeaderFavoriteAlbums": "Izimpahla Ezithandwayo",
+ "HeaderAlbumArtists": "Abasethi wenkulumo"
}
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 702f8d45b..0cb450cdf 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -518,6 +518,11 @@ namespace Emby.Server.Implementations.Playlists
return relativePath;
}
+ public Folder GetPlaylistsFolder()
+ {
+ return GetPlaylistsFolder(Guid.Empty);
+ }
+
public Folder GetPlaylistsFolder(Guid userId)
{
const string TypeName = "PlaylistsFolder";
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index 1303012e1..d7189ef0c 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Data;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -11,7 +10,6 @@ using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Emby.Server.Implementations.Library;
-using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Common;
@@ -30,7 +28,7 @@ namespace Emby.Server.Implementations.Plugins
/// <summary>
/// Defines the <see cref="PluginManager" />.
/// </summary>
- public class PluginManager : IPluginManager
+ public sealed class PluginManager : IPluginManager, IDisposable
{
private const string MetafileName = "meta.json";
@@ -191,15 +189,6 @@ namespace Emby.Server.Implementations.Plugins
}
}
- /// <inheritdoc />
- public void UnloadAssemblies()
- {
- foreach (var assemblyLoadContext in _assemblyLoadContexts)
- {
- assemblyLoadContext.Unload();
- }
- }
-
/// <summary>
/// Creates all the plugin instances.
/// </summary>
@@ -441,6 +430,15 @@ namespace Emby.Server.Implementations.Plugins
return SaveManifest(manifest, path);
}
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ foreach (var assemblyLoadContext in _assemblyLoadContexts)
+ {
+ assemblyLoadContext.Unload();
+ }
+ }
+
/// <summary>
/// Reconciles the manifest against any properties that exist locally in a pre-packaged meta.json found at the path.
/// If no file is found, no reconciliation occurs.
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
new file mode 100644
index 000000000..acd4bf905
--- /dev/null
+++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionAndPlaylistPathsTask.cs
@@ -0,0 +1,146 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Controller.Collections;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Playlists;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Globalization;
+using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Tasks;
+using Microsoft.Extensions.Logging;
+
+namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
+
+/// <summary>
+/// Deletes path references from collections and playlists that no longer exists.
+/// </summary>
+public class CleanupCollectionAndPlaylistPathsTask : IScheduledTask
+{
+ private readonly ILocalizationManager _localization;
+ private readonly ICollectionManager _collectionManager;
+ private readonly IPlaylistManager _playlistManager;
+ private readonly ILogger<CleanupCollectionAndPlaylistPathsTask> _logger;
+ private readonly IProviderManager _providerManager;
+ private readonly IFileSystem _fileSystem;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="CleanupCollectionAndPlaylistPathsTask"/> class.
+ /// </summary>
+ /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
+ /// <param name="collectionManager">Instance of the <see cref="ICollectionManager"/> interface.</param>
+ /// <param name="playlistManager">Instance of the <see cref="IPlaylistManager"/> interface.</param>
+ /// <param name="logger">The logger.</param>
+ /// <param name="providerManager">The provider manager.</param>
+ /// <param name="fileSystem">The filesystem.</param>
+ public CleanupCollectionAndPlaylistPathsTask(
+ ILocalizationManager localization,
+ ICollectionManager collectionManager,
+ IPlaylistManager playlistManager,
+ ILogger<CleanupCollectionAndPlaylistPathsTask> logger,
+ IProviderManager providerManager,
+ IFileSystem fileSystem)
+ {
+ _localization = localization;
+ _collectionManager = collectionManager;
+ _playlistManager = playlistManager;
+ _logger = logger;
+ _providerManager = providerManager;
+ _fileSystem = fileSystem;
+ }
+
+ /// <inheritdoc />
+ public string Name => _localization.GetLocalizedString("TaskCleanCollectionsAndPlaylists");
+
+ /// <inheritdoc />
+ public string Key => "CleanCollectionsAndPlaylists";
+
+ /// <inheritdoc />
+ public string Description => _localization.GetLocalizedString("TaskCleanCollectionsAndPlaylistsDescription");
+
+ /// <inheritdoc />
+ public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
+
+ /// <inheritdoc />
+ public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
+ {
+ var collectionsFolder = await _collectionManager.GetCollectionsFolder(false).ConfigureAwait(false);
+ if (collectionsFolder is null)
+ {
+ _logger.LogDebug("There is no collections folder to be found");
+ }
+ else
+ {
+ var collections = collectionsFolder.Children.OfType<BoxSet>().ToArray();
+ _logger.LogDebug("Found {CollectionLength} boxsets", collections.Length);
+
+ for (var index = 0; index < collections.Length; index++)
+ {
+ var collection = collections[index];
+ _logger.LogDebug("Checking boxset {CollectionName}", collection.Name);
+
+ CleanupLinkedChildren(collection, cancellationToken);
+ progress.Report(50D / collections.Length * (index + 1));
+ }
+ }
+
+ var playlistsFolder = _playlistManager.GetPlaylistsFolder();
+ if (playlistsFolder is null)
+ {
+ _logger.LogDebug("There is no playlists folder to be found");
+ return;
+ }
+
+ var playlists = playlistsFolder.Children.OfType<Playlist>().ToArray();
+ _logger.LogDebug("Found {PlaylistLength} playlists", playlists.Length);
+
+ for (var index = 0; index < playlists.Length; index++)
+ {
+ var playlist = playlists[index];
+ _logger.LogDebug("Checking playlist {PlaylistName}", playlist.Name);
+
+ CleanupLinkedChildren(playlist, cancellationToken);
+ progress.Report(50D / playlists.Length * (index + 1));
+ }
+ }
+
+ private void CleanupLinkedChildren<T>(T folder, CancellationToken cancellationToken)
+ where T : Folder
+ {
+ List<LinkedChild>? itemsToRemove = null;
+ foreach (var linkedChild in folder.LinkedChildren)
+ {
+ if (!File.Exists(folder.Path))
+ {
+ _logger.LogInformation("Item in {FolderName} cannot be found at {ItemPath}", folder.Name, linkedChild.Path);
+ (itemsToRemove ??= new List<LinkedChild>()).Add(linkedChild);
+ }
+ }
+
+ if (itemsToRemove is not null)
+ {
+ _logger.LogDebug("Updating {FolderName}", folder.Name);
+ folder.LinkedChildren = folder.LinkedChildren.Except(itemsToRemove).ToArray();
+ folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken);
+
+ _providerManager.QueueRefresh(
+ folder.Id,
+ new MetadataRefreshOptions(new DirectoryService(_fileSystem))
+ {
+ ForceSave = true
+ },
+ RefreshPriority.High);
+ }
+ }
+
+ /// <inheritdoc />
+ public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
+ {
+ return new[] { new TaskTriggerInfo() { Type = TaskTriggerInfo.TriggerStartup } };
+ }
+}
diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs
deleted file mode 100644
index f78fc6f97..000000000
--- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanupCollectionPathsTask.cs
+++ /dev/null
@@ -1,119 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using MediaBrowser.Controller.Collections;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Movies;
-using MediaBrowser.Controller.Library;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Globalization;
-using MediaBrowser.Model.IO;
-using MediaBrowser.Model.Tasks;
-using Microsoft.Extensions.Logging;
-
-namespace Emby.Server.Implementations.ScheduledTasks.Tasks;
-
-/// <summary>
-/// Deletes Path references from collections that no longer exists.
-/// </summary>
-public class CleanupCollectionPathsTask : IScheduledTask
-{
- private readonly ILocalizationManager _localization;
- private readonly ICollectionManager _collectionManager;
- private readonly ILogger<CleanupCollectionPathsTask> _logger;
- private readonly IProviderManager _providerManager;
- private readonly IFileSystem _fileSystem;
-
- /// <summary>
- /// Initializes a new instance of the <see cref="CleanupCollectionPathsTask"/> class.
- /// </summary>
- /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
- /// <param name="collectionManager">Instance of the <see cref="ICollectionManager"/> interface.</param>
- /// <param name="logger">The logger.</param>
- /// <param name="providerManager">The provider manager.</param>
- /// <param name="fileSystem">The filesystem.</param>
- public CleanupCollectionPathsTask(
- ILocalizationManager localization,
- ICollectionManager collectionManager,
- ILogger<CleanupCollectionPathsTask> logger,
- IProviderManager providerManager,
- IFileSystem fileSystem)
- {
- _localization = localization;
- _collectionManager = collectionManager;
- _logger = logger;
- _providerManager = providerManager;
- _fileSystem = fileSystem;
- }
-
- /// <inheritdoc />
- public string Name => _localization.GetLocalizedString("TaskCleanCollections");
-
- /// <inheritdoc />
- public string Key => "CleanCollections";
-
- /// <inheritdoc />
- public string Description => _localization.GetLocalizedString("TaskCleanCollectionsDescription");
-
- /// <inheritdoc />
- public string Category => _localization.GetLocalizedString("TasksMaintenanceCategory");
-
- /// <inheritdoc />
- public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken)
- {
- var collectionsFolder = await _collectionManager.GetCollectionsFolder(false).ConfigureAwait(false);
- if (collectionsFolder is null)
- {
- _logger.LogDebug("There is no collection folder to be found");
- return;
- }
-
- var collections = collectionsFolder.Children.OfType<BoxSet>().ToArray();
- _logger.LogDebug("Found {CollectionLength} Boxsets", collections.Length);
-
- var itemsToRemove = new List<LinkedChild>();
- for (var index = 0; index < collections.Length; index++)
- {
- var collection = collections[index];
- _logger.LogDebug("Check Boxset {CollectionName}", collection.Name);
-
- foreach (var collectionLinkedChild in collection.LinkedChildren)
- {
- if (!File.Exists(collectionLinkedChild.Path))
- {
- _logger.LogInformation("Item in boxset {CollectionName} cannot be found at {ItemPath}", collection.Name, collectionLinkedChild.Path);
- itemsToRemove.Add(collectionLinkedChild);
- }
- }
-
- if (itemsToRemove.Count != 0)
- {
- _logger.LogDebug("Update Boxset {CollectionName}", collection.Name);
- collection.LinkedChildren = collection.LinkedChildren.Except(itemsToRemove).ToArray();
- await collection.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken)
- .ConfigureAwait(false);
-
- _providerManager.QueueRefresh(
- collection.Id,
- new MetadataRefreshOptions(new DirectoryService(_fileSystem))
- {
- ForceSave = true
- },
- RefreshPriority.High);
-
- itemsToRemove.Clear();
- }
-
- progress.Report(100D / collections.Length * (index + 1));
- }
- }
-
- /// <inheritdoc />
- public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
- {
- return new[] { new TaskTriggerInfo() { Type = TaskTriggerInfo.TriggerStartup } };
- }
-}
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index b4a622ccf..e935f7e5e 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -36,6 +36,7 @@ using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
@@ -44,7 +45,7 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// Class SessionManager.
/// </summary>
- public class SessionManager : ISessionManager, IDisposable
+ public sealed class SessionManager : ISessionManager, IAsyncDisposable
{
private readonly IUserDataManager _userDataManager;
private readonly ILogger<SessionManager> _logger;
@@ -57,11 +58,9 @@ namespace Emby.Server.Implementations.Session
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerApplicationHost _appHost;
private readonly IDeviceManager _deviceManager;
-
- /// <summary>
- /// The active connections.
- /// </summary>
- private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections = new(StringComparer.OrdinalIgnoreCase);
+ private readonly CancellationTokenRegistration _shutdownCallback;
+ private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections
+ = new(StringComparer.OrdinalIgnoreCase);
private Timer _idleTimer;
@@ -79,7 +78,8 @@ namespace Emby.Server.Implementations.Session
IImageProcessor imageProcessor,
IServerApplicationHost appHost,
IDeviceManager deviceManager,
- IMediaSourceManager mediaSourceManager)
+ IMediaSourceManager mediaSourceManager,
+ IHostApplicationLifetime hostApplicationLifetime)
{
_logger = logger;
_eventManager = eventManager;
@@ -92,6 +92,7 @@ namespace Emby.Server.Implementations.Session
_appHost = appHost;
_deviceManager = deviceManager;
_mediaSourceManager = mediaSourceManager;
+ _shutdownCallback = hostApplicationLifetime.ApplicationStopping.Register(OnApplicationStopping);
_deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated;
}
@@ -151,36 +152,6 @@ namespace Emby.Server.Implementations.Session
}
}
- /// <inheritdoc />
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- /// <summary>
- /// Releases unmanaged and optionally managed resources.
- /// </summary>
- /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool disposing)
- {
- if (_disposed)
- {
- return;
- }
-
- if (disposing)
- {
- _idleTimer?.Dispose();
- }
-
- _idleTimer = null;
-
- _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
-
- _disposed = true;
- }
-
private void CheckDisposed()
{
if (_disposed)
@@ -980,28 +951,28 @@ namespace Emby.Server.Implementations.Session
private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
{
- bool playedToCompletion = false;
-
- if (!playbackFailed)
+ if (playbackFailed)
{
- var data = _userDataManager.GetUserData(user, item);
-
- if (positionTicks.HasValue)
- {
- playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
- }
- else
- {
- // If the client isn't able to report this, then we'll just have to make an assumption
- data.PlayCount++;
- data.Played = item.SupportsPlayedStatus;
- data.PlaybackPositionTicks = 0;
- playedToCompletion = true;
- }
+ return false;
+ }
- _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None);
+ var data = _userDataManager.GetUserData(user, item);
+ bool playedToCompletion;
+ if (positionTicks.HasValue)
+ {
+ playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
+ }
+ else
+ {
+ // If the client isn't able to report this, then we'll just have to make an assumption
+ data.PlayCount++;
+ data.Played = item.SupportsPlayedStatus;
+ data.PlaybackPositionTicks = 0;
+ playedToCompletion = true;
}
+ _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None);
+
return playedToCompletion;
}
@@ -1331,32 +1302,6 @@ namespace Emby.Server.Implementations.Session
}
/// <summary>
- /// Sends the server shutdown notification.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public Task SendServerShutdownNotification(CancellationToken cancellationToken)
- {
- CheckDisposed();
-
- return SendMessageToSessions(Sessions, SessionMessageType.ServerShuttingDown, string.Empty, cancellationToken);
- }
-
- /// <summary>
- /// Sends the server restart notification.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public Task SendServerRestartNotification(CancellationToken cancellationToken)
- {
- CheckDisposed();
-
- _logger.LogDebug("Beginning SendServerRestartNotification");
-
- return SendMessageToSessions(Sessions, SessionMessageType.ServerRestarting, string.Empty, cancellationToken);
- }
-
- /// <summary>
/// Adds the additional user.
/// </summary>
/// <param name="sessionId">The session identifier.</param>
@@ -1833,5 +1778,53 @@ namespace Emby.Server.Implementations.Session
return SendMessageToSessions(sessions, name, data, cancellationToken);
}
+
+ /// <inheritdoc />
+ public async ValueTask DisposeAsync()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ foreach (var session in _activeConnections.Values)
+ {
+ await session.DisposeAsync().ConfigureAwait(false);
+ }
+
+ if (_idleTimer is not null)
+ {
+ await _idleTimer.DisposeAsync().ConfigureAwait(false);
+ _idleTimer = null;
+ }
+
+ await _shutdownCallback.DisposeAsync().ConfigureAwait(false);
+
+ _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
+ _disposed = true;
+ }
+
+ private async void OnApplicationStopping()
+ {
+ _logger.LogInformation("Sending shutdown notifications");
+ try
+ {
+ var messageType = _appHost.ShouldRestart ? SessionMessageType.ServerRestarting : SessionMessageType.ServerShuttingDown;
+
+ await SendMessageToSessions(Sessions, messageType, string.Empty, CancellationToken.None).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error sending server shutdown notifications");
+ }
+
+ // Close open websockets to allow Kestrel to shut down cleanly
+ foreach (var session in _activeConnections.Values)
+ {
+ await session.DisposeAsync().ConfigureAwait(false);
+ }
+
+ _activeConnections.Clear();
+ }
}
}
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 267ba4afb..649397d68 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -23,7 +23,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
@@ -48,7 +47,6 @@ public class LiveTvController : BaseJellyfinApiController
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IConfigurationManager _configurationManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
- private readonly ISessionManager _sessionManager;
/// <summary>
/// Initializes a new instance of the <see cref="LiveTvController"/> class.
@@ -61,7 +59,6 @@ public class LiveTvController : BaseJellyfinApiController
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
/// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
- /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
public LiveTvController(
ILiveTvManager liveTvManager,
IUserManager userManager,
@@ -70,8 +67,7 @@ public class LiveTvController : BaseJellyfinApiController
IDtoService dtoService,
IMediaSourceManager mediaSourceManager,
IConfigurationManager configurationManager,
- TranscodingJobHelper transcodingJobHelper,
- ISessionManager sessionManager)
+ TranscodingJobHelper transcodingJobHelper)
{
_liveTvManager = liveTvManager;
_userManager = userManager;
@@ -81,7 +77,6 @@ public class LiveTvController : BaseJellyfinApiController
_mediaSourceManager = mediaSourceManager;
_configurationManager = configurationManager;
_transcodingJobHelper = transcodingJobHelper;
- _sessionManager = sessionManager;
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index a29790961..42ac4a9b4 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -4,7 +4,6 @@ using System.ComponentModel.DataAnnotations;
using System.IO;
using System.Linq;
using System.Net.Mime;
-using System.Threading.Tasks;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Configuration;
@@ -107,11 +106,7 @@ public class SystemController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult RestartApplication()
{
- Task.Run(async () =>
- {
- await Task.Delay(100).ConfigureAwait(false);
- _appHost.Restart();
- });
+ _appHost.Restart();
return NoContent();
}
@@ -127,11 +122,7 @@ public class SystemController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult ShutdownApplication()
{
- Task.Run(async () =>
- {
- await Task.Delay(100).ConfigureAwait(false);
- await _appHost.Shutdown().ConfigureAwait(false);
- });
+ _appHost.Shutdown();
return NoContent();
}
diff --git a/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs
index 8bf626035..acf3645fd 100644
--- a/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs
+++ b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs
@@ -33,8 +33,7 @@ public class RobotsRedirectionMiddleware
/// <returns>The async task.</returns>
public async Task Invoke(HttpContext httpContext)
{
- var localPath = httpContext.Request.Path.ToString();
- if (string.Equals(localPath, "/robots.txt", StringComparison.OrdinalIgnoreCase))
+ if (httpContext.Request.Path.Equals("/robots.txt", StringComparison.OrdinalIgnoreCase))
{
_logger.LogDebug("Redirecting robots.txt request to web/robots.txt");
httpContext.Response.Redirect("web/robots.txt");
diff --git a/Jellyfin.Data/Enums/PersonKind.cs b/Jellyfin.Data/Enums/PersonKind.cs
index 10a805666..29308789a 100644
--- a/Jellyfin.Data/Enums/PersonKind.cs
+++ b/Jellyfin.Data/Enums/PersonKind.cs
@@ -94,4 +94,40 @@ public enum PersonKind
/// A person who was the illustrator.
/// </summary>
Illustrator,
+
+ /// <summary>
+ /// A person responsible for drawing the art.
+ /// </summary>
+ Penciller,
+
+ /// <summary>
+ /// A person responsible for inking the pencil art.
+ /// </summary>
+ Inker,
+
+ /// <summary>
+ /// A person responsible for applying color to drawings.
+ /// </summary>
+ Colorist,
+
+ /// <summary>
+ /// A person responsible for drawing text and speech bubbles.
+ /// </summary>
+ Letterer,
+
+ /// <summary>
+ /// A person responsible for drawing the cover art.
+ /// </summary>
+ CoverArtist,
+
+ /// <summary>
+ /// A person contributing to a resource by revising or elucidating the content, e.g., adding an introduction, notes, or other critical matter.
+ /// An editor may also prepare a resource for production, publication, or distribution.
+ /// </summary>
+ Editor,
+
+ /// <summary>
+ /// A person who renders a text from one language into another.
+ /// </summary>
+ Translator
}
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 5010751dd..94ac4798c 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -20,7 +20,6 @@ using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Users;
using Microsoft.EntityFrameworkCore;
@@ -35,7 +34,6 @@ namespace Jellyfin.Server.Implementations.Users
{
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IEventManager _eventManager;
- private readonly ICryptoProvider _cryptoProvider;
private readonly INetworkManager _networkManager;
private readonly IApplicationHost _appHost;
private readonly IImageProcessor _imageProcessor;
@@ -53,7 +51,6 @@ namespace Jellyfin.Server.Implementations.Users
/// </summary>
/// <param name="dbProvider">The database provider.</param>
/// <param name="eventManager">The event manager.</param>
- /// <param name="cryptoProvider">The cryptography provider.</param>
/// <param name="networkManager">The network manager.</param>
/// <param name="appHost">The application host.</param>
/// <param name="imageProcessor">The image processor.</param>
@@ -61,7 +58,6 @@ namespace Jellyfin.Server.Implementations.Users
public UserManager(
IDbContextFactory<JellyfinDbContext> dbProvider,
IEventManager eventManager,
- ICryptoProvider cryptoProvider,
INetworkManager networkManager,
IApplicationHost appHost,
IImageProcessor imageProcessor,
@@ -69,7 +65,6 @@ namespace Jellyfin.Server.Implementations.Users
{
_dbProvider = dbProvider;
_eventManager = eventManager;
- _cryptoProvider = cryptoProvider;
_networkManager = networkManager;
_appHost = appHost;
_imageProcessor = imageProcessor;
@@ -384,7 +379,7 @@ namespace Jellyfin.Server.Implementations.Users
}
var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
- var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint)
+ var authResult = await AuthenticateLocalUser(username, password, user)
.ConfigureAwait(false);
var authenticationProvider = authResult.AuthenticationProvider;
var success = authResult.Success;
@@ -787,8 +782,7 @@ namespace Jellyfin.Server.Implementations.Users
private async Task<(IAuthenticationProvider? AuthenticationProvider, string Username, bool Success)> AuthenticateLocalUser(
string username,
string password,
- User? user,
- string remoteEndPoint)
+ User? user)
{
bool success = false;
IAuthenticationProvider? authenticationProvider = null;
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index 0c6315c66..4c116745b 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -103,9 +103,6 @@ namespace Jellyfin.Server
}
/// <inheritdoc />
- protected override void RestartInternal() => Program.Restart();
-
- /// <inheritdoc />
protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal()
{
// Jellyfin.Server
@@ -114,8 +111,5 @@ namespace Jellyfin.Server
// Jellyfin.Server.Implementations
yield return typeof(JellyfinDbContext).Assembly;
}
-
- /// <inheritdoc />
- protected override void ShutdownInternal() => Program.Shutdown();
}
}
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 3271e08e4..89dbbdd2f 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -59,6 +59,7 @@ namespace Jellyfin.Server.Extensions
serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>();
+ serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
return serviceCollection.AddAuthorizationCore(options =>
{
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
index 996f3fe8a..e1a43bb48 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
@@ -73,8 +73,7 @@ namespace Jellyfin.Server.Migrations.Routines
var queryResult = connection.Query("SELECT DISTINCT OfficialRating FROM TypedBaseItems");
foreach (var entry in queryResult)
{
- var ratingString = entry.GetString(0);
- if (string.IsNullOrEmpty(ratingString))
+ if (!entry.TryGetString(0, out var ratingString) || string.IsNullOrEmpty(ratingString))
{
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating IS NULL OR OfficialRating='';");
}
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 6e8b17a73..f9259d0d9 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -4,7 +4,6 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
-using System.Threading;
using System.Threading.Tasks;
using CommandLine;
using Emby.Server.Implementations;
@@ -42,7 +41,6 @@ namespace Jellyfin.Server
public const string LoggingConfigFileSystem = "logging.json";
private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory();
- private static CancellationTokenSource _tokenSource = new();
private static long _startTimestamp;
private static ILogger _logger = NullLogger.Instance;
private static bool _restartOnShutdown;
@@ -65,36 +63,9 @@ namespace Jellyfin.Server
.MapResult(StartApp, ErrorParsingArguments);
}
- /// <summary>
- /// Shuts down the application.
- /// </summary>
- internal static void Shutdown()
- {
- if (!_tokenSource.IsCancellationRequested)
- {
- _tokenSource.Cancel();
- }
- }
-
- /// <summary>
- /// Restarts the application.
- /// </summary>
- internal static void Restart()
- {
- _restartOnShutdown = true;
-
- Shutdown();
- }
-
private static async Task StartApp(StartupOptions options)
{
_startTimestamp = Stopwatch.GetTimestamp();
-
- // Log all uncaught exceptions to std error
- static void UnhandledExceptionToConsole(object sender, UnhandledExceptionEventArgs e) =>
- Console.Error.WriteLine("Unhandled Exception\n" + e.ExceptionObject);
- AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionToConsole;
-
ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options);
// $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
@@ -112,38 +83,10 @@ namespace Jellyfin.Server
StartupHelpers.InitializeLoggingFramework(startupConfig, appPaths);
_logger = _loggerFactory.CreateLogger("Main");
- // Log uncaught exceptions to the logging instead of std error
- AppDomain.CurrentDomain.UnhandledException -= UnhandledExceptionToConsole;
+ // Use the logging framework for uncaught exceptions instead of std error
AppDomain.CurrentDomain.UnhandledException += (_, e)
=> _logger.LogCritical((Exception)e.ExceptionObject, "Unhandled Exception");
- // Intercept Ctrl+C and Ctrl+Break
- Console.CancelKeyPress += (_, e) =>
- {
- if (_tokenSource.IsCancellationRequested)
- {
- return; // Already shutting down
- }
-
- e.Cancel = true;
- _logger.LogInformation("Ctrl+C, shutting down");
- Environment.ExitCode = 128 + 2;
- Shutdown();
- };
-
- // Register a SIGTERM handler
- AppDomain.CurrentDomain.ProcessExit += (_, _) =>
- {
- if (_tokenSource.IsCancellationRequested)
- {
- return; // Already shutting down
- }
-
- _logger.LogInformation("Received a SIGTERM signal, shutting down");
- Environment.ExitCode = 128 + 15;
- Shutdown();
- };
-
_logger.LogInformation(
"Jellyfin version: {Version}",
Assembly.GetEntryAssembly()!.GetName().Version!.ToString(3));
@@ -173,12 +116,10 @@ namespace Jellyfin.Server
do
{
- _restartOnShutdown = false;
await StartServer(appPaths, options, startupConfig).ConfigureAwait(false);
if (_restartOnShutdown)
{
- _tokenSource = new CancellationTokenSource();
_startTimestamp = Stopwatch.GetTimestamp();
}
} while (_restartOnShutdown);
@@ -186,7 +127,7 @@ namespace Jellyfin.Server
private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig)
{
- var appHost = new CoreAppHost(
+ using var appHost = new CoreAppHost(
appPaths,
_loggerFactory,
options,
@@ -196,6 +137,7 @@ namespace Jellyfin.Server
try
{
host = Host.CreateDefaultBuilder()
+ .UseConsoleLifetime()
.ConfigureServices(services => appHost.Init(services))
.ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths, _logger))
.ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig))
@@ -210,7 +152,7 @@ namespace Jellyfin.Server
try
{
- await host.StartAsync(_tokenSource.Token).ConfigureAwait(false);
+ await host.StartAsync().ConfigureAwait(false);
if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket())
{
@@ -219,22 +161,18 @@ namespace Jellyfin.Server
StartupHelpers.SetUnixSocketPermissions(startupConfig, socketPath, _logger);
}
}
- catch (Exception ex) when (ex is not TaskCanceledException)
+ catch (Exception)
{
_logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again");
throw;
}
- await appHost.RunStartupTasksAsync(_tokenSource.Token).ConfigureAwait(false);
+ await appHost.RunStartupTasksAsync().ConfigureAwait(false);
_logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp));
- // Block main thread until shutdown
- await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false);
- }
- catch (TaskCanceledException)
- {
- // Don't throw on cancellation
+ await host.WaitForShutdownAsync().ConfigureAwait(false);
+ _restartOnShutdown = appHost.ShouldRestart;
}
catch (Exception ex)
{
@@ -257,7 +195,6 @@ namespace Jellyfin.Server
}
}
- await appHost.DisposeAsync().ConfigureAwait(false);
host?.Dispose();
}
}
diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs
index 96ee701b3..5985d3dd8 100644
--- a/MediaBrowser.Common/IApplicationHost.cs
+++ b/MediaBrowser.Common/IApplicationHost.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Reflection;
-using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common
@@ -48,6 +47,11 @@ namespace MediaBrowser.Common
bool IsShuttingDown { get; }
/// <summary>
+ /// Gets a value indicating whether the application should restart.
+ /// </summary>
+ bool ShouldRestart { get; }
+
+ /// <summary>
/// Gets the application version.
/// </summary>
/// <value>The application version.</value>
@@ -126,8 +130,7 @@ namespace MediaBrowser.Common
/// <summary>
/// Shuts down.
/// </summary>
- /// <returns>A task.</returns>
- Task Shutdown();
+ void Shutdown();
/// <summary>
/// Initializes this instance.
diff --git a/MediaBrowser.Common/Plugins/IPluginManager.cs b/MediaBrowser.Common/Plugins/IPluginManager.cs
index 1d73de3c9..0ff9719e9 100644
--- a/MediaBrowser.Common/Plugins/IPluginManager.cs
+++ b/MediaBrowser.Common/Plugins/IPluginManager.cs
@@ -30,11 +30,6 @@ namespace MediaBrowser.Common.Plugins
IEnumerable<Assembly> LoadAssemblies();
/// <summary>
- /// Unloads all of the assemblies.
- /// </summary>
- void UnloadAssemblies();
-
- /// <summary>
/// Registers the plugin's services with the DI.
/// Note: DI is not yet instantiated yet.
/// </summary>
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index 095b261c0..f51162f9d 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -3,6 +3,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -29,7 +30,7 @@ namespace MediaBrowser.Controller.Entities
public class CollectionFolder : Folder, ICollectionFolder
{
private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
- private static readonly Dictionary<string, LibraryOptions> _libraryOptions = new Dictionary<string, LibraryOptions>();
+ private static readonly ConcurrentDictionary<string, LibraryOptions> _libraryOptions = new ConcurrentDictionary<string, LibraryOptions>();
private bool _requiresRefresh;
/// <summary>
@@ -139,45 +140,26 @@ namespace MediaBrowser.Controller.Entities
}
public static LibraryOptions GetLibraryOptions(string path)
- {
- lock (_libraryOptions)
- {
- if (!_libraryOptions.TryGetValue(path, out var options))
- {
- options = LoadLibraryOptions(path);
- _libraryOptions[path] = options;
- }
-
- return options;
- }
- }
+ => _libraryOptions.GetOrAdd(path, LoadLibraryOptions);
public static void SaveLibraryOptions(string path, LibraryOptions options)
{
- lock (_libraryOptions)
- {
- _libraryOptions[path] = options;
+ _libraryOptions[path] = options;
- var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions);
- foreach (var mediaPath in clone.PathInfos)
+ var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions);
+ foreach (var mediaPath in clone.PathInfos)
+ {
+ if (!string.IsNullOrEmpty(mediaPath.Path))
{
- if (!string.IsNullOrEmpty(mediaPath.Path))
- {
- mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path);
- }
+ mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path);
}
-
- XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path));
}
+
+ XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path));
}
public static void OnCollectionFolderChange()
- {
- lock (_libraryOptions)
- {
- _libraryOptions.Clear();
- }
- }
+ => _libraryOptions.Clear();
public override bool IsSaveLocalMetadataEnabled()
{
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index b6e680ab9..c311d3b8a 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -48,6 +48,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3);
private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1);
+ private readonly Version _minFFmpegVaapiH26xEncA53CcSei = new Version(6, 0);
private static readonly string[] _videoProfilesH264 = new[]
{
@@ -2006,6 +2007,14 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -svtav1-params:0 rc=1:tune=0:film-grain=0:enable-overlays=1:enable-tf=0";
}
+ /* Access unit too large: 8192 < 20880 error */
+ if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)) &&
+ _mediaEncoder.EncoderVersion >= _minFFmpegVaapiH26xEncA53CcSei)
+ {
+ param += " -sei -a53_cc";
+ }
+
return param;
}
diff --git a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
index d1a51c2cf..bb68a3b6d 100644
--- a/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
+++ b/MediaBrowser.Controller/Playlists/IPlaylistManager.cs
@@ -44,6 +44,12 @@ namespace MediaBrowser.Controller.Playlists
/// <summary>
/// Gets the playlists folder.
/// </summary>
+ /// <returns>Folder.</returns>
+ Folder GetPlaylistsFolder();
+
+ /// <summary>
+ /// Gets the playlists folder for a user.
+ /// </summary>
/// <param name="userId">The user identifier.</param>
/// <returns>Folder.</returns>
Folder GetPlaylistsFolder(Guid userId);
diff --git a/MediaBrowser.Controller/Resolvers/IItemResolver.cs b/MediaBrowser.Controller/Resolvers/IItemResolver.cs
index b95d00aa3..282aa721e 100644
--- a/MediaBrowser.Controller/Resolvers/IItemResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/IItemResolver.cs
@@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>BaseItem.</returns>
- BaseItem ResolvePath(ItemResolveArgs args);
+ BaseItem? ResolvePath(ItemResolveArgs args);
}
public interface IMultiItemResolver
diff --git a/MediaBrowser.Controller/Resolvers/ItemResolver.cs b/MediaBrowser.Controller/Resolvers/ItemResolver.cs
index a6da8384e..5c9dd6f07 100644
--- a/MediaBrowser.Controller/Resolvers/ItemResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/ItemResolver.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -23,7 +21,7 @@ namespace MediaBrowser.Controller.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>`0.</returns>
- protected internal virtual T Resolve(ItemResolveArgs args)
+ protected internal virtual T? Resolve(ItemResolveArgs args)
{
return null;
}
@@ -42,7 +40,7 @@ namespace MediaBrowser.Controller.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>BaseItem.</returns>
- public BaseItem ResolvePath(ItemResolveArgs args)
+ public BaseItem? ResolvePath(ItemResolveArgs args)
{
var item = Resolve(args);
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index 0c4719a0e..53df7133b 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -233,20 +233,6 @@ namespace MediaBrowser.Controller.Session
Task SendRestartRequiredNotification(CancellationToken cancellationToken);
/// <summary>
- /// Sends the server shutdown notification.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- Task SendServerShutdownNotification(CancellationToken cancellationToken);
-
- /// <summary>
- /// Sends the server restart notification.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- Task SendServerRestartNotification(CancellationToken cancellationToken);
-
- /// <summary>
/// Adds the additional user.
/// </summary>
/// <param name="sessionId">The session identifier.</param>
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 3055e6cde..441a3abd4 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -78,6 +78,7 @@ namespace MediaBrowser.MediaEncoding.Probing
"She/Her/Hers",
"5/8erl in Ehr'n",
"Smith/Kotzen",
+ "We;Na",
};
/// <summary>
diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs
index 786b20e9e..ec381d423 100644
--- a/MediaBrowser.Model/IO/IFileSystem.cs
+++ b/MediaBrowser.Model/IO/IFileSystem.cs
@@ -10,8 +10,6 @@ namespace MediaBrowser.Model.IO
/// </summary>
public interface IFileSystem
{
- void AddShortcutHandler(IShortcutHandler handler);
-
/// <summary>
/// Determines whether the specified filename is shortcut.
/// </summary>
@@ -117,13 +115,6 @@ namespace MediaBrowser.Model.IO
bool ContainsSubPath(string parentPath, string path);
/// <summary>
- /// Normalizes the path.
- /// </summary>
- /// <param name="path">The path.</param>
- /// <returns>System.String.</returns>
- string NormalizePath(string path);
-
- /// <summary>
/// Gets the file name without extension.
/// </summary>
/// <param name="info">The information.</param>
diff --git a/debian/jellyfin.init b/debian/jellyfin.init
index 7f5642bac..784536d87 100644
--- a/debian/jellyfin.init
+++ b/debian/jellyfin.init
@@ -1,3 +1,4 @@
+#!/bin/sh
### BEGIN INIT INFO
# Provides: Jellyfin Media Server
# Required-Start: $local_fs $network
diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec
index d9d3d4869..e78368906 100644
--- a/fedora/jellyfin.spec
+++ b/fedora/jellyfin.spec
@@ -75,6 +75,7 @@ dotnet publish --configuration Release --self-contained --runtime %{dotnet_runti
%{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir}
%{__cp} -r Jellyfin.Server/bin/Release/net7.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin
%{__install} -D %{SOURCE10} %{buildroot}%{_bindir}/jellyfin
+sed -i -e 's|/usr/lib64|%{_libdir}|g' %{buildroot}%{_bindir}/jellyfin
# Jellyfin config
%{__install} -D Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json
diff --git a/jellyfin.ruleset b/jellyfin.ruleset
index 505d35481..870cf253f 100644
--- a/jellyfin.ruleset
+++ b/jellyfin.ruleset
@@ -52,6 +52,8 @@
<Rule Id="SA1204" Action="None" />
<!-- disable warning SA1309: Fields must not begin with an underscore -->
<Rule Id="SA1309" Action="None" />
+ <!-- disable warning SA1311: Static readonly fields should begin with upper-case letter -->
+ <Rule Id="SA1311" Action="None" />
<!-- disable warning SA1413: Use trailing comma in multi-line initializers -->
<Rule Id="SA1413" Action="None" />
<!-- disable warning SA1512: Single-line comments must not be followed by blank line -->
@@ -171,4 +173,18 @@
<!-- error on RS0030: Do not used banned APIs -->
<Rule Id="RS0030" Action="Error" />
</Rules>
+
+ <Rules AnalyzerId="IDisposableAnalyzers" RuleNamespace="IDisposableAnalyzers.Correctness">
+ <!-- disable warning IDISP001: Dispose created -->
+ <Rule Id="IDISP001" Action="Info" />
+ <!-- TODO: Enable when false positives are fixed -->
+ <!-- disable warning IDISP003: Dispose previous before re-assigning -->
+ <Rule Id="IDISP003" Action="Info" />
+ <!-- disable warning IDISP004: Don't ignore created IDisposable -->
+ <Rule Id="IDISP004" Action="Info" />
+ <!-- disable warning IDISP007: Don't dispose injected -->
+ <Rule Id="IDISP007" Action="Info" />
+ <!-- disable warning IDISP008: Don't assign member with injected and created disposables -->
+ <Rule Id="IDISP008" Action="Info" />
+ </Rules>
</RuleSet>
diff --git a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
index 034691322..c465c4ad0 100644
--- a/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
+++ b/src/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj
@@ -31,8 +31,12 @@
<ProjectReference Include="..\..\MediaBrowser.Common\MediaBrowser.Common.csproj" />
</ItemGroup>
- <!-- Code analysers-->
+ <!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 5a1d3dc5f..126c0503e 100644
--- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -122,8 +122,8 @@ public class SkiaEncoder : IImageEncoder
var svg = new SKSvg();
try
{
- svg.Load(path);
- return new ImageDimensions(Convert.ToInt32(svg.Picture.CullRect.Width), Convert.ToInt32(svg.Picture.CullRect.Height));
+ using var picture = svg.Load(path);
+ return new ImageDimensions(Convert.ToInt32(picture.CullRect.Width), Convert.ToInt32(picture.CullRect.Height));
}
catch (FormatException skiaColorException)
{
@@ -432,7 +432,8 @@ public class SkiaEncoder : IImageEncoder
// scale image (the FromImage creates a copy)
var imageInfo = new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace);
- using var resizedBitmap = SKBitmap.FromImage(ResizeImage(bitmap, imageInfo));
+ using var resizedImage = ResizeImage(bitmap, imageInfo);
+ using var resizedBitmap = SKBitmap.FromImage(resizedImage);
// If all we're doing is resizing then we can stop now
if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
diff --git a/src/Jellyfin.Drawing.Skia/SkiaHelper.cs b/src/Jellyfin.Drawing.Skia/SkiaHelper.cs
index 00d224da9..bd1b2b0da 100644
--- a/src/Jellyfin.Drawing.Skia/SkiaHelper.cs
+++ b/src/Jellyfin.Drawing.Skia/SkiaHelper.cs
@@ -19,7 +19,6 @@ public static class SkiaHelper
public static SKBitmap? GetNextValidImage(SkiaEncoder skiaEncoder, IReadOnlyList<string> paths, int currentIndex, out int newIndex)
{
var imagesTested = new Dictionary<int, int>();
- SKBitmap? bitmap = null;
while (imagesTested.Count < paths.Count)
{
@@ -28,7 +27,7 @@ public static class SkiaHelper
currentIndex = 0;
}
- bitmap = skiaEncoder.Decode(paths[currentIndex], false, null, out _);
+ SKBitmap? bitmap = skiaEncoder.Decode(paths[currentIndex], false, null, out _);
imagesTested[currentIndex] = 0;
@@ -36,11 +35,12 @@ public static class SkiaHelper
if (bitmap is not null)
{
- break;
+ newIndex = currentIndex;
+ return bitmap;
}
}
newIndex = currentIndex;
- return bitmap;
+ return null;
}
}
diff --git a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
index a8f80f7e2..6dff7aa9b 100644
--- a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
+++ b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
@@ -189,12 +189,12 @@ public partial class StripCollageBuilder
// Scale image. The FromBitmap creates a copy
var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace);
- using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(currentBitmap, imageInfo));
+ using var resizeImage = SkiaEncoder.ResizeImage(currentBitmap, imageInfo);
// draw this image into the strip at the next position
var xPos = x * cellWidth;
var yPos = y * cellHeight;
- canvas.DrawBitmap(resizedBitmap, xPos, yPos);
+ canvas.DrawImage(resizeImage, xPos, yPos);
}
}
diff --git a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
index e0963ac34..2a5e24a44 100644
--- a/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
+++ b/src/Jellyfin.Drawing/Jellyfin.Drawing.csproj
@@ -21,8 +21,12 @@
<Compile Include="..\..\SharedVersion.cs" />
</ItemGroup>
- <!-- Code analysers-->
+ <!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
index 4f80aa941..36ae55ed2 100644
--- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
+++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
@@ -34,6 +34,10 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
index 3f4f55ee4..b792e7ec6 100644
--- a/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
+++ b/src/Jellyfin.MediaEncoding.Hls/Jellyfin.MediaEncoding.Hls.csproj
@@ -7,6 +7,10 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
index 71572bcf6..09b1f8faa 100644
--- a/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
+++ b/src/Jellyfin.MediaEncoding.Keyframes/Jellyfin.MediaEncoding.Keyframes.csproj
@@ -11,6 +11,10 @@
<!-- Code Analyzers-->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
+ <PackageReference Include="IDisposableAnalyzers">
+ <PrivateAssets>all</PrivateAssets>
+ <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
+ </PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs
index 09eb22328..07061cfc7 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs
@@ -31,6 +31,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("/media/music/Foo B.A.R./epic.flac", false)]
[InlineData("/media/music/Foo B.A.R", false)]
[InlineData("/media/music/Foo B.A.R.", false)]
+ [InlineData("/movies/.zfs/snapshot/AutoM-2023-09", true)]
public void PathIgnored(string path, bool expected)
{
Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path));
diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
index 55bc43455..1c87d11f1 100644
--- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
@@ -2,7 +2,6 @@ using System;
using System.Collections.Concurrent;
using System.Globalization;
using System.IO;
-using System.Threading;
using Emby.Server.Implementations;
using Jellyfin.Server.Extensions;
using Jellyfin.Server.Helpers;
@@ -105,7 +104,7 @@ namespace Jellyfin.Server.Integration.Tests
var appHost = (TestAppHost)testServer.Services.GetRequiredService<IApplicationHost>();
appHost.ServiceProvider = testServer.Services;
appHost.InitializeServices().GetAwaiter().GetResult();
- appHost.RunStartupTasksAsync(CancellationToken.None).GetAwaiter().GetResult();
+ appHost.RunStartupTasksAsync().GetAwaiter().GetResult();
return testServer;
}