Merge branch 'master' into chromecast-config

# Conflicts:
#	Emby.Server.Implementations/ApplicationHost.cs
pull/10270/head
Cody Robibero 7 months ago
commit 6bd6fb6e0a

@ -27,11 +27,11 @@ jobs:
dotnet-version: '7.0.x' dotnet-version: '7.0.x'
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # v2.21.8 uses: github/codeql-action/init@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
queries: +security-extended queries: +security-extended
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # v2.21.8 uses: github/codeql-action/autobuild@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # v2.21.8 uses: github/codeql-action/analyze@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1

@ -2,16 +2,17 @@ name: Stale Check
on: on:
schedule: schedule:
- cron: '30 1 * * *' - cron: '30 */12 * * *'
workflow_dispatch: workflow_dispatch:
permissions: permissions:
issues: write issues: write
pull-requests: write pull-requests: write
actions: write
jobs: jobs:
issues: issues:
name: Check issues name: Check for stale issues
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }} if: ${{ contains(github.repository, 'jellyfin/') }}
steps: steps:
@ -26,11 +27,11 @@ jobs:
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
stale-issue-label: stale stale-issue-label: stale
stale-issue-message: |- stale-issue-message: |-
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments. This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs.
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label. If you have any questions you can use one of several ways to [contact us](https://jellyfin.org/contact).
close-issue-message: |-
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html). This issue was closed due to inactivity.
prs-conflicts: prs-conflicts:
name: Check PRs with merge conflicts name: Check PRs with merge conflicts

@ -238,3 +238,4 @@
- [Jakob Kukla](https://github.com/jakobkukla) - [Jakob Kukla](https://github.com/jakobkukla)
- [Utku Özdemir](https://github.com/utkuozdemir) - [Utku Özdemir](https://github.com/utkuozdemir)
- [JPUC1143](https://github.com/Jpuc1143/) - [JPUC1143](https://github.com/Jpuc1143/)
- [0x25CBFC4F](https://github.com/0x25CBFC4F)

@ -19,12 +19,13 @@
<PackageVersion Include="DotNet.Glob" Version="3.1.3" /> <PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.2" /> <PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.2" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" /> <PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" />
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" /> <PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" /> <PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="3.6.13" /> <PackageVersion Include="libse" Version="3.6.13" />
<PackageVersion Include="LrcParser" Version="2023.524.0" /> <PackageVersion Include="LrcParser" Version="2023.524.0" />
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" /> <PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.11" /> <PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.12" />
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" /> <PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.11" /> <PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.11" />
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" /> <PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
@ -65,14 +66,13 @@
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" /> <PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" /> <PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.2" /> <PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.0" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" /> <PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
<PackageVersion Include="SharpFuzz" Version="2.1.1" /> <PackageVersion Include="SharpFuzz" Version="2.1.1" />
<PackageVersion Include="SkiaSharp" Version="2.88.5" />
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.5" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" /> <PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" />
<PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" /> <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="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" /> <PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" />
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" /> <PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />

@ -4,7 +4,7 @@
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register # https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=7.0 ARG DOTNET_VERSION=7.0
FROM node:lts-alpine as web-builder FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master 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 \ 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 - \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \

@ -5,7 +5,7 @@
ARG DOTNET_VERSION=7.0 ARG DOTNET_VERSION=7.0
FROM node:lts-alpine as web-builder FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master 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 \ 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 - \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \

@ -5,7 +5,7 @@
ARG DOTNET_VERSION=7.0 ARG DOTNET_VERSION=7.0
FROM node:lts-alpine as web-builder FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master 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 \ 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 - \ && curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \

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

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

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

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

@ -8,7 +8,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events; using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -19,14 +18,8 @@ namespace Emby.Server.Implementations.AppBase
/// </summary> /// </summary>
public abstract class BaseConfigurationManager : IConfigurationManager public abstract class BaseConfigurationManager : IConfigurationManager
{ {
private readonly IFileSystem _fileSystem; private readonly ConcurrentDictionary<string, object> _configurations = new();
private readonly object _configurationSyncLock = new();
private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
/// <summary>
/// The _configuration sync lock.
/// </summary>
private readonly object _configurationSyncLock = new object();
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>(); private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>(); private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
@ -42,12 +35,13 @@ namespace Emby.Server.Implementations.AppBase
/// <param name="applicationPaths">The application paths.</param> /// <param name="applicationPaths">The application paths.</param>
/// <param name="loggerFactory">The logger factory.</param> /// <param name="loggerFactory">The logger factory.</param>
/// <param name="xmlSerializer">The XML serializer.</param> /// <param name="xmlSerializer">The XML serializer.</param>
/// <param name="fileSystem">The file system.</param> protected BaseConfigurationManager(
protected BaseConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem) IApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
IXmlSerializer xmlSerializer)
{ {
CommonApplicationPaths = applicationPaths; CommonApplicationPaths = applicationPaths;
XmlSerializer = xmlSerializer; XmlSerializer = xmlSerializer;
_fileSystem = fileSystem;
Logger = loggerFactory.CreateLogger<BaseConfigurationManager>(); Logger = loggerFactory.CreateLogger<BaseConfigurationManager>();
UpdateCachePath(); UpdateCachePath();
@ -272,7 +266,7 @@ namespace Emby.Server.Implementations.AppBase
{ {
var file = Path.Combine(path, Guid.NewGuid().ToString()); var file = Path.Combine(path, Guid.NewGuid().ToString());
File.WriteAllText(file, string.Empty); File.WriteAllText(file, string.Empty);
_fileSystem.DeleteFile(file); File.Delete(file);
} }
private string GetConfigurationFile(string key) private string GetConfigurationFile(string key)

@ -12,7 +12,6 @@ using System.Linq;
using System.Net; using System.Net;
using System.Reflection; using System.Reflection;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Dlna; using Emby.Dlna;
using Emby.Dlna.Main; using Emby.Dlna.Main;
@ -112,7 +111,7 @@ namespace Emby.Server.Implementations
/// <summary> /// <summary>
/// Class CompositionRoot. /// Class CompositionRoot.
/// </summary> /// </summary>
public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable public abstract class ApplicationHost : IServerApplicationHost, IDisposable
{ {
/// <summary> /// <summary>
/// The disposable parts. /// The disposable parts.
@ -120,14 +119,12 @@ namespace Emby.Server.Implementations
private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new(); private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
private readonly DeviceId _deviceId; private readonly DeviceId _deviceId;
private readonly IFileSystem _fileSystemManager;
private readonly IConfiguration _startupConfig; private readonly IConfiguration _startupConfig;
private readonly IXmlSerializer _xmlSerializer; private readonly IXmlSerializer _xmlSerializer;
private readonly IStartupOptions _startupOptions; private readonly IStartupOptions _startupOptions;
private readonly IPluginManager _pluginManager; private readonly IPluginManager _pluginManager;
private List<Type> _creatingInstances; private List<Type> _creatingInstances;
private ISessionManager _sessionManager;
/// <summary> /// <summary>
/// Gets or sets all concrete types. /// Gets or sets all concrete types.
@ -135,7 +132,7 @@ namespace Emby.Server.Implementations
/// <value>All concrete types.</value> /// <value>All concrete types.</value>
private Type[] _allConcreteTypes; private Type[] _allConcreteTypes;
private bool _disposed = false; private bool _disposed;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ApplicationHost"/> class. /// Initializes a new instance of the <see cref="ApplicationHost"/> class.
@ -154,10 +151,8 @@ namespace Emby.Server.Implementations
LoggerFactory = loggerFactory; LoggerFactory = loggerFactory;
_startupOptions = options; _startupOptions = options;
_startupConfig = startupConfig; _startupConfig = startupConfig;
_fileSystemManager = new ManagedFileSystem(LoggerFactory.CreateLogger<ManagedFileSystem>(), applicationPaths);
Logger = LoggerFactory.CreateLogger<ApplicationHost>(); Logger = LoggerFactory.CreateLogger<ApplicationHost>();
_fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler());
_deviceId = new DeviceId(ApplicationPaths, LoggerFactory); _deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version; ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
@ -165,13 +160,15 @@ namespace Emby.Server.Implementations
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString; ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
_xmlSerializer = new MyXmlSerializer(); _xmlSerializer = new MyXmlSerializer();
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager); ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer);
_pluginManager = new PluginManager( _pluginManager = new PluginManager(
LoggerFactory.CreateLogger<PluginManager>(), LoggerFactory.CreateLogger<PluginManager>(),
this, this,
ConfigurationManager.Configuration, ConfigurationManager.Configuration,
ApplicationPaths.PluginsPath, ApplicationPaths.PluginsPath,
ApplicationVersion); ApplicationVersion);
_disposableParts.TryAdd((PluginManager)_pluginManager, byte.MinValue);
} }
/// <summary> /// <summary>
@ -186,23 +183,16 @@ namespace Emby.Server.Implementations
public bool CoreStartupHasCompleted { get; private set; } public bool CoreStartupHasCompleted { get; private set; }
public virtual bool CanLaunchWebBrowser => Environment.UserInteractive
&& !_startupOptions.IsService
&& (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
/// <summary> /// <summary>
/// Gets the <see cref="INetworkManager"/> singleton instance. /// Gets the <see cref="INetworkManager"/> singleton instance.
/// </summary> /// </summary>
public INetworkManager NetManager { get; private set; } public INetworkManager NetManager { get; private set; }
/// <summary> /// <inheritdoc />
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
/// </summary>
/// <value><c>true</c> if this instance has pending application restart; otherwise, <c>false</c>.</value>
public bool HasPendingRestart { get; private set; } public bool HasPendingRestart { get; private set; }
/// <inheritdoc /> /// <inheritdoc />
public bool IsShuttingDown { get; private set; } public bool ShouldRestart { get; set; }
/// <summary> /// <summary>
/// Gets the logger. /// Gets the logger.
@ -406,11 +396,9 @@ namespace Emby.Server.Implementations
/// <summary> /// <summary>
/// Runs the startup tasks. /// Runs the startup tasks.
/// </summary> /// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns><see cref="Task" />.</returns> /// <returns><see cref="Task" />.</returns>
public async Task RunStartupTasksAsync(CancellationToken cancellationToken) public async Task RunStartupTasksAsync()
{ {
cancellationToken.ThrowIfCancellationRequested();
Logger.LogInformation("Running startup tasks"); Logger.LogInformation("Running startup tasks");
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false)); Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
@ -424,8 +412,6 @@ namespace Emby.Server.Implementations
var entryPoints = GetExports<IServerEntryPoint>(); var entryPoints = GetExports<IServerEntryPoint>();
cancellationToken.ThrowIfCancellationRequested();
var stopWatch = new Stopwatch(); var stopWatch = new Stopwatch();
stopWatch.Start(); stopWatch.Start();
@ -435,8 +421,6 @@ namespace Emby.Server.Implementations
Logger.LogInformation("Core startup complete"); Logger.LogInformation("Core startup complete");
CoreStartupHasCompleted = true; CoreStartupHasCompleted = true;
cancellationToken.ThrowIfCancellationRequested();
stopWatch.Restart(); stopWatch.Restart();
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false); await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
@ -509,7 +493,11 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton(_pluginManager); serviceCollection.AddSingleton(_pluginManager);
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths); serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
serviceCollection.AddSingleton(_fileSystemManager); serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
serviceCollection.AddScoped<ISystemManager, SystemManager>();
serviceCollection.AddSingleton<TmdbClientManager>(); serviceCollection.AddSingleton<TmdbClientManager>();
serviceCollection.AddSingleton(NetManager); serviceCollection.AddSingleton(NetManager);
@ -633,8 +621,6 @@ namespace Emby.Server.Implementations
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>(); var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
await localizationManager.LoadAll().ConfigureAwait(false); await localizationManager.LoadAll().ConfigureAwait(false);
_sessionManager = Resolve<ISessionManager>();
SetStaticProperties(); SetStaticProperties();
FindParts(); FindParts();
@ -685,7 +671,7 @@ namespace Emby.Server.Implementations
BaseItem.ProviderManager = Resolve<IProviderManager>(); BaseItem.ProviderManager = Resolve<IProviderManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>(); BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
BaseItem.ItemRepository = Resolve<IItemRepository>(); BaseItem.ItemRepository = Resolve<IItemRepository>();
BaseItem.FileSystem = _fileSystemManager; BaseItem.FileSystem = Resolve<IFileSystem>();
BaseItem.UserDataManager = Resolve<IUserDataManager>(); BaseItem.UserDataManager = Resolve<IUserDataManager>();
BaseItem.ChannelManager = Resolve<IChannelManager>(); BaseItem.ChannelManager = Resolve<IChannelManager>();
Video.LiveTvManager = Resolve<ILiveTvManager>(); Video.LiveTvManager = Resolve<ILiveTvManager>();
@ -855,38 +841,6 @@ namespace Emby.Server.Implementations
} }
} }
/// <summary>
/// Restarts this instance.
/// </summary>
public void Restart()
{
if (IsShuttingDown)
{
return;
}
IsShuttingDown = true;
_pluginManager.UnloadAssemblies();
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();
});
}
protected abstract void RestartInternal();
/// <summary> /// <summary>
/// Gets the composable part assemblies. /// Gets the composable part assemblies.
/// </summary> /// </summary>
@ -942,50 +896,6 @@ namespace Emby.Server.Implementations
protected abstract IEnumerable<Assembly> GetAssembliesWithPartsInternal(); protected abstract IEnumerable<Assembly> GetAssembliesWithPartsInternal();
/// <summary>
/// Gets the system status.
/// </summary>
/// <param name="request">Where this request originated.</param>
/// <returns>SystemInfo.</returns>
public SystemInfo GetSystemInfo(HttpRequest request)
{
return new SystemInfo
{
HasPendingRestart = HasPendingRestart,
IsShuttingDown = IsShuttingDown,
Version = ApplicationVersionString,
WebSocketPortNumber = HttpPort,
CompletedInstallations = Resolve<IInstallationManager>().CompletedInstallations.ToArray(),
Id = SystemId,
ProgramDataPath = ApplicationPaths.ProgramDataPath,
WebPath = ApplicationPaths.WebPath,
LogPath = ApplicationPaths.LogDirectoryPath,
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
CachePath = ApplicationPaths.CachePath,
CanLaunchWebBrowser = CanLaunchWebBrowser,
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
ServerName = FriendlyName,
LocalAddress = GetSmartApiUrl(request),
SupportsLibraryMonitor = true,
PackageName = _startupOptions.PackageName,
CastReceiverApplications = ConfigurationManager.Configuration.CastReceiverApplications
};
}
public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
{
return new PublicSystemInfo
{
Version = ApplicationVersionString,
ProductName = ApplicationProductName,
Id = SystemId,
ServerName = FriendlyName,
LocalAddress = GetSmartApiUrl(request),
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
};
}
/// <inheritdoc/> /// <inheritdoc/>
public string GetSmartApiUrl(IPAddress remoteAddr) public string GetSmartApiUrl(IPAddress remoteAddr)
{ {
@ -1066,30 +976,6 @@ namespace Emby.Server.Implementations
}.ToString().TrimEnd('/'); }.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() public IEnumerable<Assembly> GetApiPluginAssemblies()
{ {
var assemblies = _allConcreteTypes var assemblies = _allConcreteTypes
@ -1153,52 +1039,5 @@ namespace Emby.Server.Implementations
_disposed = true; _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);
}
}
}
} }
} }

@ -7,7 +7,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -22,11 +21,13 @@ namespace Emby.Server.Implementations.Configuration
/// Initializes a new instance of the <see cref="ServerConfigurationManager" /> class. /// Initializes a new instance of the <see cref="ServerConfigurationManager" /> class.
/// </summary> /// </summary>
/// <param name="applicationPaths">The application paths.</param> /// <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="xmlSerializer">The XML serializer.</param>
/// <param name="fileSystem">The file system.</param> public ServerConfigurationManager(
public ServerConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem) IApplicationPaths applicationPaths,
: base(applicationPaths, loggerFactory, xmlSerializer, fileSystem) ILoggerFactory loggerFactory,
IXmlSerializer xmlSerializer)
: base(applicationPaths, loggerFactory, xmlSerializer)
{ {
UpdateMetadataPath(); UpdateMetadataPath();
} }

@ -15,10 +15,6 @@ namespace Emby.Server.Implementations.IO
/// </summary> /// </summary>
public class ManagedFileSystem : IFileSystem 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 bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows();
private static readonly char[] _invalidPathCharacters = private static readonly char[] _invalidPathCharacters =
{ {
@ -29,23 +25,24 @@ namespace Emby.Server.Implementations.IO
(char)31, ':', '*', '?', '\\', '/' (char)31, ':', '*', '?', '\\', '/'
}; };
private readonly ILogger<ManagedFileSystem> _logger;
private readonly List<IShortcutHandler> _shortcutHandlers;
private readonly string _tempPath;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ManagedFileSystem"/> class. /// Initializes a new instance of the <see cref="ManagedFileSystem"/> class.
/// </summary> /// </summary>
/// <param name="logger">The <see cref="ILogger"/> instance to use.</param> /// <param name="logger">The <see cref="ILogger"/> instance to use.</param>
/// <param name="applicationPaths">The <see cref="IApplicationPaths"/> 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( public ManagedFileSystem(
ILogger<ManagedFileSystem> logger, ILogger<ManagedFileSystem> logger,
IApplicationPaths applicationPaths) IApplicationPaths applicationPaths,
IEnumerable<IShortcutHandler> shortcutHandlers)
{ {
_logger = logger; _logger = logger;
_tempPath = applicationPaths.TempDirectory; _tempPath = applicationPaths.TempDirectory;
} _shortcutHandlers = shortcutHandlers.ToList();
/// <inheritdoc />
public virtual void AddShortcutHandler(IShortcutHandler handler)
{
_shortcutHandlers.Add(handler);
} }
/// <summary> /// <summary>
@ -106,15 +103,17 @@ namespace Emby.Server.Implementations.IO
return filePath; return filePath;
} }
var filePathSpan = filePath.AsSpan();
// relative path // relative path
if (firstChar == '\\') if (firstChar == '\\')
{ {
filePath = filePath.Substring(1); filePathSpan = filePathSpan.Slice(1);
} }
try try
{ {
return Path.GetFullPath(Path.Combine(folderPath, filePath)); return Path.GetFullPath(Path.Join(folderPath, filePathSpan));
} }
catch (ArgumentException) catch (ArgumentException)
{ {

@ -89,6 +89,10 @@ namespace Emby.Server.Implementations.Library
// bts sync files // bts sync files
"**/*.bts", "**/*.bts",
"**/*.sync", "**/*.sync",
// zfs
"**/.zfs/**",
"**/.zfs"
}; };
private static readonly GlobOptions _globOptions = new GlobOptions private static readonly GlobOptions _globOptions = new GlobOptions

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

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

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

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

@ -1,5 +1,3 @@
#nullable disable
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -25,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
private readonly NamingOptions _namingOptions; private readonly NamingOptions _namingOptions;
private readonly IDirectoryService _directoryService; private readonly IDirectoryService _directoryService;
private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase) private static readonly string[] _ignoreFiles = new[]
{ {
"folder", "folder",
"thumb", "thumb",
@ -56,7 +54,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary> /// </summary>
/// <param name="args">The args.</param> /// <param name="args">The args.</param>
/// <returns>Trailer.</returns> /// <returns>Trailer.</returns>
protected override Photo Resolve(ItemResolveArgs args) protected override Photo? Resolve(ItemResolveArgs args)
{ {
if (!args.IsDirectory) if (!args.IsDirectory)
{ {
@ -68,10 +66,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
{ {
if (IsImageFile(args.Path, _imageProcessor)) 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 // 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) foreach (var file in files)
{ {
@ -92,32 +91,32 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null; 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); 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); => imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
internal static bool IsImageFile(string path, IImageProcessor imageProcessor) internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
{ {
ArgumentNullException.ThrowIfNull(path); ArgumentNullException.ThrowIfNull(path);
var filename = Path.GetFileNameWithoutExtension(path); var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
if (!imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
if (_ignoreFiles.Contains(filename))
{ {
return false; 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; return false;
} }
string extension = Path.GetExtension(path).TrimStart('.'); return true;
return imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase);
} }
} }
} }

@ -94,14 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#')) else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
{ {
var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine); var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine);
if (string.IsNullOrWhiteSpace(channel.Id)) channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
{
channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
else
{
channel.Id = channelIdPrefix + channel.Id.GetMD5().ToString("N", CultureInfo.InvariantCulture);
}
channel.Path = trimmedLine; channel.Path = trimmedLine;
channels.Add(channel); channels.Add(channel);

@ -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": "ইনষ্টলেশ্যন ব্যৰ্থতা"
}

@ -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"
}

@ -15,13 +15,13 @@
"Favorites": "Favoritter", "Favorites": "Favoritter",
"Folders": "Mapper", "Folders": "Mapper",
"Genres": "Genrer", "Genres": "Genrer",
"HeaderAlbumArtists": "Albums kunstnere", "HeaderAlbumArtists": "Albumkunstnere",
"HeaderContinueWatching": "Fortsæt afspilning", "HeaderContinueWatching": "Fortsæt afspilning",
"HeaderFavoriteAlbums": "Favorit albummer", "HeaderFavoriteAlbums": "Favoritalbummer",
"HeaderFavoriteArtists": "Favorit kunstnere", "HeaderFavoriteArtists": "Favoritkunstnere",
"HeaderFavoriteEpisodes": "Favorit afsnit", "HeaderFavoriteEpisodes": "Yndlingsafsnit",
"HeaderFavoriteShows": "Favorit serier", "HeaderFavoriteShows": "Yndlingsserier",
"HeaderFavoriteSongs": "Favorit sange", "HeaderFavoriteSongs": "Yndlingssange",
"HeaderLiveTV": "Live-TV", "HeaderLiveTV": "Live-TV",
"HeaderNextUp": "Næste", "HeaderNextUp": "Næste",
"HeaderRecordingGroups": "Optagelsesgrupper", "HeaderRecordingGroups": "Optagelsesgrupper",
@ -34,8 +34,8 @@
"Latest": "Seneste", "Latest": "Seneste",
"MessageApplicationUpdated": "Jellyfin Server er blevet opdateret", "MessageApplicationUpdated": "Jellyfin Server er blevet opdateret",
"MessageApplicationUpdatedTo": "Jellyfin Server er blevet opdateret til {0}", "MessageApplicationUpdatedTo": "Jellyfin Server er blevet opdateret til {0}",
"MessageNamedServerConfigurationUpdatedWithValue": "Server konfiguration sektion {0} er blevet opdateret", "MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfiguration sektion {0} er blevet opdateret",
"MessageServerConfigurationUpdated": "Server konfigurationen er blevet opdateret", "MessageServerConfigurationUpdated": "Serverkonfigurationen er blevet opdateret",
"MixedContent": "Blandet indhold", "MixedContent": "Blandet indhold",
"Movies": "Film", "Movies": "Film",
"Music": "Musik", "Music": "Musik",
@ -51,7 +51,7 @@
"NotificationOptionCameraImageUploaded": "Kamerabillede uploadet", "NotificationOptionCameraImageUploaded": "Kamerabillede uploadet",
"NotificationOptionInstallationFailed": "Installationen mislykkedes", "NotificationOptionInstallationFailed": "Installationen mislykkedes",
"NotificationOptionNewLibraryContent": "Nyt indhold tilføjet", "NotificationOptionNewLibraryContent": "Nyt indhold tilføjet",
"NotificationOptionPluginError": "Plugin fejl", "NotificationOptionPluginError": "Plugin-fejl",
"NotificationOptionPluginInstalled": "Plugin blev installeret", "NotificationOptionPluginInstalled": "Plugin blev installeret",
"NotificationOptionPluginUninstalled": "Plugin blev afinstalleret", "NotificationOptionPluginUninstalled": "Plugin blev afinstalleret",
"NotificationOptionPluginUpdateInstalled": "Opdatering til plugin blev installeret", "NotificationOptionPluginUpdateInstalled": "Opdatering til plugin blev installeret",
@ -92,26 +92,26 @@
"ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek", "ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
"ValueSpecialEpisodeName": "Special - {0}", "ValueSpecialEpisodeName": "Special - {0}",
"VersionNumber": "Version {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", "TaskDownloadMissingSubtitles": "Hent manglende undertekster",
"TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.", "TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.",
"TaskUpdatePlugins": "Opdater Plugins", "TaskUpdatePlugins": "Opdater Plugins",
"TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gamle.", "TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.",
"TaskCleanLogs": "Ryd Log mappe", "TaskCleanLogs": "Ryd Log-mappe",
"TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdateret metadata.", "TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.",
"TaskRefreshLibrary": "Scan Medie Bibliotek", "TaskRefreshLibrary": "Scan Mediebibliotek",
"TaskCleanCacheDescription": "Sletter cache filer som systemet ikke længere bruger.", "TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.",
"TaskCleanCache": "Ryd Cache mappe", "TaskCleanCache": "Ryd Cache-mappe",
"TasksChannelsCategory": "Internet Kanaler", "TasksChannelsCategory": "Internetkanaler",
"TasksApplicationCategory": "Applikation", "TasksApplicationCategory": "Applikation",
"TasksLibraryCategory": "Bibliotek", "TasksLibraryCategory": "Bibliotek",
"TasksMaintenanceCategory": "Vedligeholdelse", "TasksMaintenanceCategory": "Vedligeholdelse",
"TaskRefreshChapterImages": "Udtræk kapitel billeder", "TaskRefreshChapterImages": "Udtræk kapitelbilleder",
"TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.", "TaskRefreshChapterImagesDescription": "Laver miniaturebilleder for videoer, der har kapitler.",
"TaskRefreshChannelsDescription": "Opdater internet kanal information.", "TaskRefreshChannelsDescription": "Opdaterer information for internetkanal.",
"TaskRefreshChannels": "Opdater Kanaler", "TaskRefreshChannels": "Opdater Kanaler",
"TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end 1 dag gammel.", "TaskCleanTranscodeDescription": "Fjerner transcode-filer, som er mere end 1 dag gammel.",
"TaskCleanTranscode": "Tøm Transcode mappen", "TaskCleanTranscode": "Tøm Transcode-mappen",
"TaskRefreshPeople": "Opdater Personer", "TaskRefreshPeople": "Opdater Personer",
"TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.", "TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.",
"TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.", "TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.",
@ -121,8 +121,8 @@
"Default": "Standard", "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.", "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", "TaskOptimizeDatabase": "Optimér database",
"TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan tage lang tid.", "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", "TaskKeyframeExtractor": "Udtræk af nøglebillede",
"External": "Ekstern", "External": "Ekstern",
"HearingImpaired": "Hørehæmmet" "HearingImpaired": "Hørehæmmet"
} }

@ -3,9 +3,9 @@
"AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}", "AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
"Application": "Aplicación", "Application": "Aplicación",
"Artists": "Artistas", "Artists": "Artistas",
"AuthenticationSucceededWithUserName": "{0} identificado correctamente", "AuthenticationSucceededWithUserName": "{0} autenticado correctamente",
"Books": "Libros", "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", "Channels": "Canales",
"ChapterNameValue": "Capítulo {0}", "ChapterNameValue": "Capítulo {0}",
"Collections": "Colecciones", "Collections": "Colecciones",

@ -105,8 +105,8 @@
"TaskRefreshPeople": "Actualiser les acteurs", "TaskRefreshPeople": "Actualiser les acteurs",
"TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.", "TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
"TaskCleanLogs": "Nettoyer le répertoire des journaux", "TaskCleanLogs": "Nettoyer le répertoire des journaux",
"TaskRefreshLibraryDescription": "Scanne votre médiathèque pour trouver les nouveaux fichiers et actualise les métadonnées.", "TaskRefreshLibraryDescription": "Analyser sa médiathèque pour trouver les nouveaux fichiers et actualiser les métadonnées.",
"TaskRefreshLibrary": "Scanner la médiathèque", "TaskRefreshLibrary": "Analyser la médiathèque",
"TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.", "TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.",
"TaskRefreshChapterImages": "Extraire les images de chapitre", "TaskRefreshChapterImages": "Extraire les images de chapitre",
"TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.", "TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",

@ -3,5 +3,125 @@
"TaskOptimizeDatabase": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಆಪ್ಟಿಮೈಜ್ ಮಾಡಿ", "TaskOptimizeDatabase": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಆಪ್ಟಿಮೈಜ್ ಮಾಡಿ",
"TaskOptimizeDatabaseDescription": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಕಾಂಪ್ಯಾಕ್ಟ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮುಕ್ತ ಜಾಗವನ್ನು ಮೊಟಕುಗೊಳಿಸುತ್ತದೆ. ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿದ ನಂತರ ಈ ಕಾರ್ಯವನ್ನು ನಡೆಸುವುದು ಅಥವಾ ಡೇಟಾಬೇಸ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಸೂಚಿಸುವ ಇತರ ಬದಲಾವಣೆಗಳನ್ನು ಮಾಡುವುದರಿಂದ ಕಾರ್ಯಕ್ಷಮತೆಯನ್ನು ಸುಧಾರಿಸಬಹುದು.", "TaskOptimizeDatabaseDescription": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಕಾಂಪ್ಯಾಕ್ಟ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮುಕ್ತ ಜಾಗವನ್ನು ಮೊಟಕುಗೊಳಿಸುತ್ತದೆ. ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿದ ನಂತರ ಈ ಕಾರ್ಯವನ್ನು ನಡೆಸುವುದು ಅಥವಾ ಡೇಟಾಬೇಸ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಸೂಚಿಸುವ ಇತರ ಬದಲಾವಣೆಗಳನ್ನು ಮಾಡುವುದರಿಂದ ಕಾರ್ಯಕ್ಷಮತೆಯನ್ನು ಸುಧಾರಿಸಬಹುದು.",
"TaskKeyframeExtractor": "ಕೀಫ್ರೇಮ್ ಎಕ್ಸ್‌ಟ್ರಾಕ್ಟರ್", "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": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ."
} }

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

@ -1,5 +1,5 @@
{ {
"Albums": "Albums", "Albums": "Album",
"AppDeviceValues": "Apl: {0}, Peranti: {1}", "AppDeviceValues": "Apl: {0}, Peranti: {1}",
"Application": "Aplikasi", "Application": "Aplikasi",
"Artists": "Artis-artis", "Artists": "Artis-artis",

@ -29,5 +29,8 @@
"Forced": "Pressed", "Forced": "Pressed",
"External": "Outboard", "External": "Outboard",
"HeaderFavoriteEpisodes": "Treasured Tales", "HeaderFavoriteEpisodes": "Treasured Tales",
"HeaderFavoriteShows": "Treasured Tales" "HeaderFavoriteShows": "Treasured Tales",
"ChapterNameValue": "Piece {0}",
"HeaderFavoriteSongs": "Treasured Chimes",
"HeaderNextUp": "Incoming"
} }

@ -25,5 +25,14 @@
"Channels": "Amashaneli", "Channels": "Amashaneli",
"Books": "Izincwadi", "Books": "Izincwadi",
"Artists": "Abadlali", "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"
} }

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

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

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Data;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -11,7 +10,6 @@ using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Emby.Server.Implementations.Library; using Emby.Server.Implementations.Library;
using Jellyfin.Extensions;
using Jellyfin.Extensions.Json; using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters; using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Common; using MediaBrowser.Common;
@ -30,7 +28,7 @@ namespace Emby.Server.Implementations.Plugins
/// <summary> /// <summary>
/// Defines the <see cref="PluginManager" />. /// Defines the <see cref="PluginManager" />.
/// </summary> /// </summary>
public class PluginManager : IPluginManager public sealed class PluginManager : IPluginManager, IDisposable
{ {
private const string MetafileName = "meta.json"; 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> /// <summary>
/// Creates all the plugin instances. /// Creates all the plugin instances.
/// </summary> /// </summary>
@ -441,6 +430,15 @@ namespace Emby.Server.Implementations.Plugins
return SaveManifest(manifest, path); return SaveManifest(manifest, path);
} }
/// <inheritdoc />
public void Dispose()
{
foreach (var assemblyLoadContext in _assemblyLoadContexts)
{
assemblyLoadContext.Unload();
}
}
/// <summary> /// <summary>
/// Reconciles the manifest against any properties that exist locally in a pre-packaged meta.json found at the path. /// 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. /// If no file is found, no reconciliation occurs.

@ -36,6 +36,7 @@ using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Session; using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay; using MediaBrowser.Model.SyncPlay;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode; using Episode = MediaBrowser.Controller.Entities.TV.Episode;
@ -44,7 +45,7 @@ namespace Emby.Server.Implementations.Session
/// <summary> /// <summary>
/// Class SessionManager. /// Class SessionManager.
/// </summary> /// </summary>
public class SessionManager : ISessionManager, IDisposable public sealed class SessionManager : ISessionManager, IAsyncDisposable
{ {
private readonly IUserDataManager _userDataManager; private readonly IUserDataManager _userDataManager;
private readonly ILogger<SessionManager> _logger; private readonly ILogger<SessionManager> _logger;
@ -57,11 +58,9 @@ namespace Emby.Server.Implementations.Session
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private readonly IDeviceManager _deviceManager; private readonly IDeviceManager _deviceManager;
private readonly CancellationTokenRegistration _shutdownCallback;
/// <summary> private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections
/// The active connections. = new(StringComparer.OrdinalIgnoreCase);
/// </summary>
private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections = new(StringComparer.OrdinalIgnoreCase);
private Timer _idleTimer; private Timer _idleTimer;
@ -79,7 +78,8 @@ namespace Emby.Server.Implementations.Session
IImageProcessor imageProcessor, IImageProcessor imageProcessor,
IServerApplicationHost appHost, IServerApplicationHost appHost,
IDeviceManager deviceManager, IDeviceManager deviceManager,
IMediaSourceManager mediaSourceManager) IMediaSourceManager mediaSourceManager,
IHostApplicationLifetime hostApplicationLifetime)
{ {
_logger = logger; _logger = logger;
_eventManager = eventManager; _eventManager = eventManager;
@ -92,6 +92,7 @@ namespace Emby.Server.Implementations.Session
_appHost = appHost; _appHost = appHost;
_deviceManager = deviceManager; _deviceManager = deviceManager;
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;
_shutdownCallback = hostApplicationLifetime.ApplicationStopping.Register(OnApplicationStopping);
_deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated; _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() private void CheckDisposed()
{ {
if (_disposed) if (_disposed)
@ -980,28 +951,28 @@ namespace Emby.Server.Implementations.Session
private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed) private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
{ {
bool playedToCompletion = false; if (playbackFailed)
if (!playbackFailed)
{ {
var data = _userDataManager.GetUserData(user, item); return false;
}
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); 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; return playedToCompletion;
} }
@ -1330,32 +1301,6 @@ namespace Emby.Server.Implementations.Session
return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken); return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken);
} }
/// <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> /// <summary>
/// Adds the additional user. /// Adds the additional user.
/// </summary> /// </summary>
@ -1833,5 +1778,53 @@ namespace Emby.Server.Implementations.Session
return SendMessageToSessions(sessions, name, data, cancellationToken); 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();
}
} }
} }

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

@ -504,8 +504,7 @@ namespace Emby.Server.Implementations.Updates
private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken) private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken)
{ {
var extension = Path.GetExtension(package.SourceUrl); if (!Path.GetExtension(package.SourceUrl.AsSpan()).Equals(".zip", StringComparison.OrdinalIgnoreCase))
if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
{ {
_logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl); _logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl);
return; return;

@ -45,6 +45,8 @@ public class DynamicHlsController : BaseJellyfinApiController
private const string DefaultEventEncoderPreset = "superfast"; private const string DefaultEventEncoderPreset = "superfast";
private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls; private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0);
private readonly ILibraryManager _libraryManager; private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager; private readonly IUserManager _userManager;
private readonly IDlnaManager _dlnaManager; private readonly IDlnaManager _dlnaManager;
@ -1705,16 +1707,31 @@ public class DynamicHlsController : BaseJellyfinApiController
var audioCodec = _encodingHelper.GetAudioEncoder(state); var audioCodec = _encodingHelper.GetAudioEncoder(state);
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container); var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
// opus, dts, truehd and flac (in FFmpeg 5 and older) are experimental in mp4 muxer
var strictArgs = string.Empty;
var actualOutputAudioCodec = state.ActualOutputAudioCodec;
if (string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
|| string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
|| string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)
|| (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
&& _mediaEncoder.EncoderVersion < _minFFmpegFlacInMp4))
{
strictArgs = " -strict -2";
}
if (!state.IsOutputVideo) if (!state.IsOutputVideo)
{ {
var audioTranscodeParams = string.Empty;
// -vn to drop any video streams
audioTranscodeParams += "-vn";
if (EncodingHelper.IsCopyCodec(audioCodec)) if (EncodingHelper.IsCopyCodec(audioCodec))
{ {
return "-acodec copy -strict -2" + bitStreamArgs; return audioTranscodeParams + " -acodec copy" + bitStreamArgs + strictArgs;
} }
var audioTranscodeParams = string.Empty; audioTranscodeParams += " -acodec " + audioCodec + bitStreamArgs + strictArgs;
audioTranscodeParams += "-acodec " + audioCodec + bitStreamArgs;
var audioBitrate = state.OutputAudioBitrate; var audioBitrate = state.OutputAudioBitrate;
var audioChannels = state.OutputAudioChannels; var audioChannels = state.OutputAudioChannels;
@ -1742,21 +1759,9 @@ public class DynamicHlsController : BaseJellyfinApiController
audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture); audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
} }
audioTranscodeParams += " -vn";
return audioTranscodeParams; return audioTranscodeParams;
} }
// dts, flac, opus and truehd are experimental in mp4 muxer
var strictArgs = string.Empty;
var actualOutputAudioCodec = state.ActualOutputAudioCodec;
if (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
|| string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
|| string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
|| string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
{
strictArgs = " -strict -2";
}
if (EncodingHelper.IsCopyCodec(audioCodec)) if (EncodingHelper.IsCopyCodec(audioCodec))
{ {
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions); var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
@ -2041,9 +2046,9 @@ public class DynamicHlsController : BaseJellyfinApiController
return null; return null;
} }
var playlistFilename = Path.GetFileNameWithoutExtension(playlist); var playlistFilename = Path.GetFileNameWithoutExtension(playlist.AsSpan());
var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length); var indexString = Path.GetFileNameWithoutExtension(file.Name.AsSpan()).Slice(playlistFilename.Length);
return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture); return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
} }

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

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

@ -23,7 +23,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.LiveTv;
@ -48,7 +47,6 @@ public class LiveTvController : BaseJellyfinApiController
private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaSourceManager _mediaSourceManager;
private readonly IConfigurationManager _configurationManager; private readonly IConfigurationManager _configurationManager;
private readonly TranscodingJobHelper _transcodingJobHelper; private readonly TranscodingJobHelper _transcodingJobHelper;
private readonly ISessionManager _sessionManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="LiveTvController"/> class. /// 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="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param> /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
/// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param> /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
public LiveTvController( public LiveTvController(
ILiveTvManager liveTvManager, ILiveTvManager liveTvManager,
IUserManager userManager, IUserManager userManager,
@ -70,8 +67,7 @@ public class LiveTvController : BaseJellyfinApiController
IDtoService dtoService, IDtoService dtoService,
IMediaSourceManager mediaSourceManager, IMediaSourceManager mediaSourceManager,
IConfigurationManager configurationManager, IConfigurationManager configurationManager,
TranscodingJobHelper transcodingJobHelper, TranscodingJobHelper transcodingJobHelper)
ISessionManager sessionManager)
{ {
_liveTvManager = liveTvManager; _liveTvManager = liveTvManager;
_userManager = userManager; _userManager = userManager;
@ -81,7 +77,6 @@ public class LiveTvController : BaseJellyfinApiController
_mediaSourceManager = mediaSourceManager; _mediaSourceManager = mediaSourceManager;
_configurationManager = configurationManager; _configurationManager = configurationManager;
_transcodingJobHelper = transcodingJobHelper; _transcodingJobHelper = transcodingJobHelper;
_sessionManager = sessionManager;
} }
/// <summary> /// <summary>

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

@ -4,14 +4,12 @@ using System.ComponentModel.DataAnnotations;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Mime; using System.Net.Mime;
using System.Threading.Tasks;
using Jellyfin.Api.Attributes; using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants; using Jellyfin.Api.Constants;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net; using MediaBrowser.Common.Net;
using MediaBrowser.Controller; using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.IO; using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net; using MediaBrowser.Model.Net;
using MediaBrowser.Model.System; using MediaBrowser.Model.System;
@ -27,32 +25,36 @@ namespace Jellyfin.Api.Controllers;
/// </summary> /// </summary>
public class SystemController : BaseJellyfinApiController public class SystemController : BaseJellyfinApiController
{ {
private readonly ILogger<SystemController> _logger;
private readonly IServerApplicationHost _appHost; private readonly IServerApplicationHost _appHost;
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly INetworkManager _network; private readonly INetworkManager _networkManager;
private readonly ILogger<SystemController> _logger; private readonly ISystemManager _systemManager;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="SystemController"/> class. /// Initializes a new instance of the <see cref="SystemController"/> class.
/// </summary> /// </summary>
/// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
/// <param name="appPaths">Instance of <see cref="IServerApplicationPaths"/> interface.</param>
/// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param> /// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param> /// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
/// <param name="network">Instance of <see cref="INetworkManager"/> interface.</param> /// <param name="networkManager">Instance of <see cref="INetworkManager"/> interface.</param>
/// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param> /// <param name="systemManager">Instance of <see cref="ISystemManager"/> interface.</param>
public SystemController( public SystemController(
IServerConfigurationManager serverConfigurationManager, ILogger<SystemController> logger,
IServerApplicationHost appHost, IServerApplicationHost appHost,
IServerApplicationPaths appPaths,
IFileSystem fileSystem, IFileSystem fileSystem,
INetworkManager network, INetworkManager networkManager,
ILogger<SystemController> logger) ISystemManager systemManager)
{ {
_appPaths = serverConfigurationManager.ApplicationPaths; _logger = logger;
_appHost = appHost; _appHost = appHost;
_appPaths = appPaths;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_network = network; _networkManager = networkManager;
_logger = logger; _systemManager = systemManager;
} }
/// <summary> /// <summary>
@ -66,9 +68,7 @@ public class SystemController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult<SystemInfo> GetSystemInfo() public ActionResult<SystemInfo> GetSystemInfo()
{ => _systemManager.GetSystemInfo(Request);
return _appHost.GetSystemInfo(Request);
}
/// <summary> /// <summary>
/// Gets public information about the server. /// Gets public information about the server.
@ -78,9 +78,7 @@ public class SystemController : BaseJellyfinApiController
[HttpGet("Info/Public")] [HttpGet("Info/Public")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<PublicSystemInfo> GetPublicSystemInfo() public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
{ => _systemManager.GetPublicSystemInfo(Request);
return _appHost.GetPublicSystemInfo(Request);
}
/// <summary> /// <summary>
/// Pings the system. /// Pings the system.
@ -91,9 +89,7 @@ public class SystemController : BaseJellyfinApiController
[HttpPost("Ping", Name = "PostPingSystem")] [HttpPost("Ping", Name = "PostPingSystem")]
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<string> PingSystem() public ActionResult<string> PingSystem()
{ => _appHost.Name;
return _appHost.Name;
}
/// <summary> /// <summary>
/// Restarts the application. /// Restarts the application.
@ -107,11 +103,7 @@ public class SystemController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult RestartApplication() public ActionResult RestartApplication()
{ {
Task.Run(async () => _systemManager.Restart();
{
await Task.Delay(100).ConfigureAwait(false);
_appHost.Restart();
});
return NoContent(); return NoContent();
} }
@ -127,11 +119,7 @@ public class SystemController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status403Forbidden)] [ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult ShutdownApplication() public ActionResult ShutdownApplication()
{ {
Task.Run(async () => _systemManager.Shutdown();
{
await Task.Delay(100).ConfigureAwait(false);
await _appHost.Shutdown().ConfigureAwait(false);
});
return NoContent(); return NoContent();
} }
@ -189,7 +177,7 @@ public class SystemController : BaseJellyfinApiController
return new EndPointInfo return new EndPointInfo
{ {
IsLocal = HttpContext.IsLocal(), IsLocal = HttpContext.IsLocal(),
IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP()) IsInNetwork = _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP())
}; };
} }
@ -227,7 +215,7 @@ public class SystemController : BaseJellyfinApiController
[ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo() public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
{ {
var result = _network.GetMacAddresses() var result = _networkManager.GetMacAddresses()
.Select(i => new WakeOnLanInfo(i)); .Select(i => new WakeOnLanInfo(i));
return Ok(result); return Ok(result);
} }

@ -200,13 +200,6 @@ public class DynamicHlsHelper
if (state.VideoStream is not null && state.VideoRequest is not null) if (state.VideoStream is not null && state.VideoRequest is not null)
{ {
// Provide a workaround for the case issue between flac and fLaC.
var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString());
if (!string.IsNullOrEmpty(flacWaPlaylist))
{
builder.Append(flacWaPlaylist);
}
var encodingOptions = _serverConfigurationManager.GetEncodingOptions(); var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
// Provide SDR HEVC entrance for backward compatibility. // Provide SDR HEVC entrance for backward compatibility.
@ -236,14 +229,7 @@ public class DynamicHlsHelper
} }
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate; var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup); AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
// Provide a workaround for the case issue between flac and fLaC.
flacWaPlaylist = ApplyFlacCaseWorkaround(state, sdrPlaylist.ToString());
if (!string.IsNullOrEmpty(flacWaPlaylist))
{
builder.Append(flacWaPlaylist);
}
// Restore the video codec // Restore the video codec
state.OutputVideoCodec = "copy"; state.OutputVideoCodec = "copy";
@ -274,13 +260,6 @@ public class DynamicHlsHelper
state.VideoStream.Level = originalLevel; state.VideoStream.Level = originalLevel;
var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField); var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
builder.Append(newPlaylist); builder.Append(newPlaylist);
// Provide a workaround for the case issue between flac and fLaC.
flacWaPlaylist = ApplyFlacCaseWorkaround(state, newPlaylist);
if (!string.IsNullOrEmpty(flacWaPlaylist))
{
builder.Append(flacWaPlaylist);
}
} }
} }
@ -767,16 +746,4 @@ public class DynamicHlsHelper
newValue.ToString(), newValue.ToString(),
StringComparison.Ordinal); StringComparison.Ordinal);
} }
private string ApplyFlacCaseWorkaround(StreamState state, string srcPlaylist)
{
if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
{
return string.Empty;
}
var newPlaylist = srcPlaylist.Replace(",flac\"", ",fLaC\"", StringComparison.Ordinal);
return newPlaylist.Contains(",fLaC\"", StringComparison.Ordinal) ? newPlaylist : string.Empty;
}
} }

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

@ -191,6 +191,11 @@ public static class StreamingHelpers
state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream, state.OutputAudioChannels) ?? 0; state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream, state.OutputAudioChannels) ?? 0;
} }
if (outputAudioCodec.StartsWith("pcm_", StringComparison.Ordinal))
{
containerInternal = ".pcm";
}
state.OutputAudioCodec = outputAudioCodec; state.OutputAudioCodec = outputAudioCodec;
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.'); state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec); state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
@ -243,7 +248,7 @@ public static class StreamingHelpers
? GetOutputFileExtension(state, mediaSource) ? GetOutputFileExtension(state, mediaSource)
: ("." + state.OutputContainer); : ("." + state.OutputContainer);
state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId); state.OutputFilePath = GetOutputFilePath(state, ext, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
return state; return state;
} }
@ -418,11 +423,11 @@ public static class StreamingHelpers
/// <returns>System.String.</returns> /// <returns>System.String.</returns>
private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource) private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
{ {
var ext = Path.GetExtension(state.RequestedUrl); var ext = Path.GetExtension(state.RequestedUrl.AsSpan());
if (!string.IsNullOrEmpty(ext)) if (ext.IsEmpty)
{ {
return ext; return null;
} }
// Try to infer based on the desired video codec // Try to infer based on the desired video codec
@ -504,7 +509,7 @@ public static class StreamingHelpers
/// <param name="deviceId">The device id.</param> /// <param name="deviceId">The device id.</param>
/// <param name="playSessionId">The play session id.</param> /// <param name="playSessionId">The play session id.</param>
/// <returns>The complete file path, including the folder, for the transcoding file.</returns> /// <returns>The complete file path, including the folder, for the transcoding file.</returns>
private static string GetOutputFilePath(StreamState state, string outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId) private static string GetOutputFilePath(StreamState state, string? outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId)
{ {
var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}"; var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";

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

@ -33,8 +33,7 @@ public class RobotsRedirectionMiddleware
/// <returns>The async task.</returns> /// <returns>The async task.</returns>
public async Task Invoke(HttpContext httpContext) public async Task Invoke(HttpContext httpContext)
{ {
var localPath = httpContext.Request.Path.ToString(); if (httpContext.Request.Path.Equals("/robots.txt", StringComparison.OrdinalIgnoreCase))
if (string.Equals(localPath, "/robots.txt", StringComparison.OrdinalIgnoreCase))
{ {
_logger.LogDebug("Redirecting robots.txt request to web/robots.txt"); _logger.LogDebug("Redirecting robots.txt request to web/robots.txt");
httpContext.Response.Redirect("web/robots.txt"); httpContext.Response.Redirect("web/robots.txt");

@ -94,4 +94,40 @@ public enum PersonKind
/// A person who was the illustrator. /// A person who was the illustrator.
/// </summary> /// </summary>
Illustrator, 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
} }

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

@ -21,7 +21,6 @@ using MediaBrowser.Controller.Events;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dto; using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Users; using MediaBrowser.Model.Users;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -36,7 +35,6 @@ namespace Jellyfin.Server.Implementations.Users
{ {
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider; private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
private readonly IEventManager _eventManager; private readonly IEventManager _eventManager;
private readonly ICryptoProvider _cryptoProvider;
private readonly INetworkManager _networkManager; private readonly INetworkManager _networkManager;
private readonly IApplicationHost _appHost; private readonly IApplicationHost _appHost;
private readonly IImageProcessor _imageProcessor; private readonly IImageProcessor _imageProcessor;
@ -55,7 +53,6 @@ namespace Jellyfin.Server.Implementations.Users
/// </summary> /// </summary>
/// <param name="dbProvider">The database provider.</param> /// <param name="dbProvider">The database provider.</param>
/// <param name="eventManager">The event manager.</param> /// <param name="eventManager">The event manager.</param>
/// <param name="cryptoProvider">The cryptography provider.</param>
/// <param name="networkManager">The network manager.</param> /// <param name="networkManager">The network manager.</param>
/// <param name="appHost">The application host.</param> /// <param name="appHost">The application host.</param>
/// <param name="imageProcessor">The image processor.</param> /// <param name="imageProcessor">The image processor.</param>
@ -64,7 +61,6 @@ namespace Jellyfin.Server.Implementations.Users
public UserManager( public UserManager(
IDbContextFactory<JellyfinDbContext> dbProvider, IDbContextFactory<JellyfinDbContext> dbProvider,
IEventManager eventManager, IEventManager eventManager,
ICryptoProvider cryptoProvider,
INetworkManager networkManager, INetworkManager networkManager,
IApplicationHost appHost, IApplicationHost appHost,
IImageProcessor imageProcessor, IImageProcessor imageProcessor,
@ -73,7 +69,6 @@ namespace Jellyfin.Server.Implementations.Users
{ {
_dbProvider = dbProvider; _dbProvider = dbProvider;
_eventManager = eventManager; _eventManager = eventManager;
_cryptoProvider = cryptoProvider;
_networkManager = networkManager; _networkManager = networkManager;
_appHost = appHost; _appHost = appHost;
_imageProcessor = imageProcessor; _imageProcessor = imageProcessor;
@ -393,7 +388,7 @@ namespace Jellyfin.Server.Implementations.Users
} }
var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase)); 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); .ConfigureAwait(false);
var authenticationProvider = authResult.AuthenticationProvider; var authenticationProvider = authResult.AuthenticationProvider;
var success = authResult.Success; var success = authResult.Success;
@ -803,8 +798,7 @@ namespace Jellyfin.Server.Implementations.Users
private async Task<(IAuthenticationProvider? AuthenticationProvider, string Username, bool Success)> AuthenticateLocalUser( private async Task<(IAuthenticationProvider? AuthenticationProvider, string Username, bool Success)> AuthenticateLocalUser(
string username, string username,
string password, string password,
User? user, User? user)
string remoteEndPoint)
{ {
bool success = false; bool success = false;
IAuthenticationProvider? authenticationProvider = null; IAuthenticationProvider? authenticationProvider = null;

@ -102,9 +102,6 @@ namespace Jellyfin.Server
base.RegisterServices(serviceCollection); base.RegisterServices(serviceCollection);
} }
/// <inheritdoc />
protected override void RestartInternal() => Program.Restart();
/// <inheritdoc /> /// <inheritdoc />
protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal() protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal()
{ {
@ -114,8 +111,5 @@ namespace Jellyfin.Server
// Jellyfin.Server.Implementations // Jellyfin.Server.Implementations
yield return typeof(JellyfinDbContext).Assembly; yield return typeof(JellyfinDbContext).Assembly;
} }
/// <inheritdoc />
protected override void ShutdownInternal() => Program.Shutdown();
} }
} }

@ -59,6 +59,7 @@ namespace Jellyfin.Server.Extensions
serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupHandler>(); serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>(); serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>(); serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
return serviceCollection.AddAuthorizationCore(options => return serviceCollection.AddAuthorizationCore(options =>
{ {

@ -73,8 +73,7 @@ namespace Jellyfin.Server.Migrations.Routines
var queryResult = connection.Query("SELECT DISTINCT OfficialRating FROM TypedBaseItems"); var queryResult = connection.Query("SELECT DISTINCT OfficialRating FROM TypedBaseItems");
foreach (var entry in queryResult) foreach (var entry in queryResult)
{ {
var ratingString = entry.GetString(0); if (!entry.TryGetString(0, out var ratingString) || string.IsNullOrEmpty(ratingString))
if (string.IsNullOrEmpty(ratingString))
{ {
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating IS NULL OR OfficialRating='';"); connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating IS NULL OR OfficialRating='';");
} }

@ -4,7 +4,6 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommandLine; using CommandLine;
using Emby.Server.Implementations; using Emby.Server.Implementations;
@ -42,7 +41,6 @@ namespace Jellyfin.Server
public const string LoggingConfigFileSystem = "logging.json"; public const string LoggingConfigFileSystem = "logging.json";
private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory(); private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory();
private static CancellationTokenSource _tokenSource = new();
private static long _startTimestamp; private static long _startTimestamp;
private static ILogger _logger = NullLogger.Instance; private static ILogger _logger = NullLogger.Instance;
private static bool _restartOnShutdown; private static bool _restartOnShutdown;
@ -65,36 +63,9 @@ namespace Jellyfin.Server
.MapResult(StartApp, ErrorParsingArguments); .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) private static async Task StartApp(StartupOptions options)
{ {
_startTimestamp = Stopwatch.GetTimestamp(); _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); ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options);
// $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
@ -112,38 +83,10 @@ namespace Jellyfin.Server
StartupHelpers.InitializeLoggingFramework(startupConfig, appPaths); StartupHelpers.InitializeLoggingFramework(startupConfig, appPaths);
_logger = _loggerFactory.CreateLogger("Main"); _logger = _loggerFactory.CreateLogger("Main");
// Log uncaught exceptions to the logging instead of std error // Use the logging framework for uncaught exceptions instead of std error
AppDomain.CurrentDomain.UnhandledException -= UnhandledExceptionToConsole;
AppDomain.CurrentDomain.UnhandledException += (_, e) AppDomain.CurrentDomain.UnhandledException += (_, e)
=> _logger.LogCritical((Exception)e.ExceptionObject, "Unhandled Exception"); => _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( _logger.LogInformation(
"Jellyfin version: {Version}", "Jellyfin version: {Version}",
Assembly.GetEntryAssembly()!.GetName().Version!.ToString(3)); Assembly.GetEntryAssembly()!.GetName().Version!.ToString(3));
@ -173,12 +116,10 @@ namespace Jellyfin.Server
do do
{ {
_restartOnShutdown = false;
await StartServer(appPaths, options, startupConfig).ConfigureAwait(false); await StartServer(appPaths, options, startupConfig).ConfigureAwait(false);
if (_restartOnShutdown) if (_restartOnShutdown)
{ {
_tokenSource = new CancellationTokenSource();
_startTimestamp = Stopwatch.GetTimestamp(); _startTimestamp = Stopwatch.GetTimestamp();
} }
} while (_restartOnShutdown); } while (_restartOnShutdown);
@ -186,7 +127,7 @@ namespace Jellyfin.Server
private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig) private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig)
{ {
var appHost = new CoreAppHost( using var appHost = new CoreAppHost(
appPaths, appPaths,
_loggerFactory, _loggerFactory,
options, options,
@ -196,6 +137,7 @@ namespace Jellyfin.Server
try try
{ {
host = Host.CreateDefaultBuilder() host = Host.CreateDefaultBuilder()
.UseConsoleLifetime()
.ConfigureServices(services => appHost.Init(services)) .ConfigureServices(services => appHost.Init(services))
.ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths, _logger)) .ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths, _logger))
.ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig)) .ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig))
@ -210,7 +152,7 @@ namespace Jellyfin.Server
try try
{ {
await host.StartAsync(_tokenSource.Token).ConfigureAwait(false); await host.StartAsync().ConfigureAwait(false);
if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket()) if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket())
{ {
@ -219,22 +161,18 @@ namespace Jellyfin.Server
StartupHelpers.SetUnixSocketPermissions(startupConfig, socketPath, _logger); 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"); _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; throw;
} }
await appHost.RunStartupTasksAsync(_tokenSource.Token).ConfigureAwait(false); await appHost.RunStartupTasksAsync().ConfigureAwait(false);
_logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp)); _logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp));
// Block main thread until shutdown await host.WaitForShutdownAsync().ConfigureAwait(false);
await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false); _restartOnShutdown = appHost.ShouldRestart;
}
catch (TaskCanceledException)
{
// Don't throw on cancellation
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -257,7 +195,6 @@ namespace Jellyfin.Server
} }
} }
await appHost.DisposeAsync().ConfigureAwait(false);
host?.Dispose(); host?.Dispose();
} }
} }

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

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common namespace MediaBrowser.Common
@ -36,16 +35,15 @@ namespace MediaBrowser.Common
string SystemId { get; } string SystemId { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether this instance has pending kernel reload. /// Gets a value indicating whether this instance has pending changes requiring a restart.
/// </summary> /// </summary>
/// <value><c>true</c> if this instance has pending kernel reload; otherwise, <c>false</c>.</value> /// <value><c>true</c> if this instance has a pending restart; otherwise, <c>false</c>.</value>
bool HasPendingRestart { get; } bool HasPendingRestart { get; }
/// <summary> /// <summary>
/// Gets a value indicating whether this instance is currently shutting down. /// Gets or sets a value indicating whether the application should restart.
/// </summary> /// </summary>
/// <value><c>true</c> if this instance is shutting down; otherwise, <c>false</c>.</value> bool ShouldRestart { get; set; }
bool IsShuttingDown { get; }
/// <summary> /// <summary>
/// Gets the application version. /// Gets the application version.
@ -87,11 +85,6 @@ namespace MediaBrowser.Common
/// </summary> /// </summary>
void NotifyPendingRestart(); void NotifyPendingRestart();
/// <summary>
/// Restarts this instance.
/// </summary>
void Restart();
/// <summary> /// <summary>
/// Gets the exports. /// Gets the exports.
/// </summary> /// </summary>
@ -123,12 +116,6 @@ namespace MediaBrowser.Common
/// <returns>``0.</returns> /// <returns>``0.</returns>
T Resolve<T>(); T Resolve<T>();
/// <summary>
/// Shuts down.
/// </summary>
/// <returns>A task.</returns>
Task Shutdown();
/// <summary> /// <summary>
/// Initializes this instance. /// Initializes this instance.
/// </summary> /// </summary>

@ -29,11 +29,6 @@ namespace MediaBrowser.Common.Plugins
/// <returns>An IEnumerable{Assembly}.</returns> /// <returns>An IEnumerable{Assembly}.</returns>
IEnumerable<Assembly> LoadAssemblies(); IEnumerable<Assembly> LoadAssemblies();
/// <summary>
/// Unloads all of the assemblies.
/// </summary>
void UnloadAssemblies();
/// <summary> /// <summary>
/// Registers the plugin's services with the DI. /// Registers the plugin's services with the DI.
/// Note: DI is not yet instantiated yet. /// Note: DI is not yet instantiated yet.

@ -2,7 +2,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Jellyfin.Data.Entities; using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
@ -70,14 +69,6 @@ namespace MediaBrowser.Controller.Drawing
string? GetImageCacheTag(User user); string? GetImageCacheTag(User user);
/// <summary>
/// Processes the image.
/// </summary>
/// <param name="options">The options.</param>
/// <param name="toStream">To stream.</param>
/// <returns>Task.</returns>
Task ProcessImage(ImageProcessingOptions options, Stream toStream);
/// <summary> /// <summary>
/// Processes the image. /// Processes the image.
/// </summary> /// </summary>
@ -97,7 +88,5 @@ namespace MediaBrowser.Controller.Drawing
/// <param name="options">The options.</param> /// <param name="options">The options.</param>
/// <param name="libraryName">The library name to draw onto the collage.</param> /// <param name="libraryName">The library name to draw onto the collage.</param>
void CreateImageCollage(ImageCollageOptions options, string? libraryName); void CreateImageCollage(ImageCollageOptions options, string? libraryName);
bool SupportsTransparency(string path);
} }
} }

@ -119,7 +119,8 @@ namespace MediaBrowser.Controller.Drawing
private bool IsFormatSupported(string originalImagePath) private bool IsFormatSupported(string originalImagePath)
{ {
var ext = Path.GetExtension(originalImagePath); var ext = Path.GetExtension(originalImagePath);
return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, "." + outputFormat, StringComparison.OrdinalIgnoreCase)); ext = ext.Replace(".jpeg", ".jpg", StringComparison.OrdinalIgnoreCase);
return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, outputFormat.GetExtension(), StringComparison.OrdinalIgnoreCase));
} }
} }
} }

@ -3,6 +3,7 @@
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -29,7 +30,7 @@ namespace MediaBrowser.Controller.Entities
public class CollectionFolder : Folder, ICollectionFolder public class CollectionFolder : Folder, ICollectionFolder
{ {
private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; 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; private bool _requiresRefresh;
/// <summary> /// <summary>
@ -139,45 +140,26 @@ namespace MediaBrowser.Controller.Entities
} }
public static LibraryOptions GetLibraryOptions(string path) public static LibraryOptions GetLibraryOptions(string path)
{ => _libraryOptions.GetOrAdd(path, LoadLibraryOptions);
lock (_libraryOptions)
{
if (!_libraryOptions.TryGetValue(path, out var options))
{
options = LoadLibraryOptions(path);
_libraryOptions[path] = options;
}
return options;
}
}
public static void SaveLibraryOptions(string path, LibraryOptions options) 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); var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions);
foreach (var mediaPath in clone.PathInfos) 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() public static void OnCollectionFolderChange()
{ => _libraryOptions.Clear();
lock (_libraryOptions)
{
_libraryOptions.Clear();
}
}
public override bool IsSaveLocalMetadataEnabled() public override bool IsSaveLocalMetadataEnabled()
{ {

@ -0,0 +1,193 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Entities;
namespace MediaBrowser.Controller.Extensions;
/// <summary>
/// Provides extension methods for <see cref="XmlReader"/> to parse <see cref="BaseItem"/>'s.
/// </summary>
public static class XmlReaderExtensions
{
/// <summary>
/// Reads a trimmed string from the current node.
/// </summary>
/// <param name="reader">The <see cref="XmlReader"/>.</param>
/// <returns>The trimmed content.</returns>
public static string ReadNormalizedString(this XmlReader reader)
{
ArgumentNullException.ThrowIfNull(reader);
return reader.ReadElementContentAsString().Trim();
}
/// <summary>
/// Reads an int from the current node.
/// </summary>
/// <param name="reader">The <see cref="XmlReader"/>.</param>
/// <param name="value">The parsed <c>int</c>.</param>
/// <returns>A value indicating whether the parsing succeeded.</returns>
public static bool TryReadInt(this XmlReader reader, out int value)
{
ArgumentNullException.ThrowIfNull(reader);
return int.TryParse(reader.ReadElementContentAsString(), CultureInfo.InvariantCulture, out value);
}
/// <summary>
/// Parses a <see cref="DateTime"/> from the current node.
/// </summary>
/// <param name="reader">The <see cref="XmlReader"/>.</param>
/// <param name="value">The parsed <see cref="DateTime"/>.</param>
/// <returns>A value indicating whether the parsing succeeded.</returns>
public static bool TryReadDateTime(this XmlReader reader, out DateTime value)
{
ArgumentNullException.ThrowIfNull(reader);
return DateTime.TryParse(
reader.ReadElementContentAsString(),
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out value);
}
/// <summary>
/// Parses a <see cref="DateTime"/> from the current node.
/// </summary>
/// <param name="reader">The <see cref="XmlReader"/>.</param>
/// <param name="formatString">The date format string.</param>
/// <param name="value">The parsed <see cref="DateTime"/>.</param>
/// <returns>A value indicating whether the parsing succeeded.</returns>
public static bool TryReadDateTimeExact(this XmlReader reader, string formatString, out DateTime value)
{
ArgumentNullException.ThrowIfNull(reader);
ArgumentNullException.ThrowIfNull(formatString);
return DateTime.TryParseExact(
reader.ReadElementContentAsString(),
formatString,
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
out value);
}
/// <summary>
/// Parses a <see cref="PersonInfo"/> from the xml node.
/// </summary>
/// <param name="reader">The <see cref="XmlReader"/>.</param>
/// <returns>A <see cref="PersonInfo"/>, or <c>null</c> if none is found.</returns>
public static PersonInfo? GetPersonFromXmlNode(this XmlReader reader)
{
ArgumentNullException.ThrowIfNull(reader);
if (reader.IsEmptyElement)
{
reader.Read();
return null;
}
var name = string.Empty;
var type = PersonKind.Actor; // If type is not specified assume actor
var role = string.Empty;
int? sortOrder = null;
string? imageUrl = null;
using var subtree = reader.ReadSubtree();
subtree.MoveToContent();
subtree.Read();
while (subtree is { EOF: false, ReadState: ReadState.Interactive })
{
if (subtree.NodeType != XmlNodeType.Element)
{
subtree.Read();
continue;
}
switch (subtree.Name)
{
case "name":
case "Name":
name = subtree.ReadNormalizedString();
break;
case "role":
case "Role":
role = subtree.ReadNormalizedString();
break;
case "type":
case "Type":
Enum.TryParse(subtree.ReadElementContentAsString(), true, out type);
break;
case "order":
case "sortorder":
case "SortOrder":
if (subtree.TryReadInt(out var sortOrderVal))
{
sortOrder = sortOrderVal;
}
break;
case "thumb":
imageUrl = subtree.ReadNormalizedString();
break;
default:
subtree.Skip();
break;
}
}
if (string.IsNullOrWhiteSpace(name))
{
return null;
}
return new PersonInfo
{
Name = name,
Role = role,
Type = type,
SortOrder = sortOrder,
ImageUrl = imageUrl
};
}
/// <summary>
/// Used to split names of comma or pipe delimited genres and people.
/// </summary>
/// <param name="reader">The <see cref="XmlReader"/>.</param>
/// <returns>IEnumerable{System.String}.</returns>
public static IEnumerable<string> GetStringArray(this XmlReader reader)
{
ArgumentNullException.ThrowIfNull(reader);
var value = reader.ReadElementContentAsString();
// Only split by comma if there is no pipe in the string
// We have to be careful to not split names like Matthew, Jr.
var separator = !value.Contains('|', StringComparison.Ordinal)
&& !value.Contains(';', StringComparison.Ordinal)
? new[] { ',' }
: new[] { '|', ';' };
foreach (var part in value.Trim().Trim(separator).Split(separator))
{
if (!string.IsNullOrWhiteSpace(part))
{
yield return part.Trim();
}
}
}
/// <summary>
/// Parses a <see cref="PersonInfo"/> array from the xml node.
/// </summary>
/// <param name="reader">The <see cref="XmlReader"/>.</param>
/// <param name="personKind">The <see cref="PersonKind"/>.</param>
/// <returns>The <see cref="IEnumerable{PersonInfo}"/>.</returns>
public static IEnumerable<PersonInfo> GetPersonArray(this XmlReader reader, PersonKind personKind)
=> reader.GetStringArray()
.Select(part => new PersonInfo { Name = part, Type = personKind });
}

@ -4,7 +4,6 @@
using System.Net; using System.Net;
using MediaBrowser.Common; using MediaBrowser.Common;
using MediaBrowser.Model.System;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller namespace MediaBrowser.Controller
@ -16,8 +15,6 @@ namespace MediaBrowser.Controller
{ {
bool CoreStartupHasCompleted { get; } bool CoreStartupHasCompleted { get; }
bool CanLaunchWebBrowser { get; }
/// <summary> /// <summary>
/// Gets the HTTP server port. /// Gets the HTTP server port.
/// </summary> /// </summary>
@ -41,15 +38,6 @@ namespace MediaBrowser.Controller
/// <value>The name of the friendly.</value> /// <value>The name of the friendly.</value>
string FriendlyName { get; } string FriendlyName { get; }
/// <summary>
/// Gets the system info.
/// </summary>
/// <param name="request">The HTTP request.</param>
/// <returns>SystemInfo.</returns>
SystemInfo GetSystemInfo(HttpRequest request);
PublicSystemInfo GetPublicSystemInfo(HttpRequest request);
/// <summary> /// <summary>
/// Gets a URL specific for the request. /// Gets a URL specific for the request.
/// </summary> /// </summary>

@ -0,0 +1,34 @@
using MediaBrowser.Model.System;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller;
/// <summary>
/// A service for managing the application instance.
/// </summary>
public interface ISystemManager
{
/// <summary>
/// Gets the system info.
/// </summary>
/// <param name="request">The HTTP request.</param>
/// <returns>The <see cref="SystemInfo"/>.</returns>
SystemInfo GetSystemInfo(HttpRequest request);
/// <summary>
/// Gets the public system info.
/// </summary>
/// <param name="request">The HTTP request.</param>
/// <returns>The <see cref="PublicSystemInfo"/>.</returns>
PublicSystemInfo GetPublicSystemInfo(HttpRequest request);
/// <summary>
/// Starts the application restart process.
/// </summary>
void Restart();
/// <summary>
/// Starts the application shutdown process.
/// </summary>
void Shutdown();
}

@ -48,6 +48,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0); private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3); private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3);
private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1); private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1);
private readonly Version _minFFmpegVaapiH26xEncA53CcSei = new Version(6, 0);
private static readonly string[] _videoProfilesH264 = new[] private static readonly string[] _videoProfilesH264 = new[]
{ {
@ -547,25 +548,25 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <returns>System.Nullable{VideoCodecs}.</returns> /// <returns>System.Nullable{VideoCodecs}.</returns>
public string InferVideoCodec(string url) public string InferVideoCodec(string url)
{ {
var ext = Path.GetExtension(url); var ext = Path.GetExtension(url.AsSpan());
if (string.Equals(ext, ".asf", StringComparison.OrdinalIgnoreCase)) if (ext.Equals(".asf", StringComparison.OrdinalIgnoreCase))
{ {
return "wmv"; return "wmv";
} }
if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase)) if (ext.Equals(".webm", StringComparison.OrdinalIgnoreCase))
{ {
// TODO: this may not always mean VP8, as the codec ages // TODO: this may not always mean VP8, as the codec ages
return "vp8"; return "vp8";
} }
if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase)) if (ext.Equals(".ogg", StringComparison.OrdinalIgnoreCase) || ext.Equals(".ogv", StringComparison.OrdinalIgnoreCase))
{ {
return "theora"; return "theora";
} }
if (string.Equals(ext, ".m3u8", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ts", StringComparison.OrdinalIgnoreCase)) if (ext.Equals(".m3u8", StringComparison.OrdinalIgnoreCase) || ext.Equals(".ts", StringComparison.OrdinalIgnoreCase))
{ {
return "h264"; return "h264";
} }
@ -1079,10 +1080,10 @@ namespace MediaBrowser.Controller.MediaEncoding
&& state.SubtitleStream.IsExternal) && state.SubtitleStream.IsExternal)
{ {
var subtitlePath = state.SubtitleStream.Path; var subtitlePath = state.SubtitleStream.Path;
var subtitleExtension = Path.GetExtension(subtitlePath); var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
if (string.Equals(subtitleExtension, ".sub", StringComparison.OrdinalIgnoreCase) if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase)
|| string.Equals(subtitleExtension, ".sup", StringComparison.OrdinalIgnoreCase)) || subtitleExtension.Equals(".sup", StringComparison.OrdinalIgnoreCase))
{ {
var idxFile = Path.ChangeExtension(subtitlePath, ".idx"); var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
if (File.Exists(idxFile)) if (File.Exists(idxFile))
@ -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"; 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; return param;
} }
@ -5681,7 +5690,6 @@ namespace MediaBrowser.Controller.MediaEncoding
// Apply -analyzeduration as per the environment variable, // Apply -analyzeduration as per the environment variable,
// otherwise ffmpeg will break on certain files due to default value is 0. // otherwise ffmpeg will break on certain files due to default value is 0.
// The default value of -probesize is more than enough, so leave it as is.
var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty; var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
if (state.MediaSource.AnalyzeDurationMs > 0) if (state.MediaSource.AnalyzeDurationMs > 0)
@ -5700,6 +5708,14 @@ namespace MediaBrowser.Controller.MediaEncoding
inputModifier = inputModifier.Trim(); inputModifier = inputModifier.Trim();
// Apply -probesize if configured
var ffmpegProbeSize = _config.GetFFmpegProbeSize();
if (!string.IsNullOrEmpty(ffmpegProbeSize))
{
inputModifier += $" -probesize {ffmpegProbeSize}";
}
var userAgentParam = GetUserAgentParam(state); var userAgentParam = GetUserAgentParam(state);
if (!string.IsNullOrEmpty(userAgentParam)) if (!string.IsNullOrEmpty(userAgentParam))
@ -6024,7 +6040,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var format = string.Empty; var format = string.Empty;
var keyFrame = string.Empty; var keyFrame = string.Empty;
if (string.Equals(Path.GetExtension(outputPath), ".mp4", StringComparison.OrdinalIgnoreCase) if (Path.GetExtension(outputPath.AsSpan()).Equals(".mp4", StringComparison.OrdinalIgnoreCase)
&& state.BaseRequest.Context == EncodingContext.Streaming) && state.BaseRequest.Context == EncodingContext.Streaming)
{ {
// Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js // Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js
@ -6233,6 +6249,12 @@ namespace MediaBrowser.Controller.MediaEncoding
audioTranscodeParams.Add("-acodec " + GetAudioEncoder(state)); audioTranscodeParams.Add("-acodec " + GetAudioEncoder(state));
} }
if (GetAudioEncoder(state).StartsWith("pcm_", StringComparison.Ordinal))
{
audioTranscodeParams.Add(string.Concat("-f ", GetAudioEncoder(state).AsSpan(4)));
audioTranscodeParams.Add("-ar " + state.BaseRequest.AudioBitRate);
}
if (!string.Equals(outputCodec, "opus", StringComparison.OrdinalIgnoreCase)) if (!string.Equals(outputCodec, "opus", StringComparison.OrdinalIgnoreCase))
{ {
// opus only supports specific sampling rates // opus only supports specific sampling rates

@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.Resolvers
/// </summary> /// </summary>
/// <param name="args">The args.</param> /// <param name="args">The args.</param>
/// <returns>BaseItem.</returns> /// <returns>BaseItem.</returns>
BaseItem ResolvePath(ItemResolveArgs args); BaseItem? ResolvePath(ItemResolveArgs args);
} }
public interface IMultiItemResolver public interface IMultiItemResolver

@ -1,5 +1,3 @@
#nullable disable
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
@ -23,7 +21,7 @@ namespace MediaBrowser.Controller.Resolvers
/// </summary> /// </summary>
/// <param name="args">The args.</param> /// <param name="args">The args.</param>
/// <returns>`0.</returns> /// <returns>`0.</returns>
protected internal virtual T Resolve(ItemResolveArgs args) protected internal virtual T? Resolve(ItemResolveArgs args)
{ {
return null; return null;
} }
@ -42,7 +40,7 @@ namespace MediaBrowser.Controller.Resolvers
/// </summary> /// </summary>
/// <param name="args">The args.</param> /// <param name="args">The args.</param>
/// <returns>BaseItem.</returns> /// <returns>BaseItem.</returns>
public BaseItem ResolvePath(ItemResolveArgs args) public BaseItem? ResolvePath(ItemResolveArgs args)
{ {
var item = Resolve(args); var item = Resolve(args);

@ -232,20 +232,6 @@ namespace MediaBrowser.Controller.Session
/// <returns>Task.</returns> /// <returns>Task.</returns>
Task SendRestartRequiredNotification(CancellationToken cancellationToken); 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> /// <summary>
/// Adds the additional user. /// Adds the additional user.
/// </summary> /// </summary>

@ -9,6 +9,7 @@ using System.Xml;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
using Jellyfin.Extensions; using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@ -128,42 +129,19 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name) switch (reader.Name)
{ {
// DateCreated
case "Added": case "Added":
{ if (reader.TryReadDateTime(out var dateCreated))
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{ {
if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var added)) item.DateCreated = dateCreated;
{
item.DateCreated = added;
}
else
{
Logger.LogWarning("Invalid Added value found: {Value}", val);
}
} }
break; break;
}
case "OriginalTitle": case "OriginalTitle":
{ item.OriginalTitle = reader.ReadNormalizedString();
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrEmpty(val))
{
item.OriginalTitle = val;
}
break; break;
}
case "LocalTitle": case "LocalTitle":
item.Name = reader.ReadElementContentAsString(); item.Name = reader.ReadNormalizedString();
break; break;
case "CriticRating": case "CriticRating":
{ {
var text = reader.ReadElementContentAsString(); var text = reader.ReadElementContentAsString();
@ -177,63 +155,26 @@ namespace MediaBrowser.LocalMetadata.Parsers
} }
case "SortTitle": case "SortTitle":
{ item.ForcedSortName = reader.ReadNormalizedString();
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
item.ForcedSortName = val;
}
break; break;
}
case "Overview": case "Overview":
case "Description": case "Description":
{ item.Overview = reader.ReadNormalizedString();
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
item.Overview = val;
}
break; break;
}
case "Language": case "Language":
{ item.PreferredMetadataLanguage = reader.ReadNormalizedString();
var val = reader.ReadElementContentAsString();
item.PreferredMetadataLanguage = val;
break; break;
}
case "CountryCode": case "CountryCode":
{ item.PreferredMetadataCountryCode = reader.ReadNormalizedString();
var val = reader.ReadElementContentAsString();
item.PreferredMetadataCountryCode = val;
break; break;
}
case "PlaceOfBirth": case "PlaceOfBirth":
{ var placeOfBirth = reader.ReadNormalizedString();
var val = reader.ReadElementContentAsString(); if (!string.IsNullOrEmpty(placeOfBirth) && item is Person person)
if (!string.IsNullOrWhiteSpace(val))
{ {
if (item is Person person) person.ProductionLocations = new[] { placeOfBirth };
{
person.ProductionLocations = new[] { val };
}
} }
break; break;
}
case "LockedFields": case "LockedFields":
{ {
var val = reader.ReadElementContentAsString(); var val = reader.ReadElementContentAsString();
@ -275,10 +216,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
{ {
if (!reader.IsEmptyElement) if (!reader.IsEmptyElement)
{ {
using (var subtree = reader.ReadSubtree()) reader.Skip();
{
FetchFromCountriesNode(subtree);
}
} }
else else
{ {
@ -290,183 +228,84 @@ namespace MediaBrowser.LocalMetadata.Parsers
case "ContentRating": case "ContentRating":
case "MPAARating": case "MPAARating":
{ item.OfficialRating = reader.ReadNormalizedString();
var rating = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(rating))
{
item.OfficialRating = rating;
}
break; break;
}
case "CustomRating": case "CustomRating":
{ item.CustomRating = reader.ReadNormalizedString();
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
item.CustomRating = val;
}
break; break;
}
case "RunningTime": case "RunningTime":
{ var runtimeText = reader.ReadElementContentAsString();
var text = reader.ReadElementContentAsString(); if (!string.IsNullOrWhiteSpace(runtimeText))
if (!string.IsNullOrWhiteSpace(text))
{ {
if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime)) if (int.TryParse(runtimeText.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
{ {
item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks; item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
} }
} }
break; break;
}
case "AspectRatio": case "AspectRatio":
{ var aspectRatio = reader.ReadNormalizedString();
var val = reader.ReadElementContentAsString(); if (!string.IsNullOrEmpty(aspectRatio) && item is IHasAspectRatio hasAspectRatio)
if (!string.IsNullOrWhiteSpace(val) && item is IHasAspectRatio hasAspectRatio)
{ {
hasAspectRatio.AspectRatio = val; hasAspectRatio.AspectRatio = aspectRatio;
} }
break; break;
}
case "LockData": case "LockData":
{ item.IsLocked = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
item.IsLocked = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
}
break; break;
}
case "Network": case "Network":
{ foreach (var name in reader.GetStringArray())
foreach (var name in SplitNames(reader.ReadElementContentAsString()))
{ {
if (string.IsNullOrWhiteSpace(name))
{
continue;
}
item.AddStudio(name); item.AddStudio(name);
} }
break; break;
}
case "Director": case "Director":
{ foreach (var director in reader.GetPersonArray(PersonKind.Director))
foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Director }))
{ {
if (string.IsNullOrWhiteSpace(p.Name)) itemResult.AddPerson(director);
{
continue;
}
itemResult.AddPerson(p);
} }
break; break;
}
case "Writer": case "Writer":
{ foreach (var writer in reader.GetPersonArray(PersonKind.Writer))
foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Writer }))
{ {
if (string.IsNullOrWhiteSpace(p.Name)) itemResult.AddPerson(writer);
{
continue;
}
itemResult.AddPerson(p);
} }
break; break;
}
case "Actors": case "Actors":
{ foreach (var actor in reader.GetPersonArray(PersonKind.Actor))
var actors = reader.ReadInnerXml();
if (actors.Contains('<', StringComparison.Ordinal))
{ {
// This is one of the mis-named "Actors" full nodes created by MB2 itemResult.AddPerson(actor);
// Create a reader and pass it to the persons node processor
using var xmlReader = XmlReader.Create(new StringReader($"<Persons>{actors}</Persons>"));
FetchDataFromPersonsNode(xmlReader, itemResult);
}
else
{
// Old-style piped string
foreach (var p in SplitNames(actors).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Actor }))
{
if (string.IsNullOrWhiteSpace(p.Name))
{
continue;
}
itemResult.AddPerson(p);
}
} }
break; break;
}
case "GuestStars": case "GuestStars":
{ foreach (var guestStar in reader.GetPersonArray(PersonKind.GuestStar))
foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.GuestStar }))
{ {
if (string.IsNullOrWhiteSpace(p.Name)) itemResult.AddPerson(guestStar);
{
continue;
}
itemResult.AddPerson(p);
} }
break; break;
}
case "Trailer": case "Trailer":
{ var trailer = reader.ReadNormalizedString();
var val = reader.ReadElementContentAsString(); if (!string.IsNullOrEmpty(trailer))
if (!string.IsNullOrWhiteSpace(val))
{ {
item.AddTrailerUrl(val); item.AddTrailerUrl(trailer);
} }
break; break;
}
case "DisplayOrder": case "DisplayOrder":
{ var displayOrder = reader.ReadNormalizedString();
var val = reader.ReadElementContentAsString(); if (!string.IsNullOrEmpty(displayOrder) && item is IHasDisplayOrder hasDisplayOrder)
if (item is IHasDisplayOrder hasDisplayOrder)
{ {
if (!string.IsNullOrWhiteSpace(val)) hasDisplayOrder.DisplayOrder = displayOrder;
{
hasDisplayOrder.DisplayOrder = val;
}
} }
break; break;
}
case "Trailers": case "Trailers":
{ {
if (!reader.IsEmptyElement) if (!reader.IsEmptyElement)
@ -483,20 +322,12 @@ namespace MediaBrowser.LocalMetadata.Parsers
} }
case "ProductionYear": case "ProductionYear":
{ if (reader.TryReadInt(out var productionYear) && productionYear > 1850)
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{ {
if (int.TryParse(val, out var productionYear) && productionYear > 1850) item.ProductionYear = productionYear;
{
item.ProductionYear = productionYear;
}
} }
break; break;
}
case "Rating": case "Rating":
case "IMDBrating": case "IMDBrating":
{ {
@ -517,40 +348,24 @@ namespace MediaBrowser.LocalMetadata.Parsers
case "BirthDate": case "BirthDate":
case "PremiereDate": case "PremiereDate":
case "FirstAired": case "FirstAired":
{ if (reader.TryReadDateTimeExact("yyyy-MM-dd", out var firstAired))
var firstAired = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(firstAired))
{ {
if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out var airDate) && airDate.Year > 1850) item.PremiereDate = firstAired;
{ item.ProductionYear = firstAired.Year;
item.PremiereDate = airDate;
item.ProductionYear = airDate.Year;
}
} }
break; break;
}
case "DeathDate": case "DeathDate":
case "EndDate": case "EndDate":
{ if (reader.TryReadDateTimeExact("yyyy-MM-dd", out var endDate))
var firstAired = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(firstAired))
{ {
if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out var airDate) && airDate.Year > 1850) item.EndDate = endDate;
{
item.EndDate = airDate;
}
} }
break; break;
}
case "CollectionNumber": case "CollectionNumber":
var tmdbCollection = reader.ReadElementContentAsString(); var tmdbCollection = reader.ReadNormalizedString();
if (!string.IsNullOrWhiteSpace(tmdbCollection)) if (!string.IsNullOrEmpty(tmdbCollection))
{ {
item.SetProviderId(MetadataProvider.TmdbCollection, tmdbCollection); item.SetProviderId(MetadataProvider.TmdbCollection, tmdbCollection);
} }
@ -753,41 +568,6 @@ namespace MediaBrowser.LocalMetadata.Parsers
item.Shares = list.ToArray(); item.Shares = list.ToArray();
} }
private void FetchFromCountriesNode(XmlReader reader)
{
reader.MoveToContent();
reader.Read();
// Loop through each element
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
{
if (reader.NodeType == XmlNodeType.Element)
{
switch (reader.Name)
{
case "Country":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
}
break;
}
default:
reader.Skip();
break;
}
}
else
{
reader.Read();
}
}
}
/// <summary> /// <summary>
/// Fetches from taglines node. /// Fetches from taglines node.
/// </summary> /// </summary>
@ -806,17 +586,8 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name) switch (reader.Name)
{ {
case "Tagline": case "Tagline":
{ item.Tagline = reader.ReadNormalizedString();
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
item.Tagline = val;
}
break; break;
}
default: default:
reader.Skip(); reader.Skip();
break; break;
@ -847,17 +618,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name) switch (reader.Name)
{ {
case "Genre": case "Genre":
{ var genre = reader.ReadNormalizedString();
var genre = reader.ReadElementContentAsString(); if (!string.IsNullOrEmpty(genre))
if (!string.IsNullOrWhiteSpace(genre))
{ {
item.AddGenre(genre); item.AddGenre(genre);
} }
break; break;
}
default: default:
reader.Skip(); reader.Skip();
break; break;
@ -885,17 +652,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name) switch (reader.Name)
{ {
case "Tag": case "Tag":
{ var tag = reader.ReadNormalizedString();
var tag = reader.ReadElementContentAsString(); if (!string.IsNullOrEmpty(tag))
if (!string.IsNullOrWhiteSpace(tag))
{ {
tags.Add(tag); tags.Add(tag);
} }
break; break;
}
default: default:
reader.Skip(); reader.Skip();
break; break;
@ -929,29 +692,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
{ {
case "Person": case "Person":
case "Actor": case "Actor":
{ var person = reader.GetPersonFromXmlNode();
if (reader.IsEmptyElement) if (person is not null)
{ {
reader.Read(); item.AddPerson(person);
continue;
}
using (var subtree = reader.ReadSubtree())
{
foreach (var person in GetPersonsFromXmlNode(subtree))
{
if (string.IsNullOrWhiteSpace(person.Name))
{
continue;
}
item.AddPerson(person);
}
} }
break; break;
}
default: default:
reader.Skip(); reader.Skip();
break; break;
@ -977,17 +724,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name) switch (reader.Name)
{ {
case "Trailer": case "Trailer":
{ var trailer = reader.ReadNormalizedString();
var val = reader.ReadElementContentAsString(); if (!string.IsNullOrEmpty(trailer))
if (!string.IsNullOrWhiteSpace(val))
{ {
item.AddTrailerUrl(val); item.AddTrailerUrl(trailer);
} }
break; break;
}
default: default:
reader.Skip(); reader.Skip();
break; break;
@ -1018,17 +761,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name) switch (reader.Name)
{ {
case "Studio": case "Studio":
{ var studio = reader.ReadNormalizedString();
var studio = reader.ReadElementContentAsString(); if (!string.IsNullOrEmpty(studio))
if (!string.IsNullOrWhiteSpace(studio))
{ {
item.AddStudio(studio); item.AddStudio(studio);
} }
break; break;
}
default: default:
reader.Skip(); reader.Skip();
break; break;
@ -1041,83 +780,6 @@ namespace MediaBrowser.LocalMetadata.Parsers
} }
} }
/// <summary>
/// Gets the persons from XML node.
/// </summary>
/// <param name="reader">The reader.</param>
/// <returns>IEnumerable{PersonInfo}.</returns>
private IEnumerable<PersonInfo> GetPersonsFromXmlNode(XmlReader reader)
{
var name = string.Empty;
var type = PersonKind.Actor; // If type is not specified assume actor
var role = string.Empty;
int? sortOrder = null;
reader.MoveToContent();
reader.Read();
// Loop through each element
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
{
if (reader.NodeType == XmlNodeType.Element)
{
switch (reader.Name)
{
case "Name":
name = reader.ReadElementContentAsString();
break;
case "Type":
{
var val = reader.ReadElementContentAsString();
_ = Enum.TryParse(val, true, out type);
break;
}
case "Role":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
role = val;
}
break;
}
case "SortOrder":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal))
{
sortOrder = intVal;
}
}
break;
}
default:
reader.Skip();
break;
}
}
else
{
reader.Read();
}
}
var personInfo = new PersonInfo { Name = name.Trim(), Role = role, Type = type, SortOrder = sortOrder };
return new[] { personInfo };
}
/// <summary> /// <summary>
/// Get linked child. /// Get linked child.
/// </summary> /// </summary>
@ -1138,17 +800,11 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name) switch (reader.Name)
{ {
case "Path": case "Path":
{ linkedItem.Path = reader.ReadNormalizedString();
linkedItem.Path = reader.ReadElementContentAsString();
break; break;
}
case "ItemId": case "ItemId":
{ linkedItem.LibraryItemId = reader.ReadNormalizedString();
linkedItem.LibraryItemId = reader.ReadElementContentAsString();
break; break;
}
default: default:
reader.Skip(); reader.Skip();
break; break;
@ -1189,22 +845,14 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name) switch (reader.Name)
{ {
case "UserId": case "UserId":
{ item.UserId = reader.ReadNormalizedString();
item.UserId = reader.ReadElementContentAsString();
break; break;
}
case "CanEdit": case "CanEdit":
{
item.CanEdit = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase); item.CanEdit = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
break; break;
}
default: default:
{
reader.Skip(); reader.Skip();
break; break;
}
} }
} }
else else
@ -1221,34 +869,5 @@ namespace MediaBrowser.LocalMetadata.Parsers
return null; return null;
} }
/// <summary>
/// Used to split names of comma or pipe delimited genres and people.
/// </summary>
/// <param name="value">The value.</param>
/// <returns>IEnumerable{System.String}.</returns>
private IEnumerable<string> SplitNames(string value)
{
// Only split by comma if there is no pipe in the string
// We have to be careful to not split names like Matthew, Jr.
var separator = !value.Contains('|', StringComparison.Ordinal)
&& !value.Contains(';', StringComparison.Ordinal) ? new[] { ',' } : new[] { '|', ';' };
value = value.Trim().Trim(separator);
return string.IsNullOrWhiteSpace(value) ? Array.Empty<string>() : Split(value, separator, StringSplitOptions.RemoveEmptyEntries);
}
/// <summary>
/// Provides an additional overload for string.split.
/// </summary>
/// <param name="val">The val.</param>
/// <param name="separators">The separators.</param>
/// <param name="options">The options.</param>
/// <returns>System.String[][].</returns>
private string[] Split(string val, char[] separators, StringSplitOptions options)
{
return val.Split(separators, options);
}
} }
} }

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Xml; using System.Xml;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -30,12 +31,8 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name) switch (reader.Name)
{ {
case "PlaylistMediaType": case "PlaylistMediaType":
{ item.PlaylistMediaType = reader.ReadNormalizedString();
item.PlaylistMediaType = reader.ReadElementContentAsString();
break; break;
}
case "PlaylistItems": case "PlaylistItems":
if (!reader.IsEmptyElement) if (!reader.IsEmptyElement)
@ -94,10 +91,8 @@ namespace MediaBrowser.LocalMetadata.Parsers
} }
default: default:
{
reader.Skip(); reader.Skip();
break; break;
}
} }
} }
else else

@ -1,4 +1,3 @@
#nullable disable
#pragma warning disable CS1591 #pragma warning disable CS1591
using System; using System;
@ -23,7 +22,7 @@ using Microsoft.Extensions.Logging;
namespace MediaBrowser.MediaEncoding.Attachments namespace MediaBrowser.MediaEncoding.Attachments
{ {
public class AttachmentExtractor : IAttachmentExtractor, IDisposable public sealed class AttachmentExtractor : IAttachmentExtractor
{ {
private readonly ILogger<AttachmentExtractor> _logger; private readonly ILogger<AttachmentExtractor> _logger;
private readonly IApplicationPaths _appPaths; private readonly IApplicationPaths _appPaths;
@ -34,8 +33,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks = private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
new ConcurrentDictionary<string, SemaphoreSlim>(); new ConcurrentDictionary<string, SemaphoreSlim>();
private bool _disposed = false;
public AttachmentExtractor( public AttachmentExtractor(
ILogger<AttachmentExtractor> logger, ILogger<AttachmentExtractor> logger,
IApplicationPaths appPaths, IApplicationPaths appPaths,
@ -177,22 +174,16 @@ namespace MediaBrowser.MediaEncoding.Attachments
process.Start(); process.Start();
var ranToCompletion = await ProcessExtensions.WaitForExitAsync(process, cancellationToken).ConfigureAwait(false); try
if (!ranToCompletion)
{ {
try await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
{ exitCode = process.ExitCode;
_logger.LogWarning("Killing ffmpeg attachment extraction process"); }
process.Kill(); catch (OperationCanceledException)
} {
catch (Exception ex) process.Kill(true);
{ exitCode = -1;
_logger.LogError(ex, "Error killing attachment extraction process");
}
} }
exitCode = ranToCompletion ? process.ExitCode : -1;
} }
var failed = false; var failed = false;
@ -296,7 +287,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
ArgumentException.ThrowIfNullOrEmpty(outputPath); ArgumentException.ThrowIfNullOrEmpty(outputPath);
Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputPath)));
var processArgs = string.Format( var processArgs = string.Format(
CultureInfo.InvariantCulture, CultureInfo.InvariantCulture,
@ -325,22 +316,16 @@ namespace MediaBrowser.MediaEncoding.Attachments
process.Start(); process.Start();
var ranToCompletion = await ProcessExtensions.WaitForExitAsync(process, cancellationToken).ConfigureAwait(false); try
if (!ranToCompletion)
{ {
try await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
{ exitCode = process.ExitCode;
_logger.LogWarning("Killing ffmpeg attachment extraction process"); }
process.Kill(); catch (OperationCanceledException)
} {
catch (Exception ex) process.Kill(true);
{ exitCode = -1;
_logger.LogError(ex, "Error killing attachment extraction process");
}
} }
exitCode = ranToCompletion ? process.ExitCode : -1;
} }
var failed = false; var failed = false;
@ -391,33 +376,8 @@ namespace MediaBrowser.MediaEncoding.Attachments
filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture); filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture);
} }
var prefix = filename.Substring(0, 1); var prefix = filename.AsSpan(0, 1);
return Path.Combine(_appPaths.DataPath, "attachments", prefix, filename); return Path.Join(_appPaths.DataPath, "attachments", prefix, filename);
}
/// <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)
{
}
_disposed = true;
} }
} }
} }

@ -316,10 +316,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
{ {
var files = _fileSystem.GetFilePaths(path, recursive); var files = _fileSystem.GetFilePaths(path, recursive);
var excludeExtensions = new[] { ".c" }; return files.FirstOrDefault(i => Path.GetFileNameWithoutExtension(i.AsSpan()).Equals(filename, StringComparison.OrdinalIgnoreCase)
&& !Path.GetExtension(i.AsSpan()).Equals(".c", StringComparison.OrdinalIgnoreCase));
return files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), filename, StringComparison.OrdinalIgnoreCase)
&& !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty));
} }
catch (Exception) catch (Exception)
{ {
@ -419,6 +417,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters; var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters;
var analyzeDuration = string.Empty; var analyzeDuration = string.Empty;
var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty; var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
var ffmpegProbeSize = _config.GetFFmpegProbeSize() ?? string.Empty;
var extraArgs = string.Empty;
if (request.MediaSource.AnalyzeDurationMs > 0) if (request.MediaSource.AnalyzeDurationMs > 0)
{ {
@ -429,12 +429,22 @@ namespace MediaBrowser.MediaEncoding.Encoder
analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration; analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration;
} }
if (!string.IsNullOrEmpty(analyzeDuration))
{
extraArgs = analyzeDuration;
}
if (!string.IsNullOrEmpty(ffmpegProbeSize))
{
extraArgs += " -probesize " + ffmpegProbeSize;
}
return GetMediaInfoInternal( return GetMediaInfoInternal(
GetInputArgument(request.MediaSource.Path, request.MediaSource), GetInputArgument(request.MediaSource.Path, request.MediaSource),
request.MediaSource.Path, request.MediaSource.Path,
request.MediaSource.Protocol, request.MediaSource.Protocol,
extractChapters, extractChapters,
analyzeDuration, extraArgs,
request.MediaType == DlnaProfileType.Audio, request.MediaType == DlnaProfileType.Audio,
request.MediaSource.VideoType, request.MediaSource.VideoType,
cancellationToken); cancellationToken);
@ -640,15 +650,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
{ {
ArgumentException.ThrowIfNullOrEmpty(inputPath); ArgumentException.ThrowIfNullOrEmpty(inputPath);
var outputExtension = targetFormat switch var outputExtension = targetFormat?.GetExtension() ?? ".jpg";
{
ImageFormat.Bmp => ".bmp",
ImageFormat.Gif => ".gif",
ImageFormat.Jpg => ".jpg",
ImageFormat.Png => ".png",
ImageFormat.Webp => ".webp",
_ => ".jpg"
};
var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension); var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension);
Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath)); Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
@ -750,11 +752,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
timeoutMs = enableHdrExtraction ? DefaultHdrImageExtractionTimeout : DefaultSdrImageExtractionTimeout; timeoutMs = enableHdrExtraction ? DefaultHdrImageExtractionTimeout : DefaultSdrImageExtractionTimeout;
} }
ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false); try
{
if (!ranToCompletion) await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
ranToCompletion = true;
}
catch (OperationCanceledException)
{ {
StopProcess(processWrapper, 1000); process.Kill(true);
ranToCompletion = false;
} }
} }
finally finally
@ -989,7 +995,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
return true; return true;
} }
private class ProcessWrapper : IDisposable private sealed class ProcessWrapper : IDisposable
{ {
private readonly MediaEncoder _mediaEncoder; private readonly MediaEncoder _mediaEncoder;
@ -1032,13 +1038,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
_mediaEncoder._runningProcesses.Remove(this); _mediaEncoder._runningProcesses.Remove(this);
} }
try process.Dispose();
{
process.Dispose();
}
catch
{
}
} }
public void Dispose() public void Dispose()

@ -78,6 +78,7 @@ namespace MediaBrowser.MediaEncoding.Probing
"She/Her/Hers", "She/Her/Hers",
"5/8erl in Ehr'n", "5/8erl in Ehr'n",
"Smith/Kotzen", "Smith/Kotzen",
"We;Na",
}; };
/// <summary> /// <summary>

@ -420,23 +420,16 @@ namespace MediaBrowser.MediaEncoding.Subtitles
throw; throw;
} }
var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false); try
if (!ranToCompletion)
{ {
try await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
{ exitCode = process.ExitCode;
_logger.LogInformation("Killing ffmpeg subtitle conversion process"); }
catch (OperationCanceledException)
process.Kill(); {
} process.Kill(true);
catch (Exception ex) exitCode = -1;
{
_logger.LogError(ex, "Error killing subtitle conversion process");
}
} }
exitCode = ranToCompletion ? process.ExitCode : -1;
} }
var failed = false; var failed = false;
@ -574,23 +567,16 @@ namespace MediaBrowser.MediaEncoding.Subtitles
throw; throw;
} }
var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false); try
if (!ranToCompletion)
{ {
try await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
{ exitCode = process.ExitCode;
_logger.LogWarning("Killing ffmpeg subtitle extraction process"); }
catch (OperationCanceledException)
process.Kill(); {
} process.Kill(true);
catch (Exception ex) exitCode = -1;
{
_logger.LogError(ex, "Error killing subtitle extraction process");
}
} }
exitCode = ranToCompletion ? process.ExitCode : -1;
} }
var failed = false; var failed = false;

@ -24,4 +24,21 @@ public static class ImageFormatExtensions
ImageFormat.Webp => "image/webp", ImageFormat.Webp => "image/webp",
_ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat)) _ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat))
}; };
/// <summary>
/// Returns the correct extension for this <see cref="ImageFormat" />.
/// </summary>
/// <param name="format">This <see cref="ImageFormat" />.</param>
/// <exception cref="InvalidEnumArgumentException">The <paramref name="format"/> is an invalid enumeration value.</exception>
/// <returns>The correct extension for this <see cref="ImageFormat" />.</returns>
public static string GetExtension(this ImageFormat format)
=> format switch
{
ImageFormat.Bmp => ".bmp",
ImageFormat.Gif => ".gif",
ImageFormat.Jpg => ".jpg",
ImageFormat.Png => ".png",
ImageFormat.Webp => ".webp",
_ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat))
};
} }

@ -10,8 +10,6 @@ namespace MediaBrowser.Model.IO
/// </summary> /// </summary>
public interface IFileSystem public interface IFileSystem
{ {
void AddShortcutHandler(IShortcutHandler handler);
/// <summary> /// <summary>
/// Determines whether the specified filename is shortcut. /// Determines whether the specified filename is shortcut.
/// </summary> /// </summary>

@ -263,7 +263,11 @@ namespace MediaBrowser.Providers.Manager
var fileStreamOptions = AsyncFile.WriteOptions; var fileStreamOptions = AsyncFile.WriteOptions;
fileStreamOptions.Mode = FileMode.Create; fileStreamOptions.Mode = FileMode.Create;
fileStreamOptions.PreallocationSize = source.Length; if (source.CanSeek)
{
fileStreamOptions.PreallocationSize = source.Length;
}
var fs = new FileStream(path, fileStreamOptions); var fs = new FileStream(path, fileStreamOptions);
await using (fs.ConfigureAwait(false)) await using (fs.ConfigureAwait(false))
{ {

@ -204,16 +204,10 @@ namespace MediaBrowser.Providers.MediaInfo
? Path.GetExtension(attachmentStream.FileName) ? Path.GetExtension(attachmentStream.FileName)
: MimeTypes.ToExtension(attachmentStream.MimeType); : MimeTypes.ToExtension(attachmentStream.MimeType);
if (string.IsNullOrEmpty(extension))
{
extension = ".jpg";
}
ImageFormat format = extension switch ImageFormat format = extension switch
{ {
".bmp" => ImageFormat.Bmp, ".bmp" => ImageFormat.Bmp,
".gif" => ImageFormat.Gif, ".gif" => ImageFormat.Gif,
".jpg" => ImageFormat.Jpg,
".png" => ImageFormat.Png, ".png" => ImageFormat.Png,
".webp" => ImageFormat.Webp, ".webp" => ImageFormat.Webp,
_ => ImageFormat.Jpg _ => ImageFormat.Jpg

File diff suppressed because it is too large Load Diff

@ -1,10 +1,11 @@
using System; using System;
using System.Globalization;
using System.IO; using System.IO;
using System.Text;
using System.Threading; using System.Threading;
using System.Xml; using System.Xml;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -81,7 +82,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers
} }
// Extract the last episode number from nfo // Extract the last episode number from nfo
// Retrieves all title and plot tags from the rest of the nfo and concatenates them with the first episode
// This is needed because XBMC metadata uses multiple episodedetails blocks instead of episodenumberend tag // This is needed because XBMC metadata uses multiple episodedetails blocks instead of episodenumberend tag
var name = new StringBuilder(item.Item.Name);
var overview = new StringBuilder(item.Item.Overview);
while ((index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase)) != -1) while ((index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase)) != -1)
{ {
xml = xmlFile.Substring(0, index + srch.Length); xml = xmlFile.Substring(0, index + srch.Length);
@ -92,12 +96,44 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{ {
reader.MoveToContent(); reader.MoveToContent();
if (reader.ReadToDescendant("episode") && int.TryParse(reader.ReadElementContentAsString(), out var num)) while (!reader.EOF && reader.ReadState == ReadState.Interactive)
{ {
item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num); cancellationToken.ThrowIfCancellationRequested();
if (reader.NodeType == XmlNodeType.Element)
{
switch (reader.Name)
{
case "name":
case "title":
case "localtitle":
name.Append(" / ").Append(reader.ReadElementContentAsString());
break;
case "episode":
{
if (int.TryParse(reader.ReadElementContentAsString(), out var num))
{
item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num);
}
break;
}
case "biography":
case "plot":
case "review":
overview.Append(" / ").Append(reader.ReadElementContentAsString());
break;
}
}
reader.Read();
} }
} }
} }
item.Item.Name = name.ToString();
item.Item.Overview = overview.ToString();
} }
catch (XmlException) catch (XmlException)
{ {
@ -112,142 +148,53 @@ namespace MediaBrowser.XbmcMetadata.Parsers
switch (reader.Name) switch (reader.Name)
{ {
case "season": case "season":
if (reader.TryReadInt(out var seasonNumber))
{ {
var number = reader.ReadElementContentAsString(); item.ParentIndexNumber = seasonNumber;
if (!string.IsNullOrWhiteSpace(number))
{
if (int.TryParse(number, out var num))
{
item.ParentIndexNumber = num;
}
}
break;
} }
break;
case "episode": case "episode":
if (reader.TryReadInt(out var episodeNumber))
{ {
var number = reader.ReadElementContentAsString(); item.IndexNumber = episodeNumber;
if (!string.IsNullOrWhiteSpace(number))
{
if (int.TryParse(number, out var num))
{
item.IndexNumber = num;
}
}
break;
} }
break;
case "episodenumberend": case "episodenumberend":
if (reader.TryReadInt(out var episodeNumberEnd))
{ {
var number = reader.ReadElementContentAsString(); item.IndexNumberEnd = episodeNumberEnd;
if (!string.IsNullOrWhiteSpace(number))
{
if (int.TryParse(number, out var num))
{
item.IndexNumberEnd = num;
}
}
break;
} }
break;
case "airsbefore_episode": case "airsbefore_episode":
case "displayepisode":
if (reader.TryReadInt(out var airsBeforeEpisode))
{ {
var val = reader.ReadElementContentAsString(); item.AirsBeforeEpisodeNumber = airsBeforeEpisode;
if (!string.IsNullOrWhiteSpace(val))
{
// int.TryParse is local aware, so it can be problematic, force us culture
if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
{
item.AirsBeforeEpisodeNumber = rval;
}
}
break;
} }
break;
case "airsafter_season": case "airsafter_season":
case "displayafterseason":
if (reader.TryReadInt(out var airsAfterSeason))
{ {
var val = reader.ReadElementContentAsString(); item.AirsAfterSeasonNumber = airsAfterSeason;
if (!string.IsNullOrWhiteSpace(val))
{
// int.TryParse is local aware, so it can be problematic, force us culture
if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
{
item.AirsAfterSeasonNumber = rval;
}
}
break;
} }
break;
case "airsbefore_season": case "airsbefore_season":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
// int.TryParse is local aware, so it can be problematic, force us culture
if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
{
item.AirsBeforeSeasonNumber = rval;
}
}
break;
}
case "displayseason": case "displayseason":
if (reader.TryReadInt(out var airsBeforeSeason))
{ {
var val = reader.ReadElementContentAsString(); item.AirsBeforeSeasonNumber = airsBeforeSeason;
if (!string.IsNullOrWhiteSpace(val))
{
// int.TryParse is local aware, so it can be problematic, force us culture
if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
{
item.AirsBeforeSeasonNumber = rval;
}
}
break;
}
case "displayepisode":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
// int.TryParse is local aware, so it can be problematic, force us culture
if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var rval))
{
item.AirsBeforeEpisodeNumber = rval;
}
}
break;
} }
break;
case "showtitle": case "showtitle":
{ item.SeriesName = reader.ReadNormalizedString();
var showtitle = reader.ReadElementContentAsString(); break;
if (!string.IsNullOrWhiteSpace(showtitle))
{
item.SeriesName = showtitle;
}
break;
}
default: default:
base.FetchDataFromXmlNode(reader, itemResult); base.FetchDataFromXmlNode(reader, itemResult);
break; break;

@ -5,6 +5,7 @@ using System.Xml;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@ -113,31 +114,23 @@ namespace MediaBrowser.XbmcMetadata.Parsers
} }
case "artist": case "artist":
var artist = reader.ReadNormalizedString();
if (!string.IsNullOrEmpty(artist) && item is MusicVideo artistVideo)
{ {
var val = reader.ReadElementContentAsString(); var list = artistVideo.Artists.ToList();
list.Add(artist);
if (!string.IsNullOrWhiteSpace(val) && item is MusicVideo movie) artistVideo.Artists = list.ToArray();
{
var list = movie.Artists.ToList();
list.Add(val);
movie.Artists = list.ToArray();
}
break;
} }
break;
case "album": case "album":
var album = reader.ReadNormalizedString();
if (!string.IsNullOrEmpty(album) && item is MusicVideo albumVideo)
{ {
var val = reader.ReadElementContentAsString(); albumVideo.Album = album;
if (!string.IsNullOrWhiteSpace(val) && item is MusicVideo movie)
{
movie.Album = val;
}
break;
} }
break;
default: default:
base.FetchDataFromXmlNode(reader, itemResult); base.FetchDataFromXmlNode(reader, itemResult);
break; break;

@ -1,7 +1,7 @@
using System.Globalization;
using System.Xml; using System.Xml;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -41,32 +41,15 @@ namespace MediaBrowser.XbmcMetadata.Parsers
switch (reader.Name) switch (reader.Name)
{ {
case "seasonnumber": case "seasonnumber":
if (reader.TryReadInt(out var seasonNumber))
{ {
var number = reader.ReadElementContentAsString(); item.IndexNumber = seasonNumber;
if (!string.IsNullOrWhiteSpace(number))
{
if (int.TryParse(number, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
{
item.IndexNumber = num;
}
}
break;
} }
break;
case "seasonname": case "seasonname":
{ item.Name = reader.ReadNormalizedString();
var name = reader.ReadElementContentAsString(); break;
if (!string.IsNullOrWhiteSpace(name))
{
item.Name = name;
}
break;
}
default: default:
base.FetchDataFromXmlNode(reader, itemResult); base.FetchDataFromXmlNode(reader, itemResult);
break; break;

@ -1,9 +1,9 @@
using System; using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Xml; using System.Xml;
using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities; using MediaBrowser.Model.Entities;
@ -76,23 +76,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers
} }
case "airs_dayofweek": case "airs_dayofweek":
{ item.AirDays = TVUtils.GetAirDays(reader.ReadElementContentAsString());
item.AirDays = TVUtils.GetAirDays(reader.ReadElementContentAsString()); break;
break;
}
case "airs_time": case "airs_time":
{ item.AirTime = reader.ReadNormalizedString();
var val = reader.ReadElementContentAsString(); break;
if (!string.IsNullOrWhiteSpace(val))
{
item.AirTime = val;
}
break;
}
case "status": case "status":
{ {
var status = reader.ReadElementContentAsString(); var status = reader.ReadElementContentAsString();

@ -60,13 +60,13 @@ namespace MediaBrowser.XbmcMetadata.Savers
} }
else else
{ {
yield return Path.ChangeExtension(item.Path, ".nfo");
// only allow movie object to read movie.nfo, not owned videos (which will be itemtype video, not movie) // only allow movie object to read movie.nfo, not owned videos (which will be itemtype video, not movie)
if (!item.IsInMixedFolder && item.ItemType == typeof(Movie)) if (!item.IsInMixedFolder && item.ItemType == typeof(Movie))
{ {
yield return Path.Combine(item.ContainingFolderPath, "movie.nfo"); yield return Path.Combine(item.ContainingFolderPath, "movie.nfo");
} }
yield return Path.ChangeExtension(item.Path, ".nfo");
} }
} }

@ -1,3 +1,4 @@
#!/bin/sh
### BEGIN INIT INFO ### BEGIN INIT INFO
# Provides: Jellyfin Media Server # Provides: Jellyfin Media Server
# Required-Start: $local_fs $network # Required-Start: $local_fs $network

@ -8,7 +8,7 @@ EnvironmentFile = /etc/sysconfig/jellyfin
User = jellyfin User = jellyfin
Group = jellyfin Group = jellyfin
WorkingDirectory = /var/lib/jellyfin WorkingDirectory = /var/lib/jellyfin
ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS
Restart = on-failure Restart = on-failure
TimeoutSec = 15 TimeoutSec = 15
SuccessExitStatus=0 143 SuccessExitStatus=0 143

@ -75,6 +75,7 @@ dotnet publish --configuration Release --self-contained --runtime %{dotnet_runti
%{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir} %{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir}
%{__cp} -r Jellyfin.Server/bin/Release/net7.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin %{__cp} -r Jellyfin.Server/bin/Release/net7.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin
%{__install} -D %{SOURCE10} %{buildroot}%{_bindir}/jellyfin %{__install} -D %{SOURCE10} %{buildroot}%{_bindir}/jellyfin
sed -i -e 's|/usr/lib64|%{_libdir}|g' %{buildroot}%{_bindir}/jellyfin
# Jellyfin config # Jellyfin config
%{__install} -D Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json %{__install} -D Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json

@ -188,7 +188,7 @@ public class SkiaEncoder : IImageEncoder
return path; return path;
} }
var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path)); var tempPath = Path.Combine(_appPaths.TempDirectory, string.Concat(Guid.NewGuid().ToString(), Path.GetExtension(path.AsSpan())));
var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid."); var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid.");
Directory.CreateDirectory(directory); Directory.CreateDirectory(directory);
File.Copy(path, tempPath, true); File.Copy(path, tempPath, true);
@ -200,20 +200,10 @@ public class SkiaEncoder : IImageEncoder
{ {
if (!orientation.HasValue) if (!orientation.HasValue)
{ {
return SKEncodedOrigin.TopLeft; return SKEncodedOrigin.Default;
} }
return orientation.Value switch return (SKEncodedOrigin)orientation.Value;
{
ImageOrientation.TopRight => SKEncodedOrigin.TopRight,
ImageOrientation.RightTop => SKEncodedOrigin.RightTop,
ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom,
ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop,
ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom,
ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight,
ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft,
_ => SKEncodedOrigin.TopLeft
};
} }
/// <summary> /// <summary>

@ -38,25 +38,25 @@ public partial class StripCollageBuilder
{ {
ArgumentNullException.ThrowIfNull(outputPath); ArgumentNullException.ThrowIfNull(outputPath);
var ext = Path.GetExtension(outputPath); var ext = Path.GetExtension(outputPath.AsSpan());
if (string.Equals(ext, ".jpg", StringComparison.OrdinalIgnoreCase) if (ext.Equals(".jpg", StringComparison.OrdinalIgnoreCase)
|| string.Equals(ext, ".jpeg", StringComparison.OrdinalIgnoreCase)) || ext.Equals(".jpeg", StringComparison.OrdinalIgnoreCase))
{ {
return SKEncodedImageFormat.Jpeg; return SKEncodedImageFormat.Jpeg;
} }
if (string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase)) if (ext.Equals(".webp", StringComparison.OrdinalIgnoreCase))
{ {
return SKEncodedImageFormat.Webp; return SKEncodedImageFormat.Webp;
} }
if (string.Equals(ext, ".gif", StringComparison.OrdinalIgnoreCase)) if (ext.Equals(".gif", StringComparison.OrdinalIgnoreCase))
{ {
return SKEncodedImageFormat.Gif; return SKEncodedImageFormat.Gif;
} }
if (string.Equals(ext, ".bmp", StringComparison.OrdinalIgnoreCase)) if (ext.Equals(".bmp", StringComparison.OrdinalIgnoreCase))
{ {
return SKEncodedImageFormat.Bmp; return SKEncodedImageFormat.Bmp;
} }

@ -107,22 +107,10 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
/// <inheritdoc /> /// <inheritdoc />
public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation; public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation;
/// <inheritdoc />
public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
{
var file = await ProcessImage(options).ConfigureAwait(false);
using var fileStream = AsyncFile.OpenRead(file.Path);
await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
}
/// <inheritdoc /> /// <inheritdoc />
public IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats() public IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats()
=> _imageEncoder.SupportedOutputFormats; => _imageEncoder.SupportedOutputFormats;
/// <inheritdoc />
public bool SupportsTransparency(string path)
=> _transparentImageTypes.Contains(Path.GetExtension(path));
/// <inheritdoc /> /// <inheritdoc />
public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options) public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options)
{ {
@ -224,7 +212,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
} }
} }
return (cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath)); return (cacheFilePath, outputFormat.GetMimeType(), _fileSystem.GetLastWriteTimeUtc(cacheFilePath));
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -262,17 +250,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
return ImageFormat.Jpg; return ImageFormat.Jpg;
} }
private string GetMimeType(ImageFormat format, string path)
=> format switch
{
ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
ImageFormat.Gif => MimeTypes.GetMimeType("i.gif"),
ImageFormat.Jpg => MimeTypes.GetMimeType("i.jpg"),
ImageFormat.Png => MimeTypes.GetMimeType("i.png"),
ImageFormat.Webp => MimeTypes.GetMimeType("i.webp"),
_ => MimeTypes.GetMimeType(path)
};
/// <summary> /// <summary>
/// Gets the cache file path based on a set of parameters. /// Gets the cache file path based on a set of parameters.
/// </summary> /// </summary>
@ -374,7 +351,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
filename.Append(",v="); filename.Append(",v=");
filename.Append(Version); filename.Append(Version);
return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant()); return GetCachePath(ResizedImageCachePath, filename.ToString(), format.GetExtension());
} }
/// <inheritdoc /> /// <inheritdoc />
@ -471,35 +448,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
return Task.FromResult((originalImagePath, dateModified)); return Task.FromResult((originalImagePath, dateModified));
} }
// TODO _mediaEncoder.ConvertImage is not implemented
// if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat))
// {
// try
// {
// string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
//
// string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png";
// var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
//
// var file = _fileSystem.GetFileInfo(outputPath);
// if (!file.Exists)
// {
// await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
// dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
// }
// else
// {
// dateModified = file.LastWriteTimeUtc;
// }
//
// originalImagePath = outputPath;
// }
// catch (Exception ex)
// {
// _logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath);
// }
// }
return Task.FromResult((originalImagePath, dateModified)); return Task.FromResult((originalImagePath, dateModified));
} }

@ -30,4 +30,17 @@ public static class ImageFormatExtensionsTests
[InlineData((ImageFormat)5)] [InlineData((ImageFormat)5)]
public static void GetMimeType_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format) public static void GetMimeType_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format)
=> Assert.Throws<InvalidEnumArgumentException>(() => format.GetMimeType()); => Assert.Throws<InvalidEnumArgumentException>(() => format.GetMimeType());
[Theory]
[MemberData(nameof(GetAllImageFormats))]
public static void GetExtension_Valid_Valid(ImageFormat format)
=> Assert.Null(Record.Exception(() => format.GetExtension()));
[Theory]
[InlineData((ImageFormat)int.MinValue)]
[InlineData((ImageFormat)int.MaxValue)]
[InlineData((ImageFormat)(-1))]
[InlineData((ImageFormat)5)]
public static void GetExtension_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format)
=> Assert.Throws<InvalidEnumArgumentException>(() => format.GetExtension());
} }

@ -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./epic.flac", false)]
[InlineData("/media/music/Foo B.A.R", false)] [InlineData("/media/music/Foo B.A.R", 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) public void PathIgnored(string path, bool expected)
{ {
Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path)); Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path));

@ -15,8 +15,8 @@ namespace Jellyfin.Server.Integration.Tests
{ {
public static class AuthHelper public static class AuthHelper
{ {
public const string AuthHeaderName = "X-Emby-Authorization"; public const string AuthHeaderName = "Authorization";
public const string DummyAuthHeader = "MediaBrowser Client=\"Jellyfin.Server Integration Tests\", DeviceId=\"69420\", Device=\"Apple II\", Version=\"10.8.0\""; public const string DummyAuthHeader = "MediaBrowser Client=\"Jellyfin.Server%20Integration%20Tests\", DeviceId=\"69420\", Device=\"Apple%20II\", Version=\"10.8.0\"";
public static async Task<string> CompleteStartupAsync(HttpClient client) public static async Task<string> CompleteStartupAsync(HttpClient client)
{ {
@ -27,16 +27,19 @@ namespace Jellyfin.Server.Integration.Tests
using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>())); using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>()));
Assert.Equal(HttpStatusCode.NoContent, completeResponse.StatusCode); Assert.Equal(HttpStatusCode.NoContent, completeResponse.StatusCode);
using var content = JsonContent.Create( using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/Users/AuthenticateByName");
httpRequest.Headers.TryAddWithoutValidation(AuthHeaderName, DummyAuthHeader);
httpRequest.Content = JsonContent.Create(
new AuthenticateUserByName() new AuthenticateUserByName()
{ {
Username = user!.Name, Username = user!.Name,
Pw = user.Password, Pw = user.Password,
}, },
options: jsonOptions); options: jsonOptions);
content.Headers.Add("X-Emby-Authorization", DummyAuthHeader);
using var authResponse = await client.PostAsync("/Users/AuthenticateByName", content); using var authResponse = await client.SendAsync(httpRequest);
authResponse.EnsureSuccessStatusCode();
var auth = await JsonSerializer.DeserializeAsync<AuthenticationResultDto>( var auth = await JsonSerializer.DeserializeAsync<AuthenticationResultDto>(
await authResponse.Content.ReadAsStreamAsync(), await authResponse.Content.ReadAsStreamAsync(),
jsonOptions); jsonOptions);

@ -0,0 +1,26 @@
using System.Net;
using System.Threading.Tasks;
using Xunit;
namespace Jellyfin.Server.Integration.Tests.Controllers;
public class PersonsControllerTests : IClassFixture<JellyfinApplicationFactory>
{
private readonly JellyfinApplicationFactory _factory;
private static string? _accessToken;
public PersonsControllerTests(JellyfinApplicationFactory factory)
{
_factory = factory;
}
[Fact]
public async Task GetPerson_DoesntExist_NotFound()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
using var response = await client.GetAsync($"Persons/DoesntExist");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
}

@ -2,7 +2,6 @@ using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Threading;
using Emby.Server.Implementations; using Emby.Server.Implementations;
using Jellyfin.Server.Extensions; using Jellyfin.Server.Extensions;
using Jellyfin.Server.Helpers; using Jellyfin.Server.Helpers;
@ -105,7 +104,7 @@ namespace Jellyfin.Server.Integration.Tests
var appHost = (TestAppHost)testServer.Services.GetRequiredService<IApplicationHost>(); var appHost = (TestAppHost)testServer.Services.GetRequiredService<IApplicationHost>();
appHost.ServiceProvider = testServer.Services; appHost.ServiceProvider = testServer.Services;
appHost.InitializeServices().GetAwaiter().GetResult(); appHost.InitializeServices().GetAwaiter().GetResult();
appHost.RunStartupTasksAsync(CancellationToken.None).GetAwaiter().GetResult(); appHost.RunStartupTasksAsync().GetAwaiter().GetResult();
return testServer; return testServer;
} }

@ -1,4 +1,4 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using Jellyfin.Data.Enums; using Jellyfin.Data.Enums;
@ -114,11 +114,11 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
_parser.Fetch(result, "Test Data/Rising.nfo", CancellationToken.None); _parser.Fetch(result, "Test Data/Rising.nfo", CancellationToken.None);
var item = result.Item; var item = result.Item;
Assert.Equal("Rising (1)", item.Name); Assert.Equal("Rising (1) / Rising (2)", item.Name);
Assert.Equal(1, item.IndexNumber); Assert.Equal(1, item.IndexNumber);
Assert.Equal(2, item.IndexNumberEnd); Assert.Equal(2, item.IndexNumberEnd);
Assert.Equal(1, item.ParentIndexNumber); Assert.Equal(1, item.ParentIndexNumber);
Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy.", item.Overview); Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy. / Sheppard tries to convince Weir to mount a rescue mission to free Colonel Sumner, Teyla, and the others captured by the Wraith.", item.Overview);
Assert.Equal(new DateTime(2004, 7, 16), item.PremiereDate); Assert.Equal(new DateTime(2004, 7, 16), item.PremiereDate);
Assert.Equal(2004, item.ProductionYear); Assert.Equal(2004, item.ProductionYear);
} }

Loading…
Cancel
Save