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'
- name: Initialize CodeQL
uses: github/codeql-action/init@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # v2.21.8
uses: github/codeql-action/init@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@6a28655e3dcb49cb0840ea372fd6d17733edd8a4 # v2.21.8
uses: github/codeql-action/autobuild@fdcae64e1484d349b3366718cdfef3d404390e85 # v2.22.1
- 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:
schedule:
- cron: '30 1 * * *'
- cron: '30 */12 * * *'
workflow_dispatch:
permissions:
issues: write
pull-requests: write
actions: write
jobs:
issues:
name: Check issues
name: Check for stale issues
runs-on: ubuntu-latest
if: ${{ contains(github.repository, 'jellyfin/') }}
steps:
@ -26,11 +27,11 @@ jobs:
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
stale-issue-label: stale
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.
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).
If you have any questions you can use one of several ways to [contact us](https://jellyfin.org/contact).
close-issue-message: |-
This issue was closed due to inactivity.
prs-conflicts:
name: Check PRs with merge conflicts

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

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

@ -4,7 +4,7 @@
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=7.0
FROM node:lts-alpine as web-builder
FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \

@ -5,7 +5,7 @@
ARG DOTNET_VERSION=7.0
FROM node:lts-alpine as web-builder
FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \

@ -5,7 +5,7 @@
ARG DOTNET_VERSION=7.0
FROM node:lts-alpine as web-builder
FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,5 +1,3 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.IO;
@ -25,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
private readonly NamingOptions _namingOptions;
private readonly IDirectoryService _directoryService;
private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
private static readonly string[] _ignoreFiles = new[]
{
"folder",
"thumb",
@ -56,7 +54,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>Trailer.</returns>
protected override Photo Resolve(ItemResolveArgs args)
protected override Photo? Resolve(ItemResolveArgs args)
{
if (!args.IsDirectory)
{
@ -68,10 +66,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
if (IsImageFile(args.Path, _imageProcessor))
{
var filename = Path.GetFileNameWithoutExtension(args.Path);
var filename = Path.GetFileNameWithoutExtension(args.Path.AsSpan());
// Make sure the image doesn't belong to a video file
var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path));
var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path)
?? throw new InvalidOperationException("Path can't be a root directory."));
foreach (var file in files)
{
@ -92,32 +91,32 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null;
}
internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, string imageFilename)
internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, ReadOnlySpan<char> imageFilename)
{
return VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename);
}
internal static bool IsOwnedByResolvedMedia(string file, string imageFilename)
internal static bool IsOwnedByResolvedMedia(ReadOnlySpan<char> file, ReadOnlySpan<char> imageFilename)
=> imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
{
ArgumentNullException.ThrowIfNull(path);
var filename = Path.GetFileNameWithoutExtension(path);
if (_ignoreFiles.Contains(filename))
var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
if (!imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (_ignoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1))
var filename = Path.GetFileNameWithoutExtension(path);
if (_ignoreFiles.Any(i => filename.StartsWith(i, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
string extension = Path.GetExtension(path).TrimStart('.');
return imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase);
return true;
}
}
}

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

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

@ -3,9 +3,9 @@
"AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
"Application": "Aplicación",
"Artists": "Artistas",
"AuthenticationSucceededWithUserName": "{0} identificado correctamente",
"AuthenticationSucceededWithUserName": "{0} autenticado correctamente",
"Books": "Libros",
"CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}",
"CameraImageUploadedFrom": "Se ha subido una nueva imagen por cámara desde {0}",
"Channels": "Canales",
"ChapterNameValue": "Capítulo {0}",
"Collections": "Colecciones",

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

@ -3,5 +3,125 @@
"TaskOptimizeDatabase": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಆಪ್ಟಿಮೈಜ್ ಮಾಡಿ",
"TaskOptimizeDatabaseDescription": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಕಾಂಪ್ಯಾಕ್ಟ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮುಕ್ತ ಜಾಗವನ್ನು ಮೊಟಕುಗೊಳಿಸುತ್ತದೆ. ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿದ ನಂತರ ಈ ಕಾರ್ಯವನ್ನು ನಡೆಸುವುದು ಅಥವಾ ಡೇಟಾಬೇಸ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಸೂಚಿಸುವ ಇತರ ಬದಲಾವಣೆಗಳನ್ನು ಮಾಡುವುದರಿಂದ ಕಾರ್ಯಕ್ಷಮತೆಯನ್ನು ಸುಧಾರಿಸಬಹುದು.",
"TaskKeyframeExtractor": "ಕೀಫ್ರೇಮ್ ಎಕ್ಸ್‌ಟ್ರಾಕ್ಟರ್",
"TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್‌ಗಳಿಂದ ಕೀಫ್ರೇಮ್‌ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು."
"TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್‌ಗಳಿಂದ ಕೀಫ್ರೇಮ್‌ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು.",
"ValueHasBeenAddedToLibrary": "{0} ಅನ್ನು ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಗೆ ಸೇರಿಸಲಾಗಿದೆ",
"ValueSpecialEpisodeName": "ವಿಶೇಷ - {0}",
"TasksLibraryCategory": "ಸಮೊಹ",
"TasksApplicationCategory": "ಅಪ್ಲಿಕೇಶನ್",
"TasksChannelsCategory": "ಇಂಟರ್ನೆಟ್ ಚಾನೆಲ್ಗಳು",
"TaskCleanCache": "ಕ್ಲೀನ್ ಕ್ಯಾಶ ಡೈರೆಕ್ಟರಿ",
"TaskCleanCacheDescription": "ಸಿಸ್ಟಮ್‌ಗೆ ಇನ್ನು ಮುಂದೆ ಅಗತ್ಯವಿಲ್ಲದ ಸಂಗ್ರಹ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
"TaskRefreshLibrary": "ಸ್ಕ್ಯಾನ್ ಮೀಡಿಯಾ ಲೈಬ್ರರಿ",
"UserOfflineFromDevice": "{1} ನಿಂದ {0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
"Albums": "ಸಂಪುಟ",
"Application": "ಅಪ್ಲಿಕೇಶನ್",
"AppDeviceValues": "ಅಪ್ಲಿಕೇಶನ್: {0}, ಸಾಧನ: {1}",
"Artists": "ಕಲಾವಿದರು",
"AuthenticationSucceededWithUserName": "{0} ಯಶಸ್ವಿಯಾಗಿ ದೃಢೀಕರಿಸಲಾಗಿದೆ",
"Books": "ಪುಸ್ತಕಗಳು",
"ChapterNameValue": "ಅಧ್ಯಾಯ {0}",
"Collections": "ಸಂಗ್ರಹಣೆಗಳು",
"Default": "ಪೂರ್ವನಿಯೋಜಿತ",
"DeviceOfflineWithName": "{0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
"DeviceOnlineWithName": "{0} ಸಂಪರ್ಕಗೊಂಡಿದೆ",
"External": "ಹೊರಗಿನ",
"FailedLoginAttemptWithUserName": "{0} ರಿಂದ ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ",
"Favorites": "ಮೆಚ್ಚಿನವುಗಳು",
"Folders": "ಫೋಲ್ಡರ್‌ಗಳು",
"Forced": "ಬಲವಂತವಾಗಿ",
"Genres": "ಪ್ರಕಾರಗಳು",
"HeaderContinueWatching": "ನೋಡುವುದನ್ನು ಮುಂದುವರಿಸಿ",
"HeaderFavoriteAlbums": "ಮೆಚ್ಚಿನ ಸಂಪುಟಗಳು",
"HeaderFavoriteArtists": "ಮೆಚ್ಚಿನ ಕಲಾವಿದರು",
"HeaderFavoriteShows": "ಮೆಚ್ಚಿನ ಪ್ರದರ್ಶನಗಳು",
"HeaderFavoriteSongs": "ಮೆಚ್ಚಿನ ಹಾಡುಗಳು",
"HeaderLiveTV": "ನೇರ ದೂರದರ್ಶನ",
"HeaderNextUp": "ಮುಂದೆ",
"HeaderRecordingGroups": "ರೆಕಾರ್ಡಿಂಗ್ ಗುಂಪುಗಳು",
"MessageApplicationUpdated": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
"CameraImageUploadedFrom": "ಹೊಸ ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು {0} ನಿಂದ ಅಪ್‌ಲೋಡ್ ಮಾಡಲಾಗಿದೆ",
"Channels": "ಮೂಲಗಳು",
"HeaderAlbumArtists": "ಸಂಪುಟ ಕಲಾವಿದರು",
"HeaderFavoriteEpisodes": "ಮೆಚ್ಚಿನ ಸಂಚಿಕೆಗಳು",
"HearingImpaired": "ಮೂಗ",
"ItemAddedWithName": "{0} ಅನ್ನು ಸಂಕಲನಕ್ಕೆ ಸೇರಿಸಲಾಗಿದೆ",
"MessageApplicationUpdatedTo": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ",
"MessageNamedServerConfigurationUpdatedWithValue": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ವಿಭಾಗ {0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
"NewVersionIsAvailable": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್‌ನ ಹೊಸ ಆವೃತ್ತಿಯು ಡೌನ್‌ಲೋಡ್‌ಗೆ ಲಭ್ಯವಿದೆ.",
"NotificationOptionAudioPlayback": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ",
"NotificationOptionCameraImageUploaded": "ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು ಅಪ್ಲೋಡ್ ಮಾಡಲಾಗಿದೆ",
"NotificationOptionPluginUninstalled": "ಪ್ಲಗಿನ್ ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ",
"NotificationOptionUserLockedOut": "ಬಳಕೆದಾರರು ಲಾಕ್ ಔಟ್ ಆಗಿದ್ದಾರೆ",
"NotificationOptionVideoPlaybackStopped": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ",
"PluginUninstalledWithName": "{0} ಅನ್ನು ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ",
"ScheduledTaskFailedWithName": "{0} ವಿಫಲವಾಗಿದೆ",
"ScheduledTaskStartedWithName": "{0} ಪ್ರಾರಂಭವಾಯಿತು",
"ServerNameNeedsToBeRestarted": "{0} ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಬೇಕಾಗಿದೆ",
"UserCreatedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ರಚಿಸಲಾಗಿದೆ",
"UserLockedOutWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಲಾಕ್ ಮಾಡಲಾಗಿದೆ",
"UserOnlineFromDevice": "{1} ನಿಂದ {0} ಆನ್‌ಲೈನ್‌ನಲ್ಲಿದೆ",
"UserPasswordChangedWithName": "{0} ಬಳಕೆದಾರರಿಗಾಗಿ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ಬದಲಾಯಿಸಲಾಗಿದೆ",
"UserPolicyUpdatedWithName": "ಬಳಕೆದಾರರ ನೀತಿಯನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ",
"UserStartedPlayingItemWithValues": "{2} ರಂದು {0} ಆಡುತ್ತಿದೆ {1}",
"UserStoppedPlayingItemWithValues": "{0} ಅವರು {1} ಅನ್ನು {2} ನಲ್ಲಿ ಆಡುವುದನ್ನು ಮುಗಿಸಿದ್ದಾರೆ",
"VersionNumber": "ಆವೃತ್ತಿ {0}",
"TasksMaintenanceCategory": "ನಿರ್ವಹಣೆ",
"TaskCleanActivityLog": "ಕ್ಲೀನ್ ಚಟುವಟಿಕೆ ಲಾಗ್",
"TaskCleanActivityLogDescription": "ಕಾನ್ಫಿಗರ್ ಮಾಡಿದ ವಯಸ್ಸಿಗಿಂತ ಹಳೆಯದಾದ ಚಟುವಟಿಕೆ ಲಾಗ್ ನಮೂದುಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
"TaskRefreshChapterImages": "ಅಧ್ಯಾಯ ಚಿತ್ರಗಳನ್ನು ಹೊರತೆಗೆಯಿರಿ",
"TaskRefreshChapterImagesDescription": "ಅಧ್ಯಾಯಗಳನ್ನು ಹೊಂದಿರುವ ವೀಡಿಯೊಗಳಿಗಾಗಿ ಥಂಬ್‌ನೇಲ್‌ಗಳನ್ನು ರಚಿಸುತ್ತದೆ.",
"TaskRefreshLibraryDescription": "ಹೊಸ ಫೈಲ್‌ಗಳಿಗಾಗಿ ನಿಮ್ಮ ಮೀಡಿಯಾ ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮೆಟಾಡೇಟಾವನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ.",
"TaskCleanLogsDescription": "{0} ದಿನಗಳಿಗಿಂತ ಹಳೆಯದಾದ ಲಾಗ್ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
"TaskUpdatePluginsDescription": "ಸ್ವಯಂಚಾಲಿತವಾಗಿ ನವೀಕರಿಸಲು ಕಾನ್ಫಿಗರ್ ಮಾಡಲಾದ ಪ್ಲಗಿನ್‌ಗಳಿಗಾಗಿ ನವೀಕರಣಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಸ್ಥಾಪಿಸುತ್ತದೆ.",
"TaskCleanTranscodeDescription": "ಒಂದು ದಿನಕ್ಕಿಂತ ಹಳೆಯದಾದ ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
"TaskDownloadMissingSubtitles": "ಕಾಣೆಯಾದ ಉಪಶೀರ್ಷಿಕೆಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಿ",
"Shows": "ಧಾರವಾಹಿಗಳು",
"Songs": "ಹಾಡುಗಳು",
"StartupEmbyServerIsLoading": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಲೋಡ್ ಆಗುತ್ತಿದೆ. ದಯವಿಟ್ಟು ಸ್ವಲ್ಪ ಸಮಯದ ನಂತರ ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.",
"UserDeletedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಅಳಿಸಲಾಗಿದೆ",
"UserDownloadingItemWithValues": "{0} ಡೌನ್‌ಲೋಡ್ ಆಗುತ್ತಿದೆ {1}",
"SubtitleDownloadFailureFromForItem": "ಉಪಶೀರ್ಷಿಕೆಗಳು {0} ನಿಂದ {1} ಗಾಗಿ ಡೌನ್‌ಲೋಡ್ ಮಾಡಲು ವಿಫಲವಾಗಿವೆ",
"Sync": "ಹೊಂದಿಕೆ",
"System": "ವ್ಯವಸ್ಥೆ",
"TvShows": "ದೂರದರ್ಶನ ಕಾರ್ಯಕ್ರಮಗಳು",
"Undefined": "ವ್ಯಾಖ್ಯಾನಿಸಲಾಗಿಲ್ಲ",
"User": "ಬಳಕೆದಾರ",
"HomeVideos": "ಮುಖಪುಟ ವೀಡಿಯೊಗಳು",
"Inherit": "ಪಾರಂಪರ್ಯವಾಗಿ",
"ItemRemovedWithName": "{0} ಅನ್ನು ಸಂಕಲನದಿಂದ ತೆಗೆದುಹಾಕಲಾಗಿದೆ",
"LabelIpAddressValue": "IP ವಿಳಾಸ: {0}",
"LabelRunningTimeValue": "ಅವಧಿ: {0}",
"Latest": "ಹೊಸದಾದ",
"MessageServerConfigurationUpdated": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
"MixedContent": "ಮಿಶ್ರ ವಿಷಯ",
"Movies": "ಚಲನಚಿತ್ರಗಳು",
"Music": "ಸಂಗೀತ",
"MusicVideos": "ಸಂಗೀತ ವೀಡಿಯೊಗಳು",
"NameInstallFailed": "{0} ಸ್ಥಾಪನೆ ವಿಫಲವಾಗಿದೆ",
"NameSeasonNumber": "ಸೀಸನ್ {0}",
"NameSeasonUnknown": "ಸೀಸನ್ ತಿಳಿದಿಲ್ಲ",
"NotificationOptionApplicationUpdateAvailable": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣ ಲಭ್ಯವಿದೆ",
"NotificationOptionApplicationUpdateInstalled": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
"NotificationOptionAudioPlaybackStopped": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ",
"NotificationOptionInstallationFailed": "ಸ್ಥಾಪನ ವೈಫಲ್ಯ",
"NotificationOptionNewLibraryContent": "ಹೊಸ ವಿಷಯವನ್ನು ಒಳಗೊಂಡಿದೆ",
"NotificationOptionPluginError": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
"NotificationOptionPluginInstalled": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
"NotificationOptionPluginUpdateInstalled": "ಪ್ಲಗಿನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
"NotificationOptionServerRestartRequired": "ಸರ್ವರ್ ಮರುಪ್ರಾರಂಭದ ಅಗತ್ಯವಿದೆ",
"NotificationOptionTaskFailed": "ನಿಗದಿತ ಕಾರ್ಯ ವೈಫಲ್ಯ",
"NotificationOptionVideoPlayback": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ",
"Photos": "ಚಿತ್ರಗಳು",
"Playlists": "ಪ್ಲೇಪಟ್ಟಿಗಳು",
"Plugin": "ಪ್ಲಗಿನ್",
"PluginInstalledWithName": "{0} ಅನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
"PluginUpdatedWithName": "{0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
"ProviderValue": "ಒದಗಿಸುವವರು: {0}",
"TaskCleanLogs": "ಕ್ಲೀನ್ ಲಾಗ್ ಡೈರೆಕ್ಟರಿ",
"TaskRefreshPeople": "ಜನರನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
"TaskRefreshPeopleDescription": "ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಯಲ್ಲಿ ನಟರು ಮತ್ತು ನಿರ್ದೇಶಕರಿಗಾಗಿ ಮೆಟಾಡೇಟಾವನ್ನು ನವೀಕರಿಸಿ.",
"TaskUpdatePlugins": "ಪ್ಲಗಿನ್‌ಗಳನ್ನು ನವೀಕರಿಸಿ",
"TaskCleanTranscode": "ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಡೈರೆಕ್ಟರಿಯನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ",
"TaskRefreshChannels": "ಚಾನಲ್‌ಗಳನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
"TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ."
}

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

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

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

@ -25,5 +25,14 @@
"Channels": "Amashaneli",
"Books": "Izincwadi",
"Artists": "Abadlali",
"Albums": "Ama-albhamu"
"Albums": "Ama-albhamu",
"CameraImageUploadedFrom": "Kulandelayo lwesithonjana sekhamera selithunyelwe kusuka ku {0}",
"HeaderFavoriteArtists": "Abasethi Abathandekayo",
"HeaderFavoriteEpisodes": "Izilimi Ezithandekayo",
"HeaderFavoriteShows": "Izisho Ezithandekayo",
"External": "Kwezifungo",
"FailedLoginAttemptWithUserName": "Ukushayiswa kwesithombe sokungena okungekho {0}",
"HeaderContinueWatching": "Buyela Ukubona",
"HeaderFavoriteAlbums": "Izimpahla Ezithandwayo",
"HeaderAlbumArtists": "Abasethi wenkulumo"
}

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

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

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

@ -36,6 +36,7 @@ using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
@ -44,7 +45,7 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// Class SessionManager.
/// </summary>
public class SessionManager : ISessionManager, IDisposable
public sealed class SessionManager : ISessionManager, IAsyncDisposable
{
private readonly IUserDataManager _userDataManager;
private readonly ILogger<SessionManager> _logger;
@ -57,11 +58,9 @@ namespace Emby.Server.Implementations.Session
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerApplicationHost _appHost;
private readonly IDeviceManager _deviceManager;
/// <summary>
/// The active connections.
/// </summary>
private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections = new(StringComparer.OrdinalIgnoreCase);
private readonly CancellationTokenRegistration _shutdownCallback;
private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections
= new(StringComparer.OrdinalIgnoreCase);
private Timer _idleTimer;
@ -79,7 +78,8 @@ namespace Emby.Server.Implementations.Session
IImageProcessor imageProcessor,
IServerApplicationHost appHost,
IDeviceManager deviceManager,
IMediaSourceManager mediaSourceManager)
IMediaSourceManager mediaSourceManager,
IHostApplicationLifetime hostApplicationLifetime)
{
_logger = logger;
_eventManager = eventManager;
@ -92,6 +92,7 @@ namespace Emby.Server.Implementations.Session
_appHost = appHost;
_deviceManager = deviceManager;
_mediaSourceManager = mediaSourceManager;
_shutdownCallback = hostApplicationLifetime.ApplicationStopping.Register(OnApplicationStopping);
_deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated;
}
@ -151,36 +152,6 @@ namespace Emby.Server.Implementations.Session
}
}
/// <inheritdoc />
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and optionally managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
_idleTimer?.Dispose();
}
_idleTimer = null;
_deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
_disposed = true;
}
private void CheckDisposed()
{
if (_disposed)
@ -980,28 +951,28 @@ namespace Emby.Server.Implementations.Session
private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
{
bool playedToCompletion = false;
if (!playbackFailed)
if (playbackFailed)
{
var data = _userDataManager.GetUserData(user, item);
if (positionTicks.HasValue)
{
playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
}
else
{
// If the client isn't able to report this, then we'll just have to make an assumption
data.PlayCount++;
data.Played = item.SupportsPlayedStatus;
data.PlaybackPositionTicks = 0;
playedToCompletion = true;
}
return false;
}
_userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None);
var data = _userDataManager.GetUserData(user, item);
bool playedToCompletion;
if (positionTicks.HasValue)
{
playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
}
else
{
// If the client isn't able to report this, then we'll just have to make an assumption
data.PlayCount++;
data.Played = item.SupportsPlayedStatus;
data.PlaybackPositionTicks = 0;
playedToCompletion = true;
}
_userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None);
return playedToCompletion;
}
@ -1330,32 +1301,6 @@ namespace Emby.Server.Implementations.Session
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>
/// Adds the additional user.
/// </summary>
@ -1833,5 +1778,53 @@ namespace Emby.Server.Implementations.Session
return SendMessageToSessions(sessions, name, data, cancellationToken);
}
/// <inheritdoc />
public async ValueTask DisposeAsync()
{
if (_disposed)
{
return;
}
foreach (var session in _activeConnections.Values)
{
await session.DisposeAsync().ConfigureAwait(false);
}
if (_idleTimer is not null)
{
await _idleTimer.DisposeAsync().ConfigureAwait(false);
_idleTimer = null;
}
await _shutdownCallback.DisposeAsync().ConfigureAwait(false);
_deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
_disposed = true;
}
private async void OnApplicationStopping()
{
_logger.LogInformation("Sending shutdown notifications");
try
{
var messageType = _appHost.ShouldRestart ? SessionMessageType.ServerRestarting : SessionMessageType.ServerShuttingDown;
await SendMessageToSessions(Sessions, messageType, string.Empty, CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error sending server shutdown notifications");
}
// Close open websockets to allow Kestrel to shut down cleanly
foreach (var session in _activeConnections.Values)
{
await session.DisposeAsync().ConfigureAwait(false);
}
_activeConnections.Clear();
}
}
}

@ -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)
{
var extension = Path.GetExtension(package.SourceUrl);
if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
if (!Path.GetExtension(package.SourceUrl.AsSpan()).Equals(".zip", StringComparison.OrdinalIgnoreCase))
{
_logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl);
return;

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

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

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

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

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

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

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

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

@ -191,6 +191,11 @@ public static class StreamingHelpers
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.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
@ -243,7 +248,7 @@ public static class StreamingHelpers
? GetOutputFileExtension(state, mediaSource)
: ("." + state.OutputContainer);
state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
state.OutputFilePath = GetOutputFilePath(state, ext, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
return state;
}
@ -418,11 +423,11 @@ public static class StreamingHelpers
/// <returns>System.String.</returns>
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
@ -504,7 +509,7 @@ public static class StreamingHelpers
/// <param name="deviceId">The device id.</param>
/// <param name="playSessionId">The play session id.</param>
/// <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!}";

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

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

@ -94,4 +94,40 @@ public enum PersonKind
/// A person who was the illustrator.
/// </summary>
Illustrator,
/// <summary>
/// A person responsible for drawing the art.
/// </summary>
Penciller,
/// <summary>
/// A person responsible for inking the pencil art.
/// </summary>
Inker,
/// <summary>
/// A person responsible for applying color to drawings.
/// </summary>
Colorist,
/// <summary>
/// A person responsible for drawing text and speech bubbles.
/// </summary>
Letterer,
/// <summary>
/// A person responsible for drawing the cover art.
/// </summary>
CoverArtist,
/// <summary>
/// A person contributing to a resource by revising or elucidating the content, e.g., adding an introduction, notes, or other critical matter.
/// An editor may also prepare a resource for production, publication, or distribution.
/// </summary>
Editor,
/// <summary>
/// A person who renders a text from one language into another.
/// </summary>
Translator
}

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

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

@ -102,9 +102,6 @@ namespace Jellyfin.Server
base.RegisterServices(serviceCollection);
}
/// <inheritdoc />
protected override void RestartInternal() => Program.Restart();
/// <inheritdoc />
protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal()
{
@ -114,8 +111,5 @@ namespace Jellyfin.Server
// Jellyfin.Server.Implementations
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, AnonymousLanAccessHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
return serviceCollection.AddAuthorizationCore(options =>
{

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

@ -4,7 +4,6 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using CommandLine;
using Emby.Server.Implementations;
@ -42,7 +41,6 @@ namespace Jellyfin.Server
public const string LoggingConfigFileSystem = "logging.json";
private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory();
private static CancellationTokenSource _tokenSource = new();
private static long _startTimestamp;
private static ILogger _logger = NullLogger.Instance;
private static bool _restartOnShutdown;
@ -65,36 +63,9 @@ namespace Jellyfin.Server
.MapResult(StartApp, ErrorParsingArguments);
}
/// <summary>
/// Shuts down the application.
/// </summary>
internal static void Shutdown()
{
if (!_tokenSource.IsCancellationRequested)
{
_tokenSource.Cancel();
}
}
/// <summary>
/// Restarts the application.
/// </summary>
internal static void Restart()
{
_restartOnShutdown = true;
Shutdown();
}
private static async Task StartApp(StartupOptions options)
{
_startTimestamp = Stopwatch.GetTimestamp();
// Log all uncaught exceptions to std error
static void UnhandledExceptionToConsole(object sender, UnhandledExceptionEventArgs e) =>
Console.Error.WriteLine("Unhandled Exception\n" + e.ExceptionObject);
AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionToConsole;
ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options);
// $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
@ -112,38 +83,10 @@ namespace Jellyfin.Server
StartupHelpers.InitializeLoggingFramework(startupConfig, appPaths);
_logger = _loggerFactory.CreateLogger("Main");
// Log uncaught exceptions to the logging instead of std error
AppDomain.CurrentDomain.UnhandledException -= UnhandledExceptionToConsole;
// Use the logging framework for uncaught exceptions instead of std error
AppDomain.CurrentDomain.UnhandledException += (_, e)
=> _logger.LogCritical((Exception)e.ExceptionObject, "Unhandled Exception");
// Intercept Ctrl+C and Ctrl+Break
Console.CancelKeyPress += (_, e) =>
{
if (_tokenSource.IsCancellationRequested)
{
return; // Already shutting down
}
e.Cancel = true;
_logger.LogInformation("Ctrl+C, shutting down");
Environment.ExitCode = 128 + 2;
Shutdown();
};
// Register a SIGTERM handler
AppDomain.CurrentDomain.ProcessExit += (_, _) =>
{
if (_tokenSource.IsCancellationRequested)
{
return; // Already shutting down
}
_logger.LogInformation("Received a SIGTERM signal, shutting down");
Environment.ExitCode = 128 + 15;
Shutdown();
};
_logger.LogInformation(
"Jellyfin version: {Version}",
Assembly.GetEntryAssembly()!.GetName().Version!.ToString(3));
@ -173,12 +116,10 @@ namespace Jellyfin.Server
do
{
_restartOnShutdown = false;
await StartServer(appPaths, options, startupConfig).ConfigureAwait(false);
if (_restartOnShutdown)
{
_tokenSource = new CancellationTokenSource();
_startTimestamp = Stopwatch.GetTimestamp();
}
} while (_restartOnShutdown);
@ -186,7 +127,7 @@ namespace Jellyfin.Server
private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig)
{
var appHost = new CoreAppHost(
using var appHost = new CoreAppHost(
appPaths,
_loggerFactory,
options,
@ -196,6 +137,7 @@ namespace Jellyfin.Server
try
{
host = Host.CreateDefaultBuilder()
.UseConsoleLifetime()
.ConfigureServices(services => appHost.Init(services))
.ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths, _logger))
.ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig))
@ -210,7 +152,7 @@ namespace Jellyfin.Server
try
{
await host.StartAsync(_tokenSource.Token).ConfigureAwait(false);
await host.StartAsync().ConfigureAwait(false);
if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket())
{
@ -219,22 +161,18 @@ namespace Jellyfin.Server
StartupHelpers.SetUnixSocketPermissions(startupConfig, socketPath, _logger);
}
}
catch (Exception ex) when (ex is not TaskCanceledException)
catch (Exception)
{
_logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again");
throw;
}
await appHost.RunStartupTasksAsync(_tokenSource.Token).ConfigureAwait(false);
await appHost.RunStartupTasksAsync().ConfigureAwait(false);
_logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp));
// Block main thread until shutdown
await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false);
}
catch (TaskCanceledException)
{
// Don't throw on cancellation
await host.WaitForShutdownAsync().ConfigureAwait(false);
_restartOnShutdown = appHost.ShouldRestart;
}
catch (Exception ex)
{
@ -257,7 +195,6 @@ namespace Jellyfin.Server
}
}
await appHost.DisposeAsync().ConfigureAwait(false);
host?.Dispose();
}
}

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

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

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

@ -2,7 +2,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
@ -70,14 +69,6 @@ namespace MediaBrowser.Controller.Drawing
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>
/// Processes the image.
/// </summary>
@ -97,7 +88,5 @@ namespace MediaBrowser.Controller.Drawing
/// <param name="options">The options.</param>
/// <param name="libraryName">The library name to draw onto the collage.</param>
void CreateImageCollage(ImageCollageOptions options, string? libraryName);
bool SupportsTransparency(string path);
}
}

@ -119,7 +119,8 @@ namespace MediaBrowser.Controller.Drawing
private bool IsFormatSupported(string 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
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -29,7 +30,7 @@ namespace MediaBrowser.Controller.Entities
public class CollectionFolder : Folder, ICollectionFolder
{
private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
private static readonly Dictionary<string, LibraryOptions> _libraryOptions = new Dictionary<string, LibraryOptions>();
private static readonly ConcurrentDictionary<string, LibraryOptions> _libraryOptions = new ConcurrentDictionary<string, LibraryOptions>();
private bool _requiresRefresh;
/// <summary>
@ -139,45 +140,26 @@ namespace MediaBrowser.Controller.Entities
}
public static LibraryOptions GetLibraryOptions(string path)
{
lock (_libraryOptions)
{
if (!_libraryOptions.TryGetValue(path, out var options))
{
options = LoadLibraryOptions(path);
_libraryOptions[path] = options;
}
return options;
}
}
=> _libraryOptions.GetOrAdd(path, LoadLibraryOptions);
public static void SaveLibraryOptions(string path, LibraryOptions options)
{
lock (_libraryOptions)
{
_libraryOptions[path] = options;
_libraryOptions[path] = options;
var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions);
foreach (var mediaPath in clone.PathInfos)
var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions);
foreach (var mediaPath in clone.PathInfos)
{
if (!string.IsNullOrEmpty(mediaPath.Path))
{
if (!string.IsNullOrEmpty(mediaPath.Path))
{
mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path);
}
mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path);
}
XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path));
}
XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path));
}
public static void OnCollectionFolderChange()
{
lock (_libraryOptions)
{
_libraryOptions.Clear();
}
}
=> _libraryOptions.Clear();
public override bool IsSaveLocalMetadataEnabled()
{

@ -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 MediaBrowser.Common;
using MediaBrowser.Model.System;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Controller
@ -16,8 +15,6 @@ namespace MediaBrowser.Controller
{
bool CoreStartupHasCompleted { get; }
bool CanLaunchWebBrowser { get; }
/// <summary>
/// Gets the HTTP server port.
/// </summary>
@ -41,15 +38,6 @@ namespace MediaBrowser.Controller
/// <value>The name of the friendly.</value>
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>
/// Gets a URL specific for the request.
/// </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 _minFFmpegOclCuTonemapMode = new Version(5, 1, 3);
private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1);
private readonly Version _minFFmpegVaapiH26xEncA53CcSei = new Version(6, 0);
private static readonly string[] _videoProfilesH264 = new[]
{
@ -547,25 +548,25 @@ namespace MediaBrowser.Controller.MediaEncoding
/// <returns>System.Nullable{VideoCodecs}.</returns>
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";
}
if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase))
if (ext.Equals(".webm", StringComparison.OrdinalIgnoreCase))
{
// TODO: this may not always mean VP8, as the codec ages
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";
}
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";
}
@ -1079,10 +1080,10 @@ namespace MediaBrowser.Controller.MediaEncoding
&& state.SubtitleStream.IsExternal)
{
var subtitlePath = state.SubtitleStream.Path;
var subtitleExtension = Path.GetExtension(subtitlePath);
var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
if (string.Equals(subtitleExtension, ".sub", StringComparison.OrdinalIgnoreCase)
|| string.Equals(subtitleExtension, ".sup", StringComparison.OrdinalIgnoreCase))
if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase)
|| subtitleExtension.Equals(".sup", StringComparison.OrdinalIgnoreCase))
{
var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
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";
}
/* 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;
}
@ -5681,7 +5690,6 @@ namespace MediaBrowser.Controller.MediaEncoding
// Apply -analyzeduration as per the environment variable,
// 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;
if (state.MediaSource.AnalyzeDurationMs > 0)
@ -5700,6 +5708,14 @@ namespace MediaBrowser.Controller.MediaEncoding
inputModifier = inputModifier.Trim();
// Apply -probesize if configured
var ffmpegProbeSize = _config.GetFFmpegProbeSize();
if (!string.IsNullOrEmpty(ffmpegProbeSize))
{
inputModifier += $" -probesize {ffmpegProbeSize}";
}
var userAgentParam = GetUserAgentParam(state);
if (!string.IsNullOrEmpty(userAgentParam))
@ -6024,7 +6040,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var format = 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)
{
// 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));
}
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))
{
// opus only supports specific sampling rates

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

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

@ -232,20 +232,6 @@ namespace MediaBrowser.Controller.Session
/// <returns>Task.</returns>
Task SendRestartRequiredNotification(CancellationToken cancellationToken);
/// <summary>
/// Sends the server shutdown notification.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task SendServerShutdownNotification(CancellationToken cancellationToken);
/// <summary>
/// Sends the server restart notification.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task.</returns>
Task SendServerRestartNotification(CancellationToken cancellationToken);
/// <summary>
/// Adds the additional user.
/// </summary>

@ -9,6 +9,7 @@ using System.Xml;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@ -128,42 +129,19 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
// DateCreated
case "Added":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
if (reader.TryReadDateTime(out var dateCreated))
{
if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var added))
{
item.DateCreated = added;
}
else
{
Logger.LogWarning("Invalid Added value found: {Value}", val);
}
item.DateCreated = dateCreated;
}
break;
}
case "OriginalTitle":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrEmpty(val))
{
item.OriginalTitle = val;
}
item.OriginalTitle = reader.ReadNormalizedString();
break;
}
case "LocalTitle":
item.Name = reader.ReadElementContentAsString();
item.Name = reader.ReadNormalizedString();
break;
case "CriticRating":
{
var text = reader.ReadElementContentAsString();
@ -177,63 +155,26 @@ namespace MediaBrowser.LocalMetadata.Parsers
}
case "SortTitle":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
item.ForcedSortName = val;
}
item.ForcedSortName = reader.ReadNormalizedString();
break;
}
case "Overview":
case "Description":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
item.Overview = val;
}
item.Overview = reader.ReadNormalizedString();
break;
}
case "Language":
{
var val = reader.ReadElementContentAsString();
item.PreferredMetadataLanguage = val;
item.PreferredMetadataLanguage = reader.ReadNormalizedString();
break;
}
case "CountryCode":
{
var val = reader.ReadElementContentAsString();
item.PreferredMetadataCountryCode = val;
item.PreferredMetadataCountryCode = reader.ReadNormalizedString();
break;
}
case "PlaceOfBirth":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
var placeOfBirth = reader.ReadNormalizedString();
if (!string.IsNullOrEmpty(placeOfBirth) && item is Person person)
{
if (item is Person person)
{
person.ProductionLocations = new[] { val };
}
person.ProductionLocations = new[] { placeOfBirth };
}
break;
}
case "LockedFields":
{
var val = reader.ReadElementContentAsString();
@ -275,10 +216,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
{
if (!reader.IsEmptyElement)
{
using (var subtree = reader.ReadSubtree())
{
FetchFromCountriesNode(subtree);
}
reader.Skip();
}
else
{
@ -290,183 +228,84 @@ namespace MediaBrowser.LocalMetadata.Parsers
case "ContentRating":
case "MPAARating":
{
var rating = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(rating))
{
item.OfficialRating = rating;
}
item.OfficialRating = reader.ReadNormalizedString();
break;
}
case "CustomRating":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
item.CustomRating = val;
}
item.CustomRating = reader.ReadNormalizedString();
break;
}
case "RunningTime":
{
var text = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(text))
var runtimeText = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(runtimeText))
{
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;
}
}
break;
}
case "AspectRatio":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val) && item is IHasAspectRatio hasAspectRatio)
var aspectRatio = reader.ReadNormalizedString();
if (!string.IsNullOrEmpty(aspectRatio) && item is IHasAspectRatio hasAspectRatio)
{
hasAspectRatio.AspectRatio = val;
hasAspectRatio.AspectRatio = aspectRatio;
}
break;
}
case "LockData":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
item.IsLocked = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
}
item.IsLocked = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
break;
}
case "Network":
{
foreach (var name in SplitNames(reader.ReadElementContentAsString()))
foreach (var name in reader.GetStringArray())
{
if (string.IsNullOrWhiteSpace(name))
{
continue;
}
item.AddStudio(name);
}
break;
}
case "Director":
{
foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Director }))
foreach (var director in reader.GetPersonArray(PersonKind.Director))
{
if (string.IsNullOrWhiteSpace(p.Name))
{
continue;
}
itemResult.AddPerson(p);
itemResult.AddPerson(director);
}
break;
}
case "Writer":
{
foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Writer }))
foreach (var writer in reader.GetPersonArray(PersonKind.Writer))
{
if (string.IsNullOrWhiteSpace(p.Name))
{
continue;
}
itemResult.AddPerson(p);
itemResult.AddPerson(writer);
}
break;
}
case "Actors":
{
var actors = reader.ReadInnerXml();
if (actors.Contains('<', StringComparison.Ordinal))
foreach (var actor in reader.GetPersonArray(PersonKind.Actor))
{
// This is one of the mis-named "Actors" full nodes created by MB2
// 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);
}
itemResult.AddPerson(actor);
}
break;
}
case "GuestStars":
{
foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.GuestStar }))
foreach (var guestStar in reader.GetPersonArray(PersonKind.GuestStar))
{
if (string.IsNullOrWhiteSpace(p.Name))
{
continue;
}
itemResult.AddPerson(p);
itemResult.AddPerson(guestStar);
}
break;
}
case "Trailer":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
var trailer = reader.ReadNormalizedString();
if (!string.IsNullOrEmpty(trailer))
{
item.AddTrailerUrl(val);
item.AddTrailerUrl(trailer);
}
break;
}
case "DisplayOrder":
{
var val = reader.ReadElementContentAsString();
if (item is IHasDisplayOrder hasDisplayOrder)
var displayOrder = reader.ReadNormalizedString();
if (!string.IsNullOrEmpty(displayOrder) && item is IHasDisplayOrder hasDisplayOrder)
{
if (!string.IsNullOrWhiteSpace(val))
{
hasDisplayOrder.DisplayOrder = val;
}
hasDisplayOrder.DisplayOrder = displayOrder;
}
break;
}
case "Trailers":
{
if (!reader.IsEmptyElement)
@ -483,20 +322,12 @@ namespace MediaBrowser.LocalMetadata.Parsers
}
case "ProductionYear":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
if (reader.TryReadInt(out var productionYear) && productionYear > 1850)
{
if (int.TryParse(val, out var productionYear) && productionYear > 1850)
{
item.ProductionYear = productionYear;
}
item.ProductionYear = productionYear;
}
break;
}
case "Rating":
case "IMDBrating":
{
@ -517,40 +348,24 @@ namespace MediaBrowser.LocalMetadata.Parsers
case "BirthDate":
case "PremiereDate":
case "FirstAired":
{
var firstAired = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(firstAired))
if (reader.TryReadDateTimeExact("yyyy-MM-dd", out var firstAired))
{
if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out var airDate) && airDate.Year > 1850)
{
item.PremiereDate = airDate;
item.ProductionYear = airDate.Year;
}
item.PremiereDate = firstAired;
item.ProductionYear = firstAired.Year;
}
break;
}
case "DeathDate":
case "EndDate":
{
var firstAired = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(firstAired))
if (reader.TryReadDateTimeExact("yyyy-MM-dd", out var endDate))
{
if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out var airDate) && airDate.Year > 1850)
{
item.EndDate = airDate;
}
item.EndDate = endDate;
}
break;
}
case "CollectionNumber":
var tmdbCollection = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(tmdbCollection))
var tmdbCollection = reader.ReadNormalizedString();
if (!string.IsNullOrEmpty(tmdbCollection))
{
item.SetProviderId(MetadataProvider.TmdbCollection, tmdbCollection);
}
@ -753,41 +568,6 @@ namespace MediaBrowser.LocalMetadata.Parsers
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>
/// Fetches from taglines node.
/// </summary>
@ -806,17 +586,8 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "Tagline":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
{
item.Tagline = val;
}
item.Tagline = reader.ReadNormalizedString();
break;
}
default:
reader.Skip();
break;
@ -847,17 +618,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "Genre":
{
var genre = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(genre))
var genre = reader.ReadNormalizedString();
if (!string.IsNullOrEmpty(genre))
{
item.AddGenre(genre);
}
break;
}
default:
reader.Skip();
break;
@ -885,17 +652,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "Tag":
{
var tag = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(tag))
var tag = reader.ReadNormalizedString();
if (!string.IsNullOrEmpty(tag))
{
tags.Add(tag);
}
break;
}
default:
reader.Skip();
break;
@ -929,29 +692,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
{
case "Person":
case "Actor":
{
if (reader.IsEmptyElement)
var person = reader.GetPersonFromXmlNode();
if (person is not null)
{
reader.Read();
continue;
}
using (var subtree = reader.ReadSubtree())
{
foreach (var person in GetPersonsFromXmlNode(subtree))
{
if (string.IsNullOrWhiteSpace(person.Name))
{
continue;
}
item.AddPerson(person);
}
item.AddPerson(person);
}
break;
}
default:
reader.Skip();
break;
@ -977,17 +724,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "Trailer":
{
var val = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(val))
var trailer = reader.ReadNormalizedString();
if (!string.IsNullOrEmpty(trailer))
{
item.AddTrailerUrl(val);
item.AddTrailerUrl(trailer);
}
break;
}
default:
reader.Skip();
break;
@ -1018,17 +761,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "Studio":
{
var studio = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(studio))
var studio = reader.ReadNormalizedString();
if (!string.IsNullOrEmpty(studio))
{
item.AddStudio(studio);
}
break;
}
default:
reader.Skip();
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>
/// Get linked child.
/// </summary>
@ -1138,17 +800,11 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "Path":
{
linkedItem.Path = reader.ReadElementContentAsString();
linkedItem.Path = reader.ReadNormalizedString();
break;
}
case "ItemId":
{
linkedItem.LibraryItemId = reader.ReadElementContentAsString();
linkedItem.LibraryItemId = reader.ReadNormalizedString();
break;
}
default:
reader.Skip();
break;
@ -1189,22 +845,14 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "UserId":
{
item.UserId = reader.ReadElementContentAsString();
item.UserId = reader.ReadNormalizedString();
break;
}
case "CanEdit":
{
item.CanEdit = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
break;
}
default:
{
reader.Skip();
break;
}
}
}
else
@ -1221,34 +869,5 @@ namespace MediaBrowser.LocalMetadata.Parsers
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.Xml;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging;
@ -30,12 +31,8 @@ namespace MediaBrowser.LocalMetadata.Parsers
switch (reader.Name)
{
case "PlaylistMediaType":
{
item.PlaylistMediaType = reader.ReadElementContentAsString();
item.PlaylistMediaType = reader.ReadNormalizedString();
break;
}
case "PlaylistItems":
if (!reader.IsEmptyElement)
@ -94,10 +91,8 @@ namespace MediaBrowser.LocalMetadata.Parsers
}
default:
{
reader.Skip();
break;
}
}
}
else

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

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

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

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

@ -24,4 +24,21 @@ public static class ImageFormatExtensions
ImageFormat.Webp => "image/webp",
_ => 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>
public interface IFileSystem
{
void AddShortcutHandler(IShortcutHandler handler);
/// <summary>
/// Determines whether the specified filename is shortcut.
/// </summary>

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

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

File diff suppressed because it is too large Load Diff

@ -1,10 +1,11 @@
using System;
using System.Globalization;
using System.IO;
using System.Text;
using System.Threading;
using System.Xml;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging;
@ -81,7 +82,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
// 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
var name = new StringBuilder(item.Item.Name);
var overview = new StringBuilder(item.Item.Overview);
while ((index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase)) != -1)
{
xml = xmlFile.Substring(0, index + srch.Length);
@ -92,12 +96,44 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
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)
{
@ -112,142 +148,53 @@ namespace MediaBrowser.XbmcMetadata.Parsers
switch (reader.Name)
{
case "season":
if (reader.TryReadInt(out var seasonNumber))
{
var number = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(number))
{
if (int.TryParse(number, out var num))
{
item.ParentIndexNumber = num;
}
}
break;
item.ParentIndexNumber = seasonNumber;
}
break;
case "episode":
if (reader.TryReadInt(out var episodeNumber))
{
var number = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(number))
{
if (int.TryParse(number, out var num))
{
item.IndexNumber = num;
}
}
break;
item.IndexNumber = episodeNumber;
}
break;
case "episodenumberend":
if (reader.TryReadInt(out var episodeNumberEnd))
{
var number = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(number))
{
if (int.TryParse(number, out var num))
{
item.IndexNumberEnd = num;
}
}
break;
item.IndexNumberEnd = episodeNumberEnd;
}
break;
case "airsbefore_episode":
case "displayepisode":
if (reader.TryReadInt(out var airsBeforeEpisode))
{
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;
item.AirsBeforeEpisodeNumber = airsBeforeEpisode;
}
break;
case "airsafter_season":
case "displayafterseason":
if (reader.TryReadInt(out var airsAfterSeason))
{
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.AirsAfterSeasonNumber = rval;
}
}
break;
item.AirsAfterSeasonNumber = airsAfterSeason;
}
break;
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":
if (reader.TryReadInt(out var airsBeforeSeason))
{
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 "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;
item.AirsBeforeSeasonNumber = airsBeforeSeason;
}
break;
case "showtitle":
{
var showtitle = reader.ReadElementContentAsString();
if (!string.IsNullOrWhiteSpace(showtitle))
{
item.SeriesName = showtitle;
}
break;
}
item.SeriesName = reader.ReadNormalizedString();
break;
default:
base.FetchDataFromXmlNode(reader, itemResult);
break;

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

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

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

@ -60,13 +60,13 @@ namespace MediaBrowser.XbmcMetadata.Savers
}
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)
if (!item.IsInMixedFolder && item.ItemType == typeof(Movie))
{
yield return Path.Combine(item.ContainingFolderPath, "movie.nfo");
}
yield return Path.ChangeExtension(item.Path, ".nfo");
}
}

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

@ -8,7 +8,7 @@ EnvironmentFile = /etc/sysconfig/jellyfin
User = jellyfin
Group = 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
TimeoutSec = 15
SuccessExitStatus=0 143

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

@ -188,7 +188,7 @@ public class SkiaEncoder : IImageEncoder
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.");
Directory.CreateDirectory(directory);
File.Copy(path, tempPath, true);
@ -200,20 +200,10 @@ public class SkiaEncoder : IImageEncoder
{
if (!orientation.HasValue)
{
return SKEncodedOrigin.TopLeft;
return SKEncodedOrigin.Default;
}
return orientation.Value switch
{
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
};
return (SKEncodedOrigin)orientation.Value;
}
/// <summary>

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

@ -107,22 +107,10 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
/// <inheritdoc />
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 />
public IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats()
=> _imageEncoder.SupportedOutputFormats;
/// <inheritdoc />
public bool SupportsTransparency(string path)
=> _transparentImageTypes.Contains(Path.GetExtension(path));
/// <inheritdoc />
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)
{
@ -262,17 +250,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
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>
/// Gets the cache file path based on a set of parameters.
/// </summary>
@ -374,7 +351,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
filename.Append(",v=");
filename.Append(Version);
return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant());
return GetCachePath(ResizedImageCachePath, filename.ToString(), format.GetExtension());
}
/// <inheritdoc />
@ -471,35 +448,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
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));
}

@ -30,4 +30,17 @@ public static class ImageFormatExtensionsTests
[InlineData((ImageFormat)5)]
public static void GetMimeType_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format)
=> 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", false)]
[InlineData("/media/music/Foo B.A.R.", false)]
[InlineData("/movies/.zfs/snapshot/AutoM-2023-09", true)]
public void PathIgnored(string path, bool expected)
{
Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path));

@ -15,8 +15,8 @@ namespace Jellyfin.Server.Integration.Tests
{
public static class AuthHelper
{
public const string AuthHeaderName = "X-Emby-Authorization";
public const string DummyAuthHeader = "MediaBrowser Client=\"Jellyfin.Server Integration Tests\", DeviceId=\"69420\", Device=\"Apple II\", Version=\"10.8.0\"";
public const string AuthHeaderName = "Authorization";
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)
{
@ -27,16 +27,19 @@ namespace Jellyfin.Server.Integration.Tests
using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>()));
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()
{
Username = user!.Name,
Pw = user.Password,
},
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>(
await authResponse.Content.ReadAsStreamAsync(),
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.Globalization;
using System.IO;
using System.Threading;
using Emby.Server.Implementations;
using Jellyfin.Server.Extensions;
using Jellyfin.Server.Helpers;
@ -105,7 +104,7 @@ namespace Jellyfin.Server.Integration.Tests
var appHost = (TestAppHost)testServer.Services.GetRequiredService<IApplicationHost>();
appHost.ServiceProvider = testServer.Services;
appHost.InitializeServices().GetAwaiter().GetResult();
appHost.RunStartupTasksAsync(CancellationToken.None).GetAwaiter().GetResult();
appHost.RunStartupTasksAsync().GetAwaiter().GetResult();
return testServer;
}

@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using System.Threading;
using Jellyfin.Data.Enums;
@ -114,11 +114,11 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
_parser.Fetch(result, "Test Data/Rising.nfo", CancellationToken.None);
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(2, item.IndexNumberEnd);
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(2004, item.ProductionYear);
}

Loading…
Cancel
Save