aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/codeql-analysis.yml6
-rw-r--r--.github/workflows/repo-stale.yaml13
-rw-r--r--CONTRIBUTORS.md1
-rw-r--r--Directory.Packages.props10
-rw-r--r--Dockerfile2
-rw-r--r--Dockerfile.arm2
-rw-r--r--Dockerfile.arm642
-rw-r--r--Emby.Dlna/DlnaManager.cs2
-rw-r--r--Emby.Naming/ExternalFiles/ExternalPathParser.cs2
-rw-r--r--Emby.Naming/Video/StubResolver.cs7
-rw-r--r--Emby.Photos/PhotoProvider.cs2
-rw-r--r--Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs20
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs189
-rw-r--r--Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs11
-rw-r--r--Emby.Server.Implementations/IO/ManagedFileSystem.cs25
-rw-r--r--Emby.Server.Implementations/Library/IgnorePatterns.cs4
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs31
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs6
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs9
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs27
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs9
-rw-r--r--Emby.Server.Implementations/Localization/Core/as.json44
-rw-r--r--Emby.Server.Implementations/Localization/Core/chr.json52
-rw-r--r--Emby.Server.Implementations/Localization/Core/da.json48
-rw-r--r--Emby.Server.Implementations/Localization/Core/es.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/fr.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/kn.json122
-rw-r--r--Emby.Server.Implementations/Localization/Core/ml.json4
-rw-r--r--Emby.Server.Implementations/Localization/Core/ms.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/pr.json5
-rw-r--r--Emby.Server.Implementations/Localization/Core/zu.json11
-rw-r--r--Emby.Server.Implementations/MediaEncoder/EncodingManager.cs2
-rw-r--r--Emby.Server.Implementations/Playlists/PlaylistManager.cs16
-rw-r--r--Emby.Server.Implementations/Plugins/PluginManager.cs22
-rw-r--r--Emby.Server.Implementations/Session/SessionManager.cs155
-rw-r--r--Emby.Server.Implementations/SystemManager.cs109
-rw-r--r--Emby.Server.Implementations/Updates/InstallationManager.cs3
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs41
-rw-r--r--Jellyfin.Api/Controllers/HlsSegmentController.cs9
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs43
-rw-r--r--Jellyfin.Api/Controllers/LiveTvController.cs7
-rw-r--r--Jellyfin.Api/Controllers/SubtitleController.cs8
-rw-r--r--Jellyfin.Api/Controllers/SystemController.cs56
-rw-r--r--Jellyfin.Api/Helpers/DynamicHlsHelper.cs35
-rw-r--r--Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs8
-rw-r--r--Jellyfin.Api/Helpers/StreamingHelpers.cs15
-rw-r--r--Jellyfin.Api/Helpers/TranscodingJobHelper.cs2
-rw-r--r--Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs3
-rw-r--r--Jellyfin.Data/Enums/PersonKind.cs36
-rw-r--r--Jellyfin.Server.Implementations/Security/AuthorizationContext.cs30
-rw-r--r--Jellyfin.Server.Implementations/Users/UserManager.cs10
-rw-r--r--Jellyfin.Server/CoreAppHost.cs6
-rw-r--r--Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs1
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs3
-rw-r--r--Jellyfin.Server/Program.cs79
-rw-r--r--MediaBrowser.Common/Extensions/ProcessExtensions.cs60
-rw-r--r--MediaBrowser.Common/IApplicationHost.cs21
-rw-r--r--MediaBrowser.Common/Plugins/IPluginManager.cs5
-rw-r--r--MediaBrowser.Controller/Drawing/IImageProcessor.cs11
-rw-r--r--MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs3
-rw-r--r--MediaBrowser.Controller/Entities/CollectionFolder.cs42
-rw-r--r--MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs193
-rw-r--r--MediaBrowser.Controller/IServerApplicationHost.cs12
-rw-r--r--MediaBrowser.Controller/ISystemManager.cs34
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs42
-rw-r--r--MediaBrowser.Controller/Resolvers/IItemResolver.cs2
-rw-r--r--MediaBrowser.Controller/Resolvers/ItemResolver.cs6
-rw-r--r--MediaBrowser.Controller/Session/ISessionManager.cs14
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs505
-rw-r--r--MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs9
-rw-r--r--MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs80
-rw-r--r--MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs52
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs1
-rw-r--r--MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs46
-rw-r--r--MediaBrowser.Model/Drawing/ImageFormatExtensions.cs17
-rw-r--r--MediaBrowser.Model/IO/IFileSystem.cs2
-rw-r--r--MediaBrowser.Providers/Manager/ImageSaver.cs6
-rw-r--r--MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs6
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs859
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs175
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs29
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs29
-rw-r--r--MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs22
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs4
-rw-r--r--debian/jellyfin.init1
-rw-r--r--fedora/jellyfin.service2
-rw-r--r--fedora/jellyfin.spec1
-rw-r--r--src/Jellyfin.Drawing.Skia/SkiaEncoder.cs16
-rw-r--r--src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs12
-rw-r--r--src/Jellyfin.Drawing/ImageProcessor.cs56
-rw-r--r--tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs13
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs1
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs13
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs26
-rw-r--r--tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs3
-rw-r--r--tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs6
97 files changed, 1538 insertions, 2276 deletions
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 7855ee4f7..4c9d68d11 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -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
diff --git a/.github/workflows/repo-stale.yaml b/.github/workflows/repo-stale.yaml
index c753c1600..2b1164116 100644
--- a/.github/workflows/repo-stale.yaml
+++ b/.github/workflows/repo-stale.yaml
@@ -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
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index b7e777817..e3af12a49 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -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)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 27851cdeb..55b03cdce 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -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" />
diff --git a/Dockerfile b/Dockerfile
index e51d285e1..9be319311 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,7 +4,7 @@
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
ARG DOTNET_VERSION=7.0
-FROM node:lts-alpine as web-builder
+FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
diff --git a/Dockerfile.arm b/Dockerfile.arm
index 46a3e9b99..e8ec6398e 100644
--- a/Dockerfile.arm
+++ b/Dockerfile.arm
@@ -5,7 +5,7 @@
ARG DOTNET_VERSION=7.0
-FROM node:lts-alpine as web-builder
+FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
diff --git a/Dockerfile.arm64 b/Dockerfile.arm64
index 4f9d5e1fd..83137ee89 100644
--- a/Dockerfile.arm64
+++ b/Dockerfile.arm64
@@ -5,7 +5,7 @@
ARG DOTNET_VERSION=7.0
-FROM node:lts-alpine as web-builder
+FROM node:20-alpine as web-builder
ARG JELLYFIN_WEB_VERSION=master
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs
index 99b3e6e7e..d67cb67b5 100644
--- a/Emby.Dlna/DlnaManager.cs
+++ b/Emby.Dlna/DlnaManager.cs
@@ -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
diff --git a/Emby.Naming/ExternalFiles/ExternalPathParser.cs b/Emby.Naming/ExternalFiles/ExternalPathParser.cs
index 953129671..4080ba10d 100644
--- a/Emby.Naming/ExternalFiles/ExternalPathParser.cs
+++ b/Emby.Naming/ExternalFiles/ExternalPathParser.cs
@@ -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)))
{
diff --git a/Emby.Naming/Video/StubResolver.cs b/Emby.Naming/Video/StubResolver.cs
index f7ba606e3..4b9df19b0 100644
--- a/Emby.Naming/Video/StubResolver.cs
+++ b/Emby.Naming/Video/StubResolver.cs
@@ -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;
diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs
index f54066c57..27329a7f2 100644
--- a/Emby.Photos/PhotoProvider.cs
+++ b/Emby.Photos/PhotoProvider.cs
@@ -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
{
diff --git a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
index a4deeddb7..a2f38c8c2 100644
--- a/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
+++ b/Emby.Server.Implementations/AppBase/BaseConfigurationManager.cs
@@ -8,7 +8,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
@@ -19,14 +18,8 @@ namespace Emby.Server.Implementations.AppBase
/// </summary>
public abstract class BaseConfigurationManager : IConfigurationManager
{
- private readonly IFileSystem _fileSystem;
-
- private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
-
- /// <summary>
- /// The _configuration sync lock.
- /// </summary>
- private readonly object _configurationSyncLock = new object();
+ private readonly ConcurrentDictionary<string, object> _configurations = new();
+ private readonly object _configurationSyncLock = new();
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
@@ -42,12 +35,13 @@ namespace Emby.Server.Implementations.AppBase
/// <param name="applicationPaths">The application paths.</param>
/// <param name="loggerFactory">The logger factory.</param>
/// <param name="xmlSerializer">The XML serializer.</param>
- /// <param name="fileSystem">The file system.</param>
- protected BaseConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem)
+ protected BaseConfigurationManager(
+ IApplicationPaths applicationPaths,
+ ILoggerFactory loggerFactory,
+ IXmlSerializer xmlSerializer)
{
CommonApplicationPaths = applicationPaths;
XmlSerializer = xmlSerializer;
- _fileSystem = fileSystem;
Logger = loggerFactory.CreateLogger<BaseConfigurationManager>();
UpdateCachePath();
@@ -272,7 +266,7 @@ namespace Emby.Server.Implementations.AppBase
{
var file = Path.Combine(path, Guid.NewGuid().ToString());
File.WriteAllText(file, string.Empty);
- _fileSystem.DeleteFile(file);
+ File.Delete(file);
}
private string GetConfigurationFile(string key)
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 5f8d275b6..3253ea026 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -12,7 +12,6 @@ using System.Linq;
using System.Net;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
-using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna;
using Emby.Dlna.Main;
@@ -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>();
@@ -856,38 +842,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>
/// <returns>IEnumerable{Assembly}.</returns>
@@ -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);
- }
- }
- }
}
}
diff --git a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
index 6b8b1a620..0ee43ce0a 100644
--- a/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
+++ b/Emby.Server.Implementations/Configuration/ServerConfigurationManager.cs
@@ -7,7 +7,6 @@ using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.IO;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
@@ -22,11 +21,13 @@ namespace Emby.Server.Implementations.Configuration
/// Initializes a new instance of the <see cref="ServerConfigurationManager" /> class.
/// </summary>
/// <param name="applicationPaths">The application paths.</param>
- /// <param name="loggerFactory">The paramref name="loggerFactory" factory.</param>
+ /// <param name="loggerFactory">The logger factory.</param>
/// <param name="xmlSerializer">The XML serializer.</param>
- /// <param name="fileSystem">The file system.</param>
- public ServerConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem)
- : base(applicationPaths, loggerFactory, xmlSerializer, fileSystem)
+ public ServerConfigurationManager(
+ IApplicationPaths applicationPaths,
+ ILoggerFactory loggerFactory,
+ IXmlSerializer xmlSerializer)
+ : base(applicationPaths, loggerFactory, xmlSerializer)
{
UpdateMetadataPath();
}
diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
index 3aa5233ed..4178936ce 100644
--- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs
+++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs
@@ -15,10 +15,6 @@ namespace Emby.Server.Implementations.IO
/// </summary>
public class ManagedFileSystem : IFileSystem
{
- private readonly ILogger<ManagedFileSystem> _logger;
-
- private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
- private readonly string _tempPath;
private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows();
private static readonly char[] _invalidPathCharacters =
{
@@ -29,23 +25,24 @@ namespace Emby.Server.Implementations.IO
(char)31, ':', '*', '?', '\\', '/'
};
+ private readonly ILogger<ManagedFileSystem> _logger;
+ private readonly List<IShortcutHandler> _shortcutHandlers;
+ private readonly string _tempPath;
+
/// <summary>
/// Initializes a new instance of the <see cref="ManagedFileSystem"/> class.
/// </summary>
/// <param name="logger">The <see cref="ILogger"/> instance to use.</param>
/// <param name="applicationPaths">The <see cref="IApplicationPaths"/> instance to use.</param>
+ /// <param name="shortcutHandlers">the <see cref="IShortcutHandler"/>'s to use.</param>
public ManagedFileSystem(
ILogger<ManagedFileSystem> logger,
- IApplicationPaths applicationPaths)
+ IApplicationPaths applicationPaths,
+ IEnumerable<IShortcutHandler> shortcutHandlers)
{
_logger = logger;
_tempPath = applicationPaths.TempDirectory;
- }
-
- /// <inheritdoc />
- public virtual void AddShortcutHandler(IShortcutHandler handler)
- {
- _shortcutHandlers.Add(handler);
+ _shortcutHandlers = shortcutHandlers.ToList();
}
/// <summary>
@@ -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)
{
diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs
index 5384c04b3..cf6fc1845 100644
--- a/Emby.Server.Implementations/Library/IgnorePatterns.cs
+++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs
@@ -89,6 +89,10 @@ namespace Emby.Server.Implementations.Library
// bts sync files
"**/*.bts",
"**/*.sync",
+
+ // zfs
+ "**/.zfs/**",
+ "**/.zfs"
};
private static readonly GlobOptions _globOptions = new GlobOptions
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index b0a4a4151..ce8b1f918 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -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))
diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
index a74f82475..862f144e6 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs
@@ -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;
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
index 381796d0e..779cfd5be 100644
--- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs
@@ -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>
diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
index 042422c6f..73861ff59 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs
@@ -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
diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
index 9026160ff..b77c6b204 100644
--- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using System;
using System.Collections.Generic;
using System.IO;
@@ -25,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
private readonly NamingOptions _namingOptions;
private readonly IDirectoryService _directoryService;
- private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
+ private static readonly string[] _ignoreFiles = new[]
{
"folder",
"thumb",
@@ -56,7 +54,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>Trailer.</returns>
- protected override Photo Resolve(ItemResolveArgs args)
+ protected override Photo? Resolve(ItemResolveArgs args)
{
if (!args.IsDirectory)
{
@@ -68,10 +66,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
{
if (IsImageFile(args.Path, _imageProcessor))
{
- var filename = Path.GetFileNameWithoutExtension(args.Path);
+ var filename = Path.GetFileNameWithoutExtension(args.Path.AsSpan());
// Make sure the image doesn't belong to a video file
- var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path));
+ var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path)
+ ?? throw new InvalidOperationException("Path can't be a root directory."));
foreach (var file in files)
{
@@ -92,32 +91,32 @@ namespace Emby.Server.Implementations.Library.Resolvers
return null;
}
- internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, string imageFilename)
+ internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, ReadOnlySpan<char> imageFilename)
{
return VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename);
}
- internal static bool IsOwnedByResolvedMedia(string file, string imageFilename)
+ internal static bool IsOwnedByResolvedMedia(ReadOnlySpan<char> file, ReadOnlySpan<char> imageFilename)
=> imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
{
ArgumentNullException.ThrowIfNull(path);
- var filename = Path.GetFileNameWithoutExtension(path);
-
- if (_ignoreFiles.Contains(filename))
+ var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
+ if (!imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
{
return false;
}
- if (_ignoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1))
+ var filename = Path.GetFileNameWithoutExtension(path);
+
+ if (_ignoreFiles.Any(i => filename.StartsWith(i, StringComparison.OrdinalIgnoreCase)))
{
return false;
}
- string extension = Path.GetExtension(path).TrimStart('.');
- return imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase);
+ return true;
}
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
index df9101f48..341782d9d 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs
@@ -94,14 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
{
var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine);
- if (string.IsNullOrWhiteSpace(channel.Id))
- {
- channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
- }
- else
- {
- channel.Id = channelIdPrefix + channel.Id.GetMD5().ToString("N", CultureInfo.InvariantCulture);
- }
+ channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
channel.Path = trimmedLine;
channels.Add(channel);
diff --git a/Emby.Server.Implementations/Localization/Core/as.json b/Emby.Server.Implementations/Localization/Core/as.json
index 0967ef424..7c7dd26e9 100644
--- a/Emby.Server.Implementations/Localization/Core/as.json
+++ b/Emby.Server.Implementations/Localization/Core/as.json
@@ -1 +1,43 @@
-{}
+{
+ "Albums": "এলবাম",
+ "Application": "আবেদন",
+ "AppDeviceValues": "এপ্‌: {0}, ডিভাইচ: {1}",
+ "Artists": "শিল্পী",
+ "Channels": "চেনেলস",
+ "Default": "ডিফল্ট",
+ "AuthenticationSucceededWithUserName": "{0} সফলভাবে প্রমাণিত",
+ "Books": "পুস্তক",
+ "Movies": "চলচ্চিত্ৰ",
+ "CameraImageUploadedFrom": "একটি নতুন ক্যামেরা চিত্র আপলোড করা হয়েছে {0}",
+ "Collections": "সংগ্রহ",
+ "HeaderFavoriteShows": "প্রিয় শোসমূহ",
+ "Latest": "শেহতীয়া",
+ "MessageApplicationUpdated": "জেলিফিন চাইভাৰ আপডেট কৰা হৈছে",
+ "MixedContent": "মিশ্ৰিত সমগ্ৰতা",
+ "NewVersionIsAvailable": "ডাউনলোড কৰিবলৈ জেলিফিন চাইভাৰৰ এটা নতুন সংস্কৰণ উপলব্ধ আছে.",
+ "NotificationOptionCameraImageUploaded": "কেমেৰাৰ চিত্ৰ আপল'ড কৰা হ'ল",
+ "External": "বাহ্যিক",
+ "Favorites": "পছন্দসই",
+ "Folders": "ফোল্ডাৰ",
+ "Forced": "বলপূর্বক",
+ "Genres": "শ্রেণী",
+ "HeaderAlbumArtists": "অ্যালবাম শিল্পী",
+ "HeaderContinueWatching": "দেখা চালিয়ে যান",
+ "FailedLoginAttemptWithUserName": "লগইন ব্যর্থ চেষ্টা কৰা হৈছে থেকে {0}",
+ "HeaderFavoriteAlbums": "প্রিয় অ্যালবামসমূহ",
+ "HeaderFavoriteArtists": "প্রিয় শিল্পীসমূহ",
+ "HeaderFavoriteEpisodes": "প্রিয় পর্বসমূহ",
+ "HeaderFavoriteSongs": "প্ৰিয় গীত",
+ "HeaderLiveTV": "প্ৰতিবেদন টিভি",
+ "HeaderNextUp": "পৰৱৰ্তী অংশ",
+ "HeaderRecordingGroups": "অলংকৰণ গোষ্ঠীসমূহ",
+ "HearingImpaired": "শ্ৰবণ অক্ষম",
+ "HomeVideos": "ঘৰৰ ভিডিঅ'সমূহ",
+ "Inherit": "উত্তপ্ত কৰা",
+ "MessageServerConfigurationUpdated": "চাইভাৰ কনফিগাৰেশ্যন আপডেট কৰা হৈছে",
+ "NotificationOptionApplicationUpdateAvailable": "অ্যাপ্লিকেশ্যন আপডেট উপলব্ধ",
+ "NotificationOptionApplicationUpdateInstalled": "অ্যাপ্লিকেশ্যন আপডেট ইনষ্টল কৰা হ'ল",
+ "NotificationOptionAudioPlayback": "অডিঅ' প্লেবেক আৰম্ভ হ'ল",
+ "NotificationOptionAudioPlaybackStopped": "অডিঅ' প্লেবেক আঁতৰ হ'ল",
+ "NotificationOptionInstallationFailed": "ইনষ্টলেশ্যন ব্যৰ্থতা"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/chr.json b/Emby.Server.Implementations/Localization/Core/chr.json
new file mode 100644
index 000000000..85d1f4c88
--- /dev/null
+++ b/Emby.Server.Implementations/Localization/Core/chr.json
@@ -0,0 +1,52 @@
+{
+ "ChapterNameValue": "Didanedi {0}",
+ "HeaderAlbumArtists": "Didanidanolisgisgi",
+ "HeaderFavoriteAlbums": "Dvganidi didanidisgisgi",
+ "HeaderLiveTV": "Anigadi didanidisgosgi",
+ "HeaderRecordingGroups": "Didanisquodiisgisgi",
+ "HomeVideos": "Diganadi dinagadisgisgi",
+ "Inherit": "Anigwe",
+ "MessageApplicationUpdatedTo": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe anigadi {0}",
+ "MixedContent": "Ganinidi dininoladisgisgi",
+ "Movies": "Anidvnisgisgi",
+ "MusicVideos": "Danodisgisgi didanidisgosgi",
+ "NotificationOptionAudioPlayback": "Didanidigwe diganuyisgisgi anigadi",
+ "NotificationOptionInstallationFailed": "Diudvdi anadvnatisgisgi",
+ "NotificationOptionPluginUninstalled": "Ditsigvhnidv anawvdisgisgi",
+ "Albums": "Anigawidaniyv",
+ "Application": "Didanvyi",
+ "Artists": "Dinidaniyi",
+ "AuthenticationSucceededWithUserName": "{0} Sesoquonisdi nagadani",
+ "Books": "Didanedi",
+ "CameraImageUploadedFrom": "Anigawidaniyv nasgi didagwalanvyi {0}",
+ "Channels": "Diganadasgi",
+ "Collections": "Diganadisgi",
+ "Default": "Dinadi",
+ "DeviceOfflineWithName": "{0} Aniyvolehvi nasgi",
+ "External": "Amohdi",
+ "Favorites": "Nvdayelvdisgi",
+ "Folders": "Didanididisgi",
+ "Forced": "Ganedi",
+ "Genres": "Diganadisgi",
+ "HeaderContinueWatching": "Uwoditsu asdanidisgisgi",
+ "HeaderFavoriteArtists": "Dvganidi dinidanolisgisgi",
+ "HeaderFavoriteEpisodes": "Dvganidi didanidilisgadisgisgi",
+ "HeaderFavoriteShows": "Dvganidi didanididanolisgisgi)",
+ "HeaderFavoriteSongs": "Dvganidi danodisgisgi",
+ "HeaderNextUp": "Anidvli uwodoli",
+ "HearingImpaired": "Anitsunidi talunidisgisgi",
+ "ItemAddedWithName": "{0} Dinigwe anididanidisgi",
+ "Latest": "Uwodoli",
+ "MessageApplicationUpdated": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe",
+ "MessageServerConfigurationUpdated": "Sedanidvdi anigadi diganidinonvhi",
+ "Music": "Danodisgisgi",
+ "NameSeasonUnknown": "Tsunita anidvdisgi",
+ "NewVersionIsAvailable": "Danodigwe anigadi Jellyfin Server tsadanidigwe adisdi uwodvdi diganidinonvhi.",
+ "NotificationOptionApplicationUpdateAvailable": "Disisdi tsadanidigwe udvdi",
+ "NotificationOptionApplicationUpdateInstalled": "Disisdi tsadanidigwe digawvdi",
+ "NotificationOptionAudioPlaybackStopped": "Didanidigwe diganuyisgisgi digawvdi",
+ "NotificationOptionCameraImageUploaded": "Asdayi adininisgisgi diganuyisgisgi",
+ "NotificationOptionNewLibraryContent": "Danodisgisgi anigadi digawvdi",
+ "NotificationOptionPluginError": "Ditsigvhnidv anadvnatisgisgi",
+ "NotificationOptionPluginInstalled": "Ditsigvhnidv digawvdi"
+}
diff --git a/Emby.Server.Implementations/Localization/Core/da.json b/Emby.Server.Implementations/Localization/Core/da.json
index 1b6eecdcf..837172a5b 100644
--- a/Emby.Server.Implementations/Localization/Core/da.json
+++ b/Emby.Server.Implementations/Localization/Core/da.json
@@ -15,13 +15,13 @@
"Favorites": "Favoritter",
"Folders": "Mapper",
"Genres": "Genrer",
- "HeaderAlbumArtists": "Albums kunstnere",
+ "HeaderAlbumArtists": "Albumkunstnere",
"HeaderContinueWatching": "Fortsæt afspilning",
- "HeaderFavoriteAlbums": "Favorit albummer",
- "HeaderFavoriteArtists": "Favorit kunstnere",
- "HeaderFavoriteEpisodes": "Favorit afsnit",
- "HeaderFavoriteShows": "Favorit serier",
- "HeaderFavoriteSongs": "Favorit sange",
+ "HeaderFavoriteAlbums": "Favoritalbummer",
+ "HeaderFavoriteArtists": "Favoritkunstnere",
+ "HeaderFavoriteEpisodes": "Yndlingsafsnit",
+ "HeaderFavoriteShows": "Yndlingsserier",
+ "HeaderFavoriteSongs": "Yndlingssange",
"HeaderLiveTV": "Live-TV",
"HeaderNextUp": "Næste",
"HeaderRecordingGroups": "Optagelsesgrupper",
@@ -34,8 +34,8 @@
"Latest": "Seneste",
"MessageApplicationUpdated": "Jellyfin Server er blevet opdateret",
"MessageApplicationUpdatedTo": "Jellyfin Server er blevet opdateret til {0}",
- "MessageNamedServerConfigurationUpdatedWithValue": "Server konfiguration sektion {0} er blevet opdateret",
- "MessageServerConfigurationUpdated": "Server konfigurationen er blevet opdateret",
+ "MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfiguration sektion {0} er blevet opdateret",
+ "MessageServerConfigurationUpdated": "Serverkonfigurationen er blevet opdateret",
"MixedContent": "Blandet indhold",
"Movies": "Film",
"Music": "Musik",
@@ -51,7 +51,7 @@
"NotificationOptionCameraImageUploaded": "Kamerabillede uploadet",
"NotificationOptionInstallationFailed": "Installationen mislykkedes",
"NotificationOptionNewLibraryContent": "Nyt indhold tilføjet",
- "NotificationOptionPluginError": "Plugin fejl",
+ "NotificationOptionPluginError": "Plugin-fejl",
"NotificationOptionPluginInstalled": "Plugin blev installeret",
"NotificationOptionPluginUninstalled": "Plugin blev afinstalleret",
"NotificationOptionPluginUpdateInstalled": "Opdatering til plugin blev installeret",
@@ -92,26 +92,26 @@
"ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
"ValueSpecialEpisodeName": "Special - {0}",
"VersionNumber": "Version {0}",
- "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfigurationen.",
+ "TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
"TaskDownloadMissingSubtitles": "Hent manglende undertekster",
"TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.",
"TaskUpdatePlugins": "Opdater Plugins",
- "TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gamle.",
- "TaskCleanLogs": "Ryd Log mappe",
- "TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdateret metadata.",
- "TaskRefreshLibrary": "Scan Medie Bibliotek",
- "TaskCleanCacheDescription": "Sletter cache filer som systemet ikke længere bruger.",
- "TaskCleanCache": "Ryd Cache mappe",
- "TasksChannelsCategory": "Internet Kanaler",
+ "TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.",
+ "TaskCleanLogs": "Ryd Log-mappe",
+ "TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.",
+ "TaskRefreshLibrary": "Scan Mediebibliotek",
+ "TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.",
+ "TaskCleanCache": "Ryd Cache-mappe",
+ "TasksChannelsCategory": "Internetkanaler",
"TasksApplicationCategory": "Applikation",
"TasksLibraryCategory": "Bibliotek",
"TasksMaintenanceCategory": "Vedligeholdelse",
- "TaskRefreshChapterImages": "Udtræk kapitel billeder",
- "TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.",
- "TaskRefreshChannelsDescription": "Opdater internet kanal information.",
+ "TaskRefreshChapterImages": "Udtræk kapitelbilleder",
+ "TaskRefreshChapterImagesDescription": "Laver miniaturebilleder for videoer, der har kapitler.",
+ "TaskRefreshChannelsDescription": "Opdaterer information for internetkanal.",
"TaskRefreshChannels": "Opdater Kanaler",
- "TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end 1 dag gammel.",
- "TaskCleanTranscode": "Tøm Transcode mappen",
+ "TaskCleanTranscodeDescription": "Fjerner transcode-filer, som er mere end 1 dag gammel.",
+ "TaskCleanTranscode": "Tøm Transcode-mappen",
"TaskRefreshPeople": "Opdater Personer",
"TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.",
"TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.",
@@ -121,8 +121,8 @@
"Default": "Standard",
"TaskOptimizeDatabaseDescription": "Komprimerer databasen og frigør plads. Denne handling køres efter at have scannet mediebiblioteket, eller efter at have lavet ændringer til databasen, for at højne ydeevnen.",
"TaskOptimizeDatabase": "Optimér database",
- "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan tage lang tid.",
- "TaskKeyframeExtractor": "Nøglebillede udtræk",
+ "TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS-playlister. Denne opgave kan tage lang tid.",
+ "TaskKeyframeExtractor": "Udtræk af nøglebillede",
"External": "Ekstern",
"HearingImpaired": "Hørehæmmet"
}
diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json
index f5636a0af..4c56f789d 100644
--- a/Emby.Server.Implementations/Localization/Core/es.json
+++ b/Emby.Server.Implementations/Localization/Core/es.json
@@ -3,9 +3,9 @@
"AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
"Application": "Aplicación",
"Artists": "Artistas",
- "AuthenticationSucceededWithUserName": "{0} identificado correctamente",
+ "AuthenticationSucceededWithUserName": "{0} autenticado correctamente",
"Books": "Libros",
- "CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}",
+ "CameraImageUploadedFrom": "Se ha subido una nueva imagen por cámara desde {0}",
"Channels": "Canales",
"ChapterNameValue": "Capítulo {0}",
"Collections": "Colecciones",
diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json
index 4877bcd7a..a2b429dcd 100644
--- a/Emby.Server.Implementations/Localization/Core/fr.json
+++ b/Emby.Server.Implementations/Localization/Core/fr.json
@@ -105,8 +105,8 @@
"TaskRefreshPeople": "Actualiser les acteurs",
"TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
- "TaskRefreshLibraryDescription": "Scanne votre médiathèque pour trouver les nouveaux fichiers et actualise les métadonnées.",
- "TaskRefreshLibrary": "Scanner la médiathèque",
+ "TaskRefreshLibraryDescription": "Analyser sa médiathèque pour trouver les nouveaux fichiers et actualiser les métadonnées.",
+ "TaskRefreshLibrary": "Analyser la médiathèque",
"TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.",
"TaskRefreshChapterImages": "Extraire les images de chapitre",
"TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",
diff --git a/Emby.Server.Implementations/Localization/Core/kn.json b/Emby.Server.Implementations/Localization/Core/kn.json
index 3c8c38ed4..5e2b3756b 100644
--- a/Emby.Server.Implementations/Localization/Core/kn.json
+++ b/Emby.Server.Implementations/Localization/Core/kn.json
@@ -3,5 +3,125 @@
"TaskOptimizeDatabase": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಆಪ್ಟಿಮೈಜ್ ಮಾಡಿ",
"TaskOptimizeDatabaseDescription": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಕಾಂಪ್ಯಾಕ್ಟ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮುಕ್ತ ಜಾಗವನ್ನು ಮೊಟಕುಗೊಳಿಸುತ್ತದೆ. ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿದ ನಂತರ ಈ ಕಾರ್ಯವನ್ನು ನಡೆಸುವುದು ಅಥವಾ ಡೇಟಾಬೇಸ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಸೂಚಿಸುವ ಇತರ ಬದಲಾವಣೆಗಳನ್ನು ಮಾಡುವುದರಿಂದ ಕಾರ್ಯಕ್ಷಮತೆಯನ್ನು ಸುಧಾರಿಸಬಹುದು.",
"TaskKeyframeExtractor": "ಕೀಫ್ರೇಮ್ ಎಕ್ಸ್‌ಟ್ರಾಕ್ಟರ್",
- "TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್‌ಗಳಿಂದ ಕೀಫ್ರೇಮ್‌ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು."
+ "TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್‌ಗಳಿಂದ ಕೀಫ್ರೇಮ್‌ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು.",
+ "ValueHasBeenAddedToLibrary": "{0} ಅನ್ನು ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಗೆ ಸೇರಿಸಲಾಗಿದೆ",
+ "ValueSpecialEpisodeName": "ವಿಶೇಷ - {0}",
+ "TasksLibraryCategory": "ಸಮೊಹ",
+ "TasksApplicationCategory": "ಅಪ್ಲಿಕೇಶನ್",
+ "TasksChannelsCategory": "ಇಂಟರ್ನೆಟ್ ಚಾನೆಲ್ಗಳು",
+ "TaskCleanCache": "ಕ್ಲೀನ್ ಕ್ಯಾಶ ಡೈರೆಕ್ಟರಿ",
+ "TaskCleanCacheDescription": "ಸಿಸ್ಟಮ್‌ಗೆ ಇನ್ನು ಮುಂದೆ ಅಗತ್ಯವಿಲ್ಲದ ಸಂಗ್ರಹ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+ "TaskRefreshLibrary": "ಸ್ಕ್ಯಾನ್ ಮೀಡಿಯಾ ಲೈಬ್ರರಿ",
+ "UserOfflineFromDevice": "{1} ನಿಂದ {0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
+ "Albums": "ಸಂಪುಟ",
+ "Application": "ಅಪ್ಲಿಕೇಶನ್",
+ "AppDeviceValues": "ಅಪ್ಲಿಕೇಶನ್: {0}, ಸಾಧನ: {1}",
+ "Artists": "ಕಲಾವಿದರು",
+ "AuthenticationSucceededWithUserName": "{0} ಯಶಸ್ವಿಯಾಗಿ ದೃಢೀಕರಿಸಲಾಗಿದೆ",
+ "Books": "ಪುಸ್ತಕಗಳು",
+ "ChapterNameValue": "ಅಧ್ಯಾಯ {0}",
+ "Collections": "ಸಂಗ್ರಹಣೆಗಳು",
+ "Default": "ಪೂರ್ವನಿಯೋಜಿತ",
+ "DeviceOfflineWithName": "{0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
+ "DeviceOnlineWithName": "{0} ಸಂಪರ್ಕಗೊಂಡಿದೆ",
+ "External": "ಹೊರಗಿನ",
+ "FailedLoginAttemptWithUserName": "{0} ರಿಂದ ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ",
+ "Favorites": "ಮೆಚ್ಚಿನವುಗಳು",
+ "Folders": "ಫೋಲ್ಡರ್‌ಗಳು",
+ "Forced": "ಬಲವಂತವಾಗಿ",
+ "Genres": "ಪ್ರಕಾರಗಳು",
+ "HeaderContinueWatching": "ನೋಡುವುದನ್ನು ಮುಂದುವರಿಸಿ",
+ "HeaderFavoriteAlbums": "ಮೆಚ್ಚಿನ ಸಂಪುಟಗಳು",
+ "HeaderFavoriteArtists": "ಮೆಚ್ಚಿನ ಕಲಾವಿದರು",
+ "HeaderFavoriteShows": "ಮೆಚ್ಚಿನ ಪ್ರದರ್ಶನಗಳು",
+ "HeaderFavoriteSongs": "ಮೆಚ್ಚಿನ ಹಾಡುಗಳು",
+ "HeaderLiveTV": "ನೇರ ದೂರದರ್ಶನ",
+ "HeaderNextUp": "ಮುಂದೆ",
+ "HeaderRecordingGroups": "ರೆಕಾರ್ಡಿಂಗ್ ಗುಂಪುಗಳು",
+ "MessageApplicationUpdated": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+ "CameraImageUploadedFrom": "ಹೊಸ ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು {0} ನಿಂದ ಅಪ್‌ಲೋಡ್ ಮಾಡಲಾಗಿದೆ",
+ "Channels": "ಮೂಲಗಳು",
+ "HeaderAlbumArtists": "ಸಂಪುಟ ಕಲಾವಿದರು",
+ "HeaderFavoriteEpisodes": "ಮೆಚ್ಚಿನ ಸಂಚಿಕೆಗಳು",
+ "HearingImpaired": "ಮೂಗ",
+ "ItemAddedWithName": "{0} ಅನ್ನು ಸಂಕಲನಕ್ಕೆ ಸೇರಿಸಲಾಗಿದೆ",
+ "MessageApplicationUpdatedTo": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ",
+ "MessageNamedServerConfigurationUpdatedWithValue": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ವಿಭಾಗ {0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+ "NewVersionIsAvailable": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್‌ನ ಹೊಸ ಆವೃತ್ತಿಯು ಡೌನ್‌ಲೋಡ್‌ಗೆ ಲಭ್ಯವಿದೆ.",
+ "NotificationOptionAudioPlayback": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ",
+ "NotificationOptionCameraImageUploaded": "ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು ಅಪ್ಲೋಡ್ ಮಾಡಲಾಗಿದೆ",
+ "NotificationOptionPluginUninstalled": "ಪ್ಲಗಿನ್ ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ",
+ "NotificationOptionUserLockedOut": "ಬಳಕೆದಾರರು ಲಾಕ್ ಔಟ್ ಆಗಿದ್ದಾರೆ",
+ "NotificationOptionVideoPlaybackStopped": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ",
+ "PluginUninstalledWithName": "{0} ಅನ್ನು ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ",
+ "ScheduledTaskFailedWithName": "{0} ವಿಫಲವಾಗಿದೆ",
+ "ScheduledTaskStartedWithName": "{0} ಪ್ರಾರಂಭವಾಯಿತು",
+ "ServerNameNeedsToBeRestarted": "{0} ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಬೇಕಾಗಿದೆ",
+ "UserCreatedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ರಚಿಸಲಾಗಿದೆ",
+ "UserLockedOutWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಲಾಕ್ ಮಾಡಲಾಗಿದೆ",
+ "UserOnlineFromDevice": "{1} ನಿಂದ {0} ಆನ್‌ಲೈನ್‌ನಲ್ಲಿದೆ",
+ "UserPasswordChangedWithName": "{0} ಬಳಕೆದಾರರಿಗಾಗಿ ಪಾಸ್‌ವರ್ಡ್ ಅನ್ನು ಬದಲಾಯಿಸಲಾಗಿದೆ",
+ "UserPolicyUpdatedWithName": "ಬಳಕೆದಾರರ ನೀತಿಯನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ",
+ "UserStartedPlayingItemWithValues": "{2} ರಂದು {0} ಆಡುತ್ತಿದೆ {1}",
+ "UserStoppedPlayingItemWithValues": "{0} ಅವರು {1} ಅನ್ನು {2} ನಲ್ಲಿ ಆಡುವುದನ್ನು ಮುಗಿಸಿದ್ದಾರೆ",
+ "VersionNumber": "ಆವೃತ್ತಿ {0}",
+ "TasksMaintenanceCategory": "ನಿರ್ವಹಣೆ",
+ "TaskCleanActivityLog": "ಕ್ಲೀನ್ ಚಟುವಟಿಕೆ ಲಾಗ್",
+ "TaskCleanActivityLogDescription": "ಕಾನ್ಫಿಗರ್ ಮಾಡಿದ ವಯಸ್ಸಿಗಿಂತ ಹಳೆಯದಾದ ಚಟುವಟಿಕೆ ಲಾಗ್ ನಮೂದುಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+ "TaskRefreshChapterImages": "ಅಧ್ಯಾಯ ಚಿತ್ರಗಳನ್ನು ಹೊರತೆಗೆಯಿರಿ",
+ "TaskRefreshChapterImagesDescription": "ಅಧ್ಯಾಯಗಳನ್ನು ಹೊಂದಿರುವ ವೀಡಿಯೊಗಳಿಗಾಗಿ ಥಂಬ್‌ನೇಲ್‌ಗಳನ್ನು ರಚಿಸುತ್ತದೆ.",
+ "TaskRefreshLibraryDescription": "ಹೊಸ ಫೈಲ್‌ಗಳಿಗಾಗಿ ನಿಮ್ಮ ಮೀಡಿಯಾ ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮೆಟಾಡೇಟಾವನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ.",
+ "TaskCleanLogsDescription": "{0} ದಿನಗಳಿಗಿಂತ ಹಳೆಯದಾದ ಲಾಗ್ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+ "TaskUpdatePluginsDescription": "ಸ್ವಯಂಚಾಲಿತವಾಗಿ ನವೀಕರಿಸಲು ಕಾನ್ಫಿಗರ್ ಮಾಡಲಾದ ಪ್ಲಗಿನ್‌ಗಳಿಗಾಗಿ ನವೀಕರಣಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಸ್ಥಾಪಿಸುತ್ತದೆ.",
+ "TaskCleanTranscodeDescription": "ಒಂದು ದಿನಕ್ಕಿಂತ ಹಳೆಯದಾದ ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಫೈಲ್‌ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
+ "TaskDownloadMissingSubtitles": "ಕಾಣೆಯಾದ ಉಪಶೀರ್ಷಿಕೆಗಳನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಿ",
+ "Shows": "ಧಾರವಾಹಿಗಳು",
+ "Songs": "ಹಾಡುಗಳು",
+ "StartupEmbyServerIsLoading": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಲೋಡ್ ಆಗುತ್ತಿದೆ. ದಯವಿಟ್ಟು ಸ್ವಲ್ಪ ಸಮಯದ ನಂತರ ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.",
+ "UserDeletedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಅಳಿಸಲಾಗಿದೆ",
+ "UserDownloadingItemWithValues": "{0} ಡೌನ್‌ಲೋಡ್ ಆಗುತ್ತಿದೆ {1}",
+ "SubtitleDownloadFailureFromForItem": "ಉಪಶೀರ್ಷಿಕೆಗಳು {0} ನಿಂದ {1} ಗಾಗಿ ಡೌನ್‌ಲೋಡ್ ಮಾಡಲು ವಿಫಲವಾಗಿವೆ",
+ "Sync": "ಹೊಂದಿಕೆ",
+ "System": "ವ್ಯವಸ್ಥೆ",
+ "TvShows": "ದೂರದರ್ಶನ ಕಾರ್ಯಕ್ರಮಗಳು",
+ "Undefined": "ವ್ಯಾಖ್ಯಾನಿಸಲಾಗಿಲ್ಲ",
+ "User": "ಬಳಕೆದಾರ",
+ "HomeVideos": "ಮುಖಪುಟ ವೀಡಿಯೊಗಳು",
+ "Inherit": "ಪಾರಂಪರ್ಯವಾಗಿ",
+ "ItemRemovedWithName": "{0} ಅನ್ನು ಸಂಕಲನದಿಂದ ತೆಗೆದುಹಾಕಲಾಗಿದೆ",
+ "LabelIpAddressValue": "IP ವಿಳಾಸ: {0}",
+ "LabelRunningTimeValue": "ಅವಧಿ: {0}",
+ "Latest": "ಹೊಸದಾದ",
+ "MessageServerConfigurationUpdated": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+ "MixedContent": "ಮಿಶ್ರ ವಿಷಯ",
+ "Movies": "ಚಲನಚಿತ್ರಗಳು",
+ "Music": "ಸಂಗೀತ",
+ "MusicVideos": "ಸಂಗೀತ ವೀಡಿಯೊಗಳು",
+ "NameInstallFailed": "{0} ಸ್ಥಾಪನೆ ವಿಫಲವಾಗಿದೆ",
+ "NameSeasonNumber": "ಸೀಸನ್ {0}",
+ "NameSeasonUnknown": "ಸೀಸನ್ ತಿಳಿದಿಲ್ಲ",
+ "NotificationOptionApplicationUpdateAvailable": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣ ಲಭ್ಯವಿದೆ",
+ "NotificationOptionApplicationUpdateInstalled": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
+ "NotificationOptionAudioPlaybackStopped": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ",
+ "NotificationOptionInstallationFailed": "ಸ್ಥಾಪನ ವೈಫಲ್ಯ",
+ "NotificationOptionNewLibraryContent": "ಹೊಸ ವಿಷಯವನ್ನು ಒಳಗೊಂಡಿದೆ",
+ "NotificationOptionPluginError": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
+ "NotificationOptionPluginInstalled": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
+ "NotificationOptionPluginUpdateInstalled": "ಪ್ಲಗಿನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
+ "NotificationOptionServerRestartRequired": "ಸರ್ವರ್ ಮರುಪ್ರಾರಂಭದ ಅಗತ್ಯವಿದೆ",
+ "NotificationOptionTaskFailed": "ನಿಗದಿತ ಕಾರ್ಯ ವೈಫಲ್ಯ",
+ "NotificationOptionVideoPlayback": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ",
+ "Photos": "ಚಿತ್ರಗಳು",
+ "Playlists": "ಪ್ಲೇಪಟ್ಟಿಗಳು",
+ "Plugin": "ಪ್ಲಗಿನ್",
+ "PluginInstalledWithName": "{0} ಅನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
+ "PluginUpdatedWithName": "{0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
+ "ProviderValue": "ಒದಗಿಸುವವರು: {0}",
+ "TaskCleanLogs": "ಕ್ಲೀನ್ ಲಾಗ್ ಡೈರೆಕ್ಟರಿ",
+ "TaskRefreshPeople": "ಜನರನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
+ "TaskRefreshPeopleDescription": "ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಯಲ್ಲಿ ನಟರು ಮತ್ತು ನಿರ್ದೇಶಕರಿಗಾಗಿ ಮೆಟಾಡೇಟಾವನ್ನು ನವೀಕರಿಸಿ.",
+ "TaskUpdatePlugins": "ಪ್ಲಗಿನ್‌ಗಳನ್ನು ನವೀಕರಿಸಿ",
+ "TaskCleanTranscode": "ಟ್ರಾನ್ಸ್‌ಕೋಡ್ ಡೈರೆಕ್ಟರಿಯನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ",
+ "TaskRefreshChannels": "ಚಾನಲ್‌ಗಳನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
+ "TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ."
}
diff --git a/Emby.Server.Implementations/Localization/Core/ml.json b/Emby.Server.Implementations/Localization/Core/ml.json
index 0620fbcdb..0b50fa529 100644
--- a/Emby.Server.Implementations/Localization/Core/ml.json
+++ b/Emby.Server.Implementations/Localization/Core/ml.json
@@ -121,5 +121,7 @@
"TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്‌കാൻ ചെയ്‌തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്‌ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്‌തതിന് ശേഷം ഈ ടാസ്‌ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
"TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക",
"HearingImpaired": "കേൾവി തകരാറുകൾ",
- "External": "പുറമേയുള്ള"
+ "External": "പുറമേയുള്ള",
+ "TaskKeyframeExtractorDescription": "കൂടുതൽ കൃത്യമായ HLS പ്ലേലിസ്റ്റുകൾ സൃഷ്‌ടിക്കുന്നതിന് വീഡിയോ ഫയലുകളിൽ നിന്ന് കീഫ്രെയിമുകൾ എക്‌സ്‌ട്രാക്‌റ്റ് ചെയ്യുന്നു. ഈ പ്രവർത്തനം പൂർത്തിയാവാൻ കുറച്ചധികം സമയം എടുത്തേക്കാം.",
+ "TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ms.json b/Emby.Server.Implementations/Localization/Core/ms.json
index 904443bea..a07222975 100644
--- a/Emby.Server.Implementations/Localization/Core/ms.json
+++ b/Emby.Server.Implementations/Localization/Core/ms.json
@@ -1,5 +1,5 @@
{
- "Albums": "Albums",
+ "Albums": "Album",
"AppDeviceValues": "Apl: {0}, Peranti: {1}",
"Application": "Aplikasi",
"Artists": "Artis-artis",
diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json
index d2446a7fd..26dc5ce82 100644
--- a/Emby.Server.Implementations/Localization/Core/pr.json
+++ b/Emby.Server.Implementations/Localization/Core/pr.json
@@ -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"
}
diff --git a/Emby.Server.Implementations/Localization/Core/zu.json b/Emby.Server.Implementations/Localization/Core/zu.json
index b5f4b920f..aa056d449 100644
--- a/Emby.Server.Implementations/Localization/Core/zu.json
+++ b/Emby.Server.Implementations/Localization/Core/zu.json
@@ -25,5 +25,14 @@
"Channels": "Amashaneli",
"Books": "Izincwadi",
"Artists": "Abadlali",
- "Albums": "Ama-albhamu"
+ "Albums": "Ama-albhamu",
+ "CameraImageUploadedFrom": "Kulandelayo lwesithonjana sekhamera selithunyelwe kusuka ku {0}",
+ "HeaderFavoriteArtists": "Abasethi Abathandekayo",
+ "HeaderFavoriteEpisodes": "Izilimi Ezithandekayo",
+ "HeaderFavoriteShows": "Izisho Ezithandekayo",
+ "External": "Kwezifungo",
+ "FailedLoginAttemptWithUserName": "Ukushayiswa kwesithombe sokungena okungekho {0}",
+ "HeaderContinueWatching": "Buyela Ukubona",
+ "HeaderFavoriteAlbums": "Izimpahla Ezithandwayo",
+ "HeaderAlbumArtists": "Abasethi wenkulumo"
}
diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
index 7732e32d0..896f47923 100644
--- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
+++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs
@@ -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)
diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
index 0cb450cdf..649c49924 100644
--- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs
+++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs
@@ -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())
diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs
index 1303012e1..d7189ef0c 100644
--- a/Emby.Server.Implementations/Plugins/PluginManager.cs
+++ b/Emby.Server.Implementations/Plugins/PluginManager.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Data;
using System.Globalization;
using System.IO;
using System.Linq;
@@ -11,7 +10,6 @@ using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Emby.Server.Implementations.Library;
-using Jellyfin.Extensions;
using Jellyfin.Extensions.Json;
using Jellyfin.Extensions.Json.Converters;
using MediaBrowser.Common;
@@ -30,7 +28,7 @@ namespace Emby.Server.Implementations.Plugins
/// <summary>
/// Defines the <see cref="PluginManager" />.
/// </summary>
- public class PluginManager : IPluginManager
+ public sealed class PluginManager : IPluginManager, IDisposable
{
private const string MetafileName = "meta.json";
@@ -191,15 +189,6 @@ namespace Emby.Server.Implementations.Plugins
}
}
- /// <inheritdoc />
- public void UnloadAssemblies()
- {
- foreach (var assemblyLoadContext in _assemblyLoadContexts)
- {
- assemblyLoadContext.Unload();
- }
- }
-
/// <summary>
/// Creates all the plugin instances.
/// </summary>
@@ -441,6 +430,15 @@ namespace Emby.Server.Implementations.Plugins
return SaveManifest(manifest, path);
}
+ /// <inheritdoc />
+ public void Dispose()
+ {
+ foreach (var assemblyLoadContext in _assemblyLoadContexts)
+ {
+ assemblyLoadContext.Unload();
+ }
+ }
+
/// <summary>
/// Reconciles the manifest against any properties that exist locally in a pre-packaged meta.json found at the path.
/// If no file is found, no reconciliation occurs.
diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs
index b4a622ccf..e935f7e5e 100644
--- a/Emby.Server.Implementations/Session/SessionManager.cs
+++ b/Emby.Server.Implementations/Session/SessionManager.cs
@@ -36,6 +36,7 @@ using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
@@ -44,7 +45,7 @@ namespace Emby.Server.Implementations.Session
/// <summary>
/// Class SessionManager.
/// </summary>
- public class SessionManager : ISessionManager, IDisposable
+ public sealed class SessionManager : ISessionManager, IAsyncDisposable
{
private readonly IUserDataManager _userDataManager;
private readonly ILogger<SessionManager> _logger;
@@ -57,11 +58,9 @@ namespace Emby.Server.Implementations.Session
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IServerApplicationHost _appHost;
private readonly IDeviceManager _deviceManager;
-
- /// <summary>
- /// The active connections.
- /// </summary>
- private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections = new(StringComparer.OrdinalIgnoreCase);
+ private readonly CancellationTokenRegistration _shutdownCallback;
+ private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections
+ = new(StringComparer.OrdinalIgnoreCase);
private Timer _idleTimer;
@@ -79,7 +78,8 @@ namespace Emby.Server.Implementations.Session
IImageProcessor imageProcessor,
IServerApplicationHost appHost,
IDeviceManager deviceManager,
- IMediaSourceManager mediaSourceManager)
+ IMediaSourceManager mediaSourceManager,
+ IHostApplicationLifetime hostApplicationLifetime)
{
_logger = logger;
_eventManager = eventManager;
@@ -92,6 +92,7 @@ namespace Emby.Server.Implementations.Session
_appHost = appHost;
_deviceManager = deviceManager;
_mediaSourceManager = mediaSourceManager;
+ _shutdownCallback = hostApplicationLifetime.ApplicationStopping.Register(OnApplicationStopping);
_deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated;
}
@@ -151,36 +152,6 @@ namespace Emby.Server.Implementations.Session
}
}
- /// <inheritdoc />
- public void Dispose()
- {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
-
- /// <summary>
- /// Releases unmanaged and optionally managed resources.
- /// </summary>
- /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
- protected virtual void Dispose(bool disposing)
- {
- if (_disposed)
- {
- return;
- }
-
- if (disposing)
- {
- _idleTimer?.Dispose();
- }
-
- _idleTimer = null;
-
- _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
-
- _disposed = true;
- }
-
private void CheckDisposed()
{
if (_disposed)
@@ -980,28 +951,28 @@ namespace Emby.Server.Implementations.Session
private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
{
- bool playedToCompletion = false;
-
- if (!playbackFailed)
+ if (playbackFailed)
{
- var data = _userDataManager.GetUserData(user, item);
-
- if (positionTicks.HasValue)
- {
- playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
- }
- else
- {
- // If the client isn't able to report this, then we'll just have to make an assumption
- data.PlayCount++;
- data.Played = item.SupportsPlayedStatus;
- data.PlaybackPositionTicks = 0;
- playedToCompletion = true;
- }
+ return false;
+ }
- _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None);
+ var data = _userDataManager.GetUserData(user, item);
+ bool playedToCompletion;
+ if (positionTicks.HasValue)
+ {
+ playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
+ }
+ else
+ {
+ // If the client isn't able to report this, then we'll just have to make an assumption
+ data.PlayCount++;
+ data.Played = item.SupportsPlayedStatus;
+ data.PlaybackPositionTicks = 0;
+ playedToCompletion = true;
}
+ _userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None);
+
return playedToCompletion;
}
@@ -1331,32 +1302,6 @@ namespace Emby.Server.Implementations.Session
}
/// <summary>
- /// Sends the server shutdown notification.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public Task SendServerShutdownNotification(CancellationToken cancellationToken)
- {
- CheckDisposed();
-
- return SendMessageToSessions(Sessions, SessionMessageType.ServerShuttingDown, string.Empty, cancellationToken);
- }
-
- /// <summary>
- /// Sends the server restart notification.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- public Task SendServerRestartNotification(CancellationToken cancellationToken)
- {
- CheckDisposed();
-
- _logger.LogDebug("Beginning SendServerRestartNotification");
-
- return SendMessageToSessions(Sessions, SessionMessageType.ServerRestarting, string.Empty, cancellationToken);
- }
-
- /// <summary>
/// Adds the additional user.
/// </summary>
/// <param name="sessionId">The session identifier.</param>
@@ -1833,5 +1778,53 @@ namespace Emby.Server.Implementations.Session
return SendMessageToSessions(sessions, name, data, cancellationToken);
}
+
+ /// <inheritdoc />
+ public async ValueTask DisposeAsync()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ foreach (var session in _activeConnections.Values)
+ {
+ await session.DisposeAsync().ConfigureAwait(false);
+ }
+
+ if (_idleTimer is not null)
+ {
+ await _idleTimer.DisposeAsync().ConfigureAwait(false);
+ _idleTimer = null;
+ }
+
+ await _shutdownCallback.DisposeAsync().ConfigureAwait(false);
+
+ _deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
+ _disposed = true;
+ }
+
+ private async void OnApplicationStopping()
+ {
+ _logger.LogInformation("Sending shutdown notifications");
+ try
+ {
+ var messageType = _appHost.ShouldRestart ? SessionMessageType.ServerRestarting : SessionMessageType.ServerShuttingDown;
+
+ await SendMessageToSessions(Sessions, messageType, string.Empty, CancellationToken.None).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error sending server shutdown notifications");
+ }
+
+ // Close open websockets to allow Kestrel to shut down cleanly
+ foreach (var session in _activeConnections.Values)
+ {
+ await session.DisposeAsync().ConfigureAwait(false);
+ }
+
+ _activeConnections.Clear();
+ }
}
}
diff --git a/Emby.Server.Implementations/SystemManager.cs b/Emby.Server.Implementations/SystemManager.cs
new file mode 100644
index 000000000..0fdfe66ca
--- /dev/null
+++ b/Emby.Server.Implementations/SystemManager.cs
@@ -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();
+ });
+ }
+}
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index 6c198b6f9..77d385ba1 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -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;
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 065a4ce5c..42c94c29d 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -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);
}
diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs
index d7cec865e..6eedfd8c7 100644
--- a/Jellyfin.Api/Controllers/HlsSegmentController.cs
+++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs
@@ -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));
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 3c5f18af5..7b10ea170 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -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;
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 267ba4afb..649397d68 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -23,7 +23,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
-using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.LiveTv;
@@ -48,7 +47,6 @@ public class LiveTvController : BaseJellyfinApiController
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IConfigurationManager _configurationManager;
private readonly TranscodingJobHelper _transcodingJobHelper;
- private readonly ISessionManager _sessionManager;
/// <summary>
/// Initializes a new instance of the <see cref="LiveTvController"/> class.
@@ -61,7 +59,6 @@ public class LiveTvController : BaseJellyfinApiController
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
/// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
- /// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
public LiveTvController(
ILiveTvManager liveTvManager,
IUserManager userManager,
@@ -70,8 +67,7 @@ public class LiveTvController : BaseJellyfinApiController
IDtoService dtoService,
IMediaSourceManager mediaSourceManager,
IConfigurationManager configurationManager,
- TranscodingJobHelper transcodingJobHelper,
- ISessionManager sessionManager)
+ TranscodingJobHelper transcodingJobHelper)
{
_liveTvManager = liveTvManager;
_userManager = userManager;
@@ -81,7 +77,6 @@ public class LiveTvController : BaseJellyfinApiController
_mediaSourceManager = mediaSourceManager;
_configurationManager = configurationManager;
_transcodingJobHelper = transcodingJobHelper;
- _sessionManager = sessionManager;
}
/// <summary>
diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs
index 7d02550b6..fb89e9610 100644
--- a/Jellyfin.Api/Controllers/SubtitleController.cs
+++ b/Jellyfin.Api/Controllers/SubtitleController.cs
@@ -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);
diff --git a/Jellyfin.Api/Controllers/SystemController.cs b/Jellyfin.Api/Controllers/SystemController.cs
index a29790961..11095a97f 100644
--- a/Jellyfin.Api/Controllers/SystemController.cs
+++ b/Jellyfin.Api/Controllers/SystemController.cs
@@ -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);
}
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index fe602fba3..276a09f41 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -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;
- }
}
diff --git a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
index 9a141a16d..5eec1b0ca 100644
--- a/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
+++ b/Jellyfin.Api/Helpers/HlsCodecStringHelpers.cs
@@ -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.
diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs
index 782cd6568..a653c5795 100644
--- a/Jellyfin.Api/Helpers/StreamingHelpers.cs
+++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs
@@ -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!}";
diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
index 73ebb396d..c16a586d6 100644
--- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
+++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs
@@ -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));
diff --git a/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs
index 8bf626035..acf3645fd 100644
--- a/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs
+++ b/Jellyfin.Api/Middleware/RobotsRedirectionMiddleware.cs
@@ -33,8 +33,7 @@ public class RobotsRedirectionMiddleware
/// <returns>The async task.</returns>
public async Task Invoke(HttpContext httpContext)
{
- var localPath = httpContext.Request.Path.ToString();
- if (string.Equals(localPath, "/robots.txt", StringComparison.OrdinalIgnoreCase))
+ if (httpContext.Request.Path.Equals("/robots.txt", StringComparison.OrdinalIgnoreCase))
{
_logger.LogDebug("Redirecting robots.txt request to web/robots.txt");
httpContext.Response.Redirect("web/robots.txt");
diff --git a/Jellyfin.Data/Enums/PersonKind.cs b/Jellyfin.Data/Enums/PersonKind.cs
index 10a805666..29308789a 100644
--- a/Jellyfin.Data/Enums/PersonKind.cs
+++ b/Jellyfin.Data/Enums/PersonKind.cs
@@ -94,4 +94,40 @@ public enum PersonKind
/// A person who was the illustrator.
/// </summary>
Illustrator,
+
+ /// <summary>
+ /// A person responsible for drawing the art.
+ /// </summary>
+ Penciller,
+
+ /// <summary>
+ /// A person responsible for inking the pencil art.
+ /// </summary>
+ Inker,
+
+ /// <summary>
+ /// A person responsible for applying color to drawings.
+ /// </summary>
+ Colorist,
+
+ /// <summary>
+ /// A person responsible for drawing text and speech bubbles.
+ /// </summary>
+ Letterer,
+
+ /// <summary>
+ /// A person responsible for drawing the cover art.
+ /// </summary>
+ CoverArtist,
+
+ /// <summary>
+ /// A person contributing to a resource by revising or elucidating the content, e.g., adding an introduction, notes, or other critical matter.
+ /// An editor may also prepare a resource for production, publication, or distribution.
+ /// </summary>
+ Editor,
+
+ /// <summary>
+ /// A person who renders a text from one language into another.
+ /// </summary>
+ Translator
}
diff --git a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
index 700e63970..77f8f7071 100644
--- a/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
+++ b/Jellyfin.Server.Implementations/Security/AuthorizationContext.cs
@@ -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)
{
diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs
index 108d2fad4..31eda08a0 100644
--- a/Jellyfin.Server.Implementations/Users/UserManager.cs
+++ b/Jellyfin.Server.Implementations/Users/UserManager.cs
@@ -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;
diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs
index 0c6315c66..4c116745b 100644
--- a/Jellyfin.Server/CoreAppHost.cs
+++ b/Jellyfin.Server/CoreAppHost.cs
@@ -103,9 +103,6 @@ namespace Jellyfin.Server
}
/// <inheritdoc />
- protected override void RestartInternal() => Program.Restart();
-
- /// <inheritdoc />
protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal()
{
// Jellyfin.Server
@@ -114,8 +111,5 @@ namespace Jellyfin.Server
// Jellyfin.Server.Implementations
yield return typeof(JellyfinDbContext).Assembly;
}
-
- /// <inheritdoc />
- protected override void ShutdownInternal() => Program.Shutdown();
}
}
diff --git a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
index 3271e08e4..89dbbdd2f 100644
--- a/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
+++ b/Jellyfin.Server/Extensions/ApiServiceCollectionExtensions.cs
@@ -59,6 +59,7 @@ namespace Jellyfin.Server.Extensions
serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>();
serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>();
+ serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
return serviceCollection.AddAuthorizationCore(options =>
{
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
index 996f3fe8a..e1a43bb48 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
@@ -73,8 +73,7 @@ namespace Jellyfin.Server.Migrations.Routines
var queryResult = connection.Query("SELECT DISTINCT OfficialRating FROM TypedBaseItems");
foreach (var entry in queryResult)
{
- var ratingString = entry.GetString(0);
- if (string.IsNullOrEmpty(ratingString))
+ if (!entry.TryGetString(0, out var ratingString) || string.IsNullOrEmpty(ratingString))
{
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating IS NULL OR OfficialRating='';");
}
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index 6e8b17a73..f9259d0d9 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -4,7 +4,6 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
-using System.Threading;
using System.Threading.Tasks;
using CommandLine;
using Emby.Server.Implementations;
@@ -42,7 +41,6 @@ namespace Jellyfin.Server
public const string LoggingConfigFileSystem = "logging.json";
private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory();
- private static CancellationTokenSource _tokenSource = new();
private static long _startTimestamp;
private static ILogger _logger = NullLogger.Instance;
private static bool _restartOnShutdown;
@@ -65,36 +63,9 @@ namespace Jellyfin.Server
.MapResult(StartApp, ErrorParsingArguments);
}
- /// <summary>
- /// Shuts down the application.
- /// </summary>
- internal static void Shutdown()
- {
- if (!_tokenSource.IsCancellationRequested)
- {
- _tokenSource.Cancel();
- }
- }
-
- /// <summary>
- /// Restarts the application.
- /// </summary>
- internal static void Restart()
- {
- _restartOnShutdown = true;
-
- Shutdown();
- }
-
private static async Task StartApp(StartupOptions options)
{
_startTimestamp = Stopwatch.GetTimestamp();
-
- // Log all uncaught exceptions to std error
- static void UnhandledExceptionToConsole(object sender, UnhandledExceptionEventArgs e) =>
- Console.Error.WriteLine("Unhandled Exception\n" + e.ExceptionObject);
- AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionToConsole;
-
ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options);
// $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
@@ -112,38 +83,10 @@ namespace Jellyfin.Server
StartupHelpers.InitializeLoggingFramework(startupConfig, appPaths);
_logger = _loggerFactory.CreateLogger("Main");
- // Log uncaught exceptions to the logging instead of std error
- AppDomain.CurrentDomain.UnhandledException -= UnhandledExceptionToConsole;
+ // Use the logging framework for uncaught exceptions instead of std error
AppDomain.CurrentDomain.UnhandledException += (_, e)
=> _logger.LogCritical((Exception)e.ExceptionObject, "Unhandled Exception");
- // Intercept Ctrl+C and Ctrl+Break
- Console.CancelKeyPress += (_, e) =>
- {
- if (_tokenSource.IsCancellationRequested)
- {
- return; // Already shutting down
- }
-
- e.Cancel = true;
- _logger.LogInformation("Ctrl+C, shutting down");
- Environment.ExitCode = 128 + 2;
- Shutdown();
- };
-
- // Register a SIGTERM handler
- AppDomain.CurrentDomain.ProcessExit += (_, _) =>
- {
- if (_tokenSource.IsCancellationRequested)
- {
- return; // Already shutting down
- }
-
- _logger.LogInformation("Received a SIGTERM signal, shutting down");
- Environment.ExitCode = 128 + 15;
- Shutdown();
- };
-
_logger.LogInformation(
"Jellyfin version: {Version}",
Assembly.GetEntryAssembly()!.GetName().Version!.ToString(3));
@@ -173,12 +116,10 @@ namespace Jellyfin.Server
do
{
- _restartOnShutdown = false;
await StartServer(appPaths, options, startupConfig).ConfigureAwait(false);
if (_restartOnShutdown)
{
- _tokenSource = new CancellationTokenSource();
_startTimestamp = Stopwatch.GetTimestamp();
}
} while (_restartOnShutdown);
@@ -186,7 +127,7 @@ namespace Jellyfin.Server
private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig)
{
- var appHost = new CoreAppHost(
+ using var appHost = new CoreAppHost(
appPaths,
_loggerFactory,
options,
@@ -196,6 +137,7 @@ namespace Jellyfin.Server
try
{
host = Host.CreateDefaultBuilder()
+ .UseConsoleLifetime()
.ConfigureServices(services => appHost.Init(services))
.ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths, _logger))
.ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig))
@@ -210,7 +152,7 @@ namespace Jellyfin.Server
try
{
- await host.StartAsync(_tokenSource.Token).ConfigureAwait(false);
+ await host.StartAsync().ConfigureAwait(false);
if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket())
{
@@ -219,22 +161,18 @@ namespace Jellyfin.Server
StartupHelpers.SetUnixSocketPermissions(startupConfig, socketPath, _logger);
}
}
- catch (Exception ex) when (ex is not TaskCanceledException)
+ catch (Exception)
{
_logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again");
throw;
}
- await appHost.RunStartupTasksAsync(_tokenSource.Token).ConfigureAwait(false);
+ await appHost.RunStartupTasksAsync().ConfigureAwait(false);
_logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp));
- // Block main thread until shutdown
- await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false);
- }
- catch (TaskCanceledException)
- {
- // Don't throw on cancellation
+ await host.WaitForShutdownAsync().ConfigureAwait(false);
+ _restartOnShutdown = appHost.ShouldRestart;
}
catch (Exception ex)
{
@@ -257,7 +195,6 @@ namespace Jellyfin.Server
}
}
- await appHost.DisposeAsync().ConfigureAwait(false);
host?.Dispose();
}
}
diff --git a/MediaBrowser.Common/Extensions/ProcessExtensions.cs b/MediaBrowser.Common/Extensions/ProcessExtensions.cs
index c3a7cb394..bb8ab130d 100644
--- a/MediaBrowser.Common/Extensions/ProcessExtensions.cs
+++ b/MediaBrowser.Common/Extensions/ProcessExtensions.cs
@@ -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);
}
}
}
diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs
index 96ee701b3..23795c6be 100644
--- a/MediaBrowser.Common/IApplicationHost.cs
+++ b/MediaBrowser.Common/IApplicationHost.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Reflection;
-using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common
@@ -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.
@@ -88,11 +86,6 @@ namespace MediaBrowser.Common
void NotifyPendingRestart();
/// <summary>
- /// Restarts this instance.
- /// </summary>
- void Restart();
-
- /// <summary>
/// Gets the exports.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
@@ -124,12 +117,6 @@ namespace MediaBrowser.Common
T Resolve<T>();
/// <summary>
- /// Shuts down.
- /// </summary>
- /// <returns>A task.</returns>
- Task Shutdown();
-
- /// <summary>
/// Initializes this instance.
/// </summary>
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
diff --git a/MediaBrowser.Common/Plugins/IPluginManager.cs b/MediaBrowser.Common/Plugins/IPluginManager.cs
index 1d73de3c9..0ff9719e9 100644
--- a/MediaBrowser.Common/Plugins/IPluginManager.cs
+++ b/MediaBrowser.Common/Plugins/IPluginManager.cs
@@ -30,11 +30,6 @@ namespace MediaBrowser.Common.Plugins
IEnumerable<Assembly> LoadAssemblies();
/// <summary>
- /// Unloads all of the assemblies.
- /// </summary>
- void UnloadAssemblies();
-
- /// <summary>
/// Registers the plugin's services with the DI.
/// Note: DI is not yet instantiated yet.
/// </summary>
diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
index cdc3d52b9..0d1e2a5a0 100644
--- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs
+++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs
@@ -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;
@@ -74,14 +73,6 @@ namespace MediaBrowser.Controller.Drawing
/// 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>
- /// <param name="options">The options.</param>
/// <returns>Task.</returns>
Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options);
@@ -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);
}
}
diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
index 7912c5e87..953cfe698 100644
--- a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
+++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs
@@ -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));
}
}
}
diff --git a/MediaBrowser.Controller/Entities/CollectionFolder.cs b/MediaBrowser.Controller/Entities/CollectionFolder.cs
index 095b261c0..f51162f9d 100644
--- a/MediaBrowser.Controller/Entities/CollectionFolder.cs
+++ b/MediaBrowser.Controller/Entities/CollectionFolder.cs
@@ -3,6 +3,7 @@
#pragma warning disable CS1591
using System;
+using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@@ -29,7 +30,7 @@ namespace MediaBrowser.Controller.Entities
public class CollectionFolder : Folder, ICollectionFolder
{
private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
- private static readonly Dictionary<string, LibraryOptions> _libraryOptions = new Dictionary<string, LibraryOptions>();
+ private static readonly ConcurrentDictionary<string, LibraryOptions> _libraryOptions = new ConcurrentDictionary<string, LibraryOptions>();
private bool _requiresRefresh;
/// <summary>
@@ -139,45 +140,26 @@ namespace MediaBrowser.Controller.Entities
}
public static LibraryOptions GetLibraryOptions(string path)
- {
- lock (_libraryOptions)
- {
- if (!_libraryOptions.TryGetValue(path, out var options))
- {
- options = LoadLibraryOptions(path);
- _libraryOptions[path] = options;
- }
-
- return options;
- }
- }
+ => _libraryOptions.GetOrAdd(path, LoadLibraryOptions);
public static void SaveLibraryOptions(string path, LibraryOptions options)
{
- lock (_libraryOptions)
- {
- _libraryOptions[path] = options;
+ _libraryOptions[path] = options;
- var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions);
- foreach (var mediaPath in clone.PathInfos)
+ var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions);
+ foreach (var mediaPath in clone.PathInfos)
+ {
+ if (!string.IsNullOrEmpty(mediaPath.Path))
{
- if (!string.IsNullOrEmpty(mediaPath.Path))
- {
- mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path);
- }
+ mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path);
}
-
- XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path));
}
+
+ XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path));
}
public static void OnCollectionFolderChange()
- {
- lock (_libraryOptions)
- {
- _libraryOptions.Clear();
- }
- }
+ => _libraryOptions.Clear();
public override bool IsSaveLocalMetadataEnabled()
{
diff --git a/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs
new file mode 100644
index 000000000..2742f21e3
--- /dev/null
+++ b/MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs
@@ -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 });
+}
diff --git a/MediaBrowser.Controller/IServerApplicationHost.cs b/MediaBrowser.Controller/IServerApplicationHost.cs
index 45ac5c3a8..e9c4d9e19 100644
--- a/MediaBrowser.Controller/IServerApplicationHost.cs
+++ b/MediaBrowser.Controller/IServerApplicationHost.cs
@@ -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>
@@ -42,15 +39,6 @@ namespace MediaBrowser.Controller
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>
/// <param name="request">The <see cref="HttpRequest"/> instance.</param>
diff --git a/MediaBrowser.Controller/ISystemManager.cs b/MediaBrowser.Controller/ISystemManager.cs
new file mode 100644
index 000000000..ef3034d2f
--- /dev/null
+++ b/MediaBrowser.Controller/ISystemManager.cs
@@ -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();
+}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index b6e680ab9..fba347bda 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -48,6 +48,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3);
private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1);
+ private readonly Version _minFFmpegVaapiH26xEncA53CcSei = new Version(6, 0);
private static readonly string[] _videoProfilesH264 = new[]
{
@@ -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
diff --git a/MediaBrowser.Controller/Resolvers/IItemResolver.cs b/MediaBrowser.Controller/Resolvers/IItemResolver.cs
index b95d00aa3..282aa721e 100644
--- a/MediaBrowser.Controller/Resolvers/IItemResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/IItemResolver.cs
@@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>BaseItem.</returns>
- BaseItem ResolvePath(ItemResolveArgs args);
+ BaseItem? ResolvePath(ItemResolveArgs args);
}
public interface IMultiItemResolver
diff --git a/MediaBrowser.Controller/Resolvers/ItemResolver.cs b/MediaBrowser.Controller/Resolvers/ItemResolver.cs
index a6da8384e..5c9dd6f07 100644
--- a/MediaBrowser.Controller/Resolvers/ItemResolver.cs
+++ b/MediaBrowser.Controller/Resolvers/ItemResolver.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
@@ -23,7 +21,7 @@ namespace MediaBrowser.Controller.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>`0.</returns>
- protected internal virtual T Resolve(ItemResolveArgs args)
+ protected internal virtual T? Resolve(ItemResolveArgs args)
{
return null;
}
@@ -42,7 +40,7 @@ namespace MediaBrowser.Controller.Resolvers
/// </summary>
/// <param name="args">The args.</param>
/// <returns>BaseItem.</returns>
- public BaseItem ResolvePath(ItemResolveArgs args)
+ public BaseItem? ResolvePath(ItemResolveArgs args)
{
var item = Resolve(args);
diff --git a/MediaBrowser.Controller/Session/ISessionManager.cs b/MediaBrowser.Controller/Session/ISessionManager.cs
index 0c4719a0e..53df7133b 100644
--- a/MediaBrowser.Controller/Session/ISessionManager.cs
+++ b/MediaBrowser.Controller/Session/ISessionManager.cs
@@ -233,20 +233,6 @@ namespace MediaBrowser.Controller.Session
Task SendRestartRequiredNotification(CancellationToken cancellationToken);
/// <summary>
- /// Sends the server shutdown notification.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- Task SendServerShutdownNotification(CancellationToken cancellationToken);
-
- /// <summary>
- /// Sends the server restart notification.
- /// </summary>
- /// <param name="cancellationToken">The cancellation token.</param>
- /// <returns>Task.</returns>
- Task SendServerRestartNotification(CancellationToken cancellationToken);
-
- /// <summary>
/// Adds the additional user.
/// </summary>
/// <param name="sessionId">The session identifier.</param>
diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
index cb369d837..8a870e0d9 100644
--- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
@@ -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;
@@ -1042,83 +781,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>
/// <param name="reader">The xml reader.</param>
@@ -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);
- }
}
}
diff --git a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs
index 88b190f2b..879a3616b 100644
--- a/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/PlaylistXmlParser.cs
@@ -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
diff --git a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
index 989e386a5..299f294b2 100644
--- a/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
+++ b/MediaBrowser.MediaEncoding/Attachments/AttachmentExtractor.cs
@@ -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);
}
}
}
diff --git a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
index 346e97ae1..99c49e4ae 100644
--- a/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Encoder/MediaEncoder.cs
@@ -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()
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index 3055e6cde..441a3abd4 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -78,6 +78,7 @@ namespace MediaBrowser.MediaEncoding.Probing
"She/Her/Hers",
"5/8erl in Ehr'n",
"Smith/Kotzen",
+ "We;Na",
};
/// <summary>
diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
index a41e0b7e9..21fa4468e 100644
--- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
+++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs
@@ -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;
diff --git a/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs b/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs
index 68a5c2534..1bb24112e 100644
--- a/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs
+++ b/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs
@@ -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))
+ };
}
diff --git a/MediaBrowser.Model/IO/IFileSystem.cs b/MediaBrowser.Model/IO/IFileSystem.cs
index 5bdda2b24..ec381d423 100644
--- a/MediaBrowser.Model/IO/IFileSystem.cs
+++ b/MediaBrowser.Model/IO/IFileSystem.cs
@@ -10,8 +10,6 @@ namespace MediaBrowser.Model.IO
/// </summary>
public interface IFileSystem
{
- void AddShortcutHandler(IShortcutHandler handler);
-
/// <summary>
/// Determines whether the specified filename is shortcut.
/// </summary>
diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs
index e7c2cd255..d82716831 100644
--- a/MediaBrowser.Providers/Manager/ImageSaver.cs
+++ b/MediaBrowser.Providers/Manager/ImageSaver.cs
@@ -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))
{
diff --git a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs
index c24f4e2fc..0bfee07fd 100644
--- a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs
+++ b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs
@@ -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
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
index 5b68924ac..47a127950 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -13,6 +13,7 @@ using MediaBrowser.Common.Providers;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
@@ -261,158 +262,84 @@ namespace MediaBrowser.XbmcMetadata.Parsers
protected virtual void FetchDataFromXmlNode(XmlReader reader, MetadataResult<T> itemResult)
{
var item = itemResult.Item;
-
var nfoConfiguration = _config.GetNfoConfiguration();
- UserItemData? userData = null;
+ UserItemData? userData;
switch (reader.Name)
{
- // DateCreated
case "dateadded":
+ if (reader.TryReadDateTime(out var dateCreated))
{
- var val = reader.ReadElementContentAsString();
-
- if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added))
- {
- item.DateCreated = added;
- }
- else
- {
- Logger.LogWarning("Invalid Added value found: {Value}", val);
- }
-
- break;
+ item.DateCreated = dateCreated;
}
+ break;
case "originaltitle":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrEmpty(val))
- {
- item.OriginalTitle = val;
- }
-
- break;
- }
-
+ item.OriginalTitle = reader.ReadNormalizedString();
+ break;
case "name":
case "title":
case "localtitle":
- item.Name = reader.ReadElementContentAsString();
+ item.Name = reader.ReadNormalizedString();
break;
-
case "sortname":
- item.SortName = reader.ReadElementContentAsString();
+ item.SortName = reader.ReadNormalizedString();
break;
-
case "criticrating":
+ var criticRatingText = reader.ReadElementContentAsString();
+ if (float.TryParse(criticRatingText, CultureInfo.InvariantCulture, out var value))
{
- var text = reader.ReadElementContentAsString();
-
- if (float.TryParse(text, CultureInfo.InvariantCulture, out var value))
- {
- item.CriticRating = value;
- }
-
- break;
+ item.CriticRating = value;
}
+ break;
case "sorttitle":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- item.ForcedSortName = val;
- }
-
- break;
- }
-
+ item.ForcedSortName = reader.ReadNormalizedString();
+ break;
case "biography":
case "plot":
case "review":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- item.Overview = val;
- }
-
- break;
- }
-
+ item.Overview = reader.ReadNormalizedString();
+ break;
case "language":
- {
- var val = reader.ReadElementContentAsString();
-
- item.PreferredMetadataLanguage = val;
-
- break;
- }
-
+ item.PreferredMetadataLanguage = reader.ReadNormalizedString();
+ break;
case "watched":
+ var played = reader.ReadElementContentAsBoolean();
+ if (!string.IsNullOrWhiteSpace(nfoConfiguration.UserId))
{
- var val = reader.ReadElementContentAsBoolean();
-
- if (!string.IsNullOrWhiteSpace(nfoConfiguration.UserId))
- {
- var user = _userManager.GetUserById(Guid.Parse(nfoConfiguration.UserId));
- userData = _userDataManager.GetUserData(user, item);
- userData.Played = val;
- _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
- }
-
- break;
+ var user = _userManager.GetUserById(Guid.Parse(nfoConfiguration.UserId));
+ userData = _userDataManager.GetUserData(user, item);
+ userData.Played = played;
+ _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
}
+ break;
case "playcount":
+ if (reader.TryReadInt(out var count)
+ && Guid.TryParse(nfoConfiguration.UserId, out var playCountUserId))
{
- var val = reader.ReadElementContentAsString();
- if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var count)
- && Guid.TryParse(nfoConfiguration.UserId, out var guid))
- {
- var user = _userManager.GetUserById(guid);
- userData = _userDataManager.GetUserData(user, item);
- userData.PlayCount = count;
- _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
- }
-
- break;
+ var user = _userManager.GetUserById(playCountUserId);
+ userData = _userDataManager.GetUserData(user, item);
+ userData.PlayCount = count;
+ _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
}
+ break;
case "lastplayed":
+ if (reader.TryReadDateTime(out var lastPlayed)
+ && Guid.TryParse(nfoConfiguration.UserId, out var lastPlayedUserId))
{
- var val = reader.ReadElementContentAsString();
- if (Guid.TryParse(nfoConfiguration.UserId, out var guid))
- {
- if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var added))
- {
- var user = _userManager.GetUserById(guid);
- userData = _userDataManager.GetUserData(user, item);
- userData.LastPlayedDate = added;
- _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
- }
- else
- {
- Logger.LogWarning("Invalid lastplayed value found: {Value}", val);
- }
- }
-
- break;
+ var user = _userManager.GetUserById(lastPlayedUserId);
+ userData = _userDataManager.GetUserData(user, item);
+ userData.LastPlayedDate = lastPlayed;
+ _userDataManager.SaveUserData(user, item, userData, UserDataSaveReason.Import, CancellationToken.None);
}
+ break;
case "countrycode":
- {
- var val = reader.ReadElementContentAsString();
-
- item.PreferredMetadataCountryCode = val;
-
- break;
- }
-
+ item.PreferredMetadataCountryCode = reader.ReadNormalizedString();
+ break;
case "lockedfields":
{
var val = reader.ReadElementContentAsString();
@@ -434,9 +361,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
case "tagline":
- item.Tagline = reader.ReadElementContentAsString();
+ item.Tagline = reader.ReadNormalizedString();
break;
-
case "country":
{
var val = reader.ReadElementContentAsString();
@@ -453,94 +379,45 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
case "mpaa":
- {
- var rating = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(rating))
- {
- item.OfficialRating = rating;
- }
-
- break;
- }
-
+ item.OfficialRating = reader.ReadNormalizedString();
+ break;
case "customrating":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- item.CustomRating = val;
- }
-
- break;
- }
-
+ item.CustomRating = reader.ReadNormalizedString();
+ break;
case "runtime":
+ var runtimeText = reader.ReadElementContentAsString();
+ if (int.TryParse(runtimeText.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
{
- var text = reader.ReadElementContentAsString();
-
- if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
- {
- item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
- }
-
- break;
+ item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
}
+ break;
case "aspectratio":
+ var aspectRatio = reader.ReadNormalizedString();
+ if (!string.IsNullOrEmpty(aspectRatio) && item is IHasAspectRatio hasAspectRatio)
{
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val)
- && item is IHasAspectRatio hasAspectRatio)
- {
- hasAspectRatio.AspectRatio = val;
- }
-
- break;
+ hasAspectRatio.AspectRatio = aspectRatio;
}
+ break;
case "lockdata":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- item.IsLocked = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
- }
-
- break;
- }
-
+ item.IsLocked = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
+ break;
case "studio":
+ var studio = reader.ReadNormalizedString();
+ if (!string.IsNullOrEmpty(studio))
{
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- item.AddStudio(val);
- }
-
- break;
+ item.AddStudio(studio);
}
+ break;
case "director":
+ foreach (var director in reader.GetPersonArray(PersonKind.Director))
{
- var val = reader.ReadElementContentAsString();
- foreach (var p in SplitNames(val).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Director }))
- {
- if (string.IsNullOrWhiteSpace(p.Name))
- {
- continue;
- }
-
- itemResult.AddPerson(p);
- }
-
- break;
+ itemResult.AddPerson(director);
}
+ break;
case "credits":
{
var val = reader.ReadElementContentAsString();
@@ -565,141 +442,76 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
case "writer":
+ foreach (var writer in reader.GetPersonArray(PersonKind.Writer))
{
- var val = reader.ReadElementContentAsString();
- foreach (var p in SplitNames(val).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Writer }))
- {
- if (string.IsNullOrWhiteSpace(p.Name))
- {
- continue;
- }
-
- itemResult.AddPerson(p);
- }
-
- break;
+ itemResult.AddPerson(writer);
}
+ break;
case "actor":
+ var person = reader.GetPersonFromXmlNode();
+ if (person is not null)
{
- if (!reader.IsEmptyElement)
- {
- using (var subtree = reader.ReadSubtree())
- {
- var person = GetPersonFromXmlNode(subtree);
-
- if (!string.IsNullOrWhiteSpace(person.Name))
- {
- itemResult.AddPerson(person);
- }
- }
- }
- else
- {
- reader.Read();
- }
-
- break;
+ itemResult.AddPerson(person);
}
+ break;
case "trailer":
+ var trailer = reader.ReadNormalizedString();
+ if (!string.IsNullOrEmpty(trailer))
{
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- val = val.Replace("plugin://plugin.video.youtube/?action=play_video&videoid=", BaseNfoSaver.YouTubeWatchUrl, StringComparison.OrdinalIgnoreCase);
-
- item.AddTrailerUrl(val);
- }
-
- break;
+ item.AddTrailerUrl(trailer.Replace(
+ "plugin://plugin.video.youtube/?action=play_video&videoid=",
+ BaseNfoSaver.YouTubeWatchUrl,
+ StringComparison.OrdinalIgnoreCase));
}
+ break;
case "displayorder":
+ var displayOrder = reader.ReadNormalizedString();
+ if (!string.IsNullOrEmpty(displayOrder) && item is IHasDisplayOrder hasDisplayOrder)
{
- var val = reader.ReadElementContentAsString();
-
- if (item is IHasDisplayOrder hasDisplayOrder && !string.IsNullOrWhiteSpace(val))
- {
- hasDisplayOrder.DisplayOrder = val;
- }
-
- break;
+ hasDisplayOrder.DisplayOrder = displayOrder;
}
+ break;
case "year":
+ if (reader.TryReadInt(out var productionYear) && productionYear > 1850)
{
- var val = reader.ReadElementContentAsString();
-
- if (int.TryParse(val, out var productionYear) && productionYear > 1850)
- {
- item.ProductionYear = productionYear;
- }
-
- break;
+ item.ProductionYear = productionYear;
}
+ break;
case "rating":
+ var rating = reader.ReadElementContentAsString().Replace(',', '.');
+ // All external meta is saving this as '.' for decimal I believe...but just to be sure
+ if (float.TryParse(rating, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var communityRating))
{
- var rating = reader.ReadElementContentAsString();
-
- // All external meta is saving this as '.' for decimal I believe...but just to be sure
- if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val))
- {
- item.CommunityRating = val;
- }
-
- break;
+ item.CommunityRating = communityRating;
}
+ break;
case "ratings":
- {
- if (!reader.IsEmptyElement)
- {
- using var subtree = reader.ReadSubtree();
- FetchFromRatingsNode(subtree, item);
- }
- else
- {
- reader.Read();
- }
-
- break;
- }
-
+ FetchFromRatingsNode(reader, item);
+ break;
case "aired":
case "formed":
case "premiered":
case "releasedate":
+ if (reader.TryReadDateTimeExact(nfoConfiguration.ReleaseDateFormat, out var releaseDate))
{
- var formatString = nfoConfiguration.ReleaseDateFormat;
-
- var val = reader.ReadElementContentAsString();
-
- if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
- {
- item.PremiereDate = date;
- item.ProductionYear = date.Year;
- }
-
- break;
+ item.PremiereDate = releaseDate;
+ item.ProductionYear = releaseDate.Year;
}
+ break;
case "enddate":
+ if (reader.TryReadDateTimeExact(nfoConfiguration.ReleaseDateFormat, out var endDate))
{
- var formatString = nfoConfiguration.ReleaseDateFormat;
-
- var val = reader.ReadElementContentAsString();
-
- if (DateTime.TryParseExact(val, formatString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var date) && date.Year > 1850)
- {
- item.EndDate = date;
- }
-
- break;
+ item.EndDate = endDate;
}
+ break;
case "genre":
{
var val = reader.ReadElementContentAsString();
@@ -721,57 +533,34 @@ namespace MediaBrowser.XbmcMetadata.Parsers
case "style":
case "tag":
+ var tag = reader.ReadNormalizedString();
+ if (!string.IsNullOrEmpty(tag))
{
- var val = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(val))
- {
- item.AddTag(val);
- }
-
- break;
+ item.AddTag(tag);
}
+ break;
case "fileinfo":
- {
- if (!reader.IsEmptyElement)
- {
- using (var subtree = reader.ReadSubtree())
- {
- FetchFromFileInfoNode(subtree, item);
- }
- }
- else
- {
- reader.Read();
- }
-
- break;
- }
-
+ FetchFromFileInfoNode(reader, item);
+ break;
case "uniqueid":
+ if (reader.IsEmptyElement)
{
- if (reader.IsEmptyElement)
- {
- reader.Read();
- break;
- }
-
- var provider = reader.GetAttribute("type");
- var id = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(provider) && !string.IsNullOrWhiteSpace(id))
- {
- item.SetProviderId(provider, id);
- }
-
+ reader.Read();
break;
}
- case "thumb":
+ var provider = reader.GetAttribute("type");
+ var providerId = reader.ReadElementContentAsString();
+ if (!string.IsNullOrWhiteSpace(provider) && !string.IsNullOrWhiteSpace(providerId))
{
- FetchThumbNode(reader, itemResult, "thumb");
- break;
+ item.SetProviderId(provider, providerId);
}
+ break;
+ case "thumb":
+ FetchThumbNode(reader, itemResult, "thumb");
+ break;
case "fanart":
{
if (reader.IsEmptyElement)
@@ -876,242 +665,188 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
}
- private void FetchFromFileInfoNode(XmlReader reader, T item)
+ private void FetchFromFileInfoNode(XmlReader parentReader, T item)
{
+ if (parentReader.IsEmptyElement)
+ {
+ parentReader.Read();
+ return;
+ }
+
+ using var reader = parentReader.ReadSubtree();
reader.MoveToContent();
reader.Read();
// Loop through each element
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
{
- if (reader.NodeType == XmlNodeType.Element)
+ if (reader.NodeType != XmlNodeType.Element)
{
- switch (reader.Name)
- {
- case "streamdetails":
- {
- if (reader.IsEmptyElement)
- {
- reader.Read();
- continue;
- }
-
- using (var subtree = reader.ReadSubtree())
- {
- FetchFromStreamDetailsNode(subtree, item);
- }
-
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
+ reader.Read();
+ continue;
}
- else
+
+ switch (reader.Name)
{
- reader.Read();
+ case "streamdetails":
+ FetchFromStreamDetailsNode(reader, item);
+ break;
+ default:
+ reader.Skip();
+ break;
}
}
}
- private void FetchFromStreamDetailsNode(XmlReader reader, T item)
+ private void FetchFromStreamDetailsNode(XmlReader parentReader, T item)
{
+ if (parentReader.IsEmptyElement)
+ {
+ parentReader.Read();
+ return;
+ }
+
+ using var reader = parentReader.ReadSubtree();
reader.MoveToContent();
reader.Read();
// Loop through each element
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
{
- if (reader.NodeType == XmlNodeType.Element)
+ if (reader.NodeType != XmlNodeType.Element)
{
- switch (reader.Name)
- {
- case "video":
- {
- if (reader.IsEmptyElement)
- {
- reader.Read();
- continue;
- }
-
- using (var subtree = reader.ReadSubtree())
- {
- FetchFromVideoNode(subtree, item);
- }
-
- break;
- }
-
- case "subtitle":
- {
- if (reader.IsEmptyElement)
- {
- reader.Read();
- continue;
- }
-
- using (var subtree = reader.ReadSubtree())
- {
- FetchFromSubtitleNode(subtree, item);
- }
-
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
+ reader.Read();
+ continue;
}
- else
+
+ switch (reader.Name)
{
- reader.Read();
+ case "video":
+ FetchFromVideoNode(reader, item);
+ break;
+ case "subtitle":
+ FetchFromSubtitleNode(reader, item);
+ break;
+ default:
+ reader.Skip();
+ break;
}
}
}
- private void FetchFromVideoNode(XmlReader reader, T item)
+ private void FetchFromVideoNode(XmlReader parentReader, T item)
{
+ if (parentReader.IsEmptyElement)
+ {
+ parentReader.Read();
+ return;
+ }
+
+ using var reader = parentReader.ReadSubtree();
reader.MoveToContent();
reader.Read();
// Loop through each element
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
{
- if (reader.NodeType == XmlNodeType.Element)
+ if (reader.NodeType != XmlNodeType.Element || item is not Video video)
{
- switch (reader.Name)
- {
- case "format3d":
- {
- var val = reader.ReadElementContentAsString();
-
- var video = item as Video;
-
- if (video is not null)
- {
- if (string.Equals("HSBS", val, StringComparison.OrdinalIgnoreCase))
- {
- video.Video3DFormat = Video3DFormat.HalfSideBySide;
- }
- else if (string.Equals("HTAB", val, StringComparison.OrdinalIgnoreCase))
- {
- video.Video3DFormat = Video3DFormat.HalfTopAndBottom;
- }
- else if (string.Equals("FTAB", val, StringComparison.OrdinalIgnoreCase))
- {
- video.Video3DFormat = Video3DFormat.FullTopAndBottom;
- }
- else if (string.Equals("FSBS", val, StringComparison.OrdinalIgnoreCase))
- {
- video.Video3DFormat = Video3DFormat.FullSideBySide;
- }
- else if (string.Equals("MVC", val, StringComparison.OrdinalIgnoreCase))
- {
- video.Video3DFormat = Video3DFormat.MVC;
- }
- }
-
- break;
- }
-
- case "aspect":
- {
- var val = reader.ReadElementContentAsString();
-
- if (item is Video video)
- {
- video.AspectRatio = val;
- }
-
- break;
- }
-
- case "width":
- {
- var val = reader.ReadElementContentAsInt();
-
- if (item is Video video)
- {
- video.Width = val;
- }
-
- break;
- }
-
- case "height":
- {
- var val = reader.ReadElementContentAsInt();
-
- if (item is Video video)
- {
- video.Height = val;
- }
-
- break;
- }
-
- case "durationinseconds":
- {
- var val = reader.ReadElementContentAsInt();
-
- if (item is Video video)
- {
- video.RunTimeTicks = new TimeSpan(0, 0, val).Ticks;
- }
-
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
+ reader.Read();
+ continue;
}
- else
+
+ switch (reader.Name)
{
- reader.Read();
+ case "format3d":
+ var format = reader.ReadElementContentAsString();
+ if (string.Equals("HSBS", format, StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.HalfSideBySide;
+ }
+ else if (string.Equals("HTAB", format, StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.HalfTopAndBottom;
+ }
+ else if (string.Equals("FTAB", format, StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.FullTopAndBottom;
+ }
+ else if (string.Equals("FSBS", format, StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.FullSideBySide;
+ }
+ else if (string.Equals("MVC", format, StringComparison.OrdinalIgnoreCase))
+ {
+ video.Video3DFormat = Video3DFormat.MVC;
+ }
+
+ break;
+ case "aspect":
+ video.AspectRatio = reader.ReadNormalizedString();
+ break;
+ case "width":
+ video.Width = reader.ReadElementContentAsInt();
+ break;
+ case "height":
+ video.Height = reader.ReadElementContentAsInt();
+ break;
+ case "durationinseconds":
+ video.RunTimeTicks = new TimeSpan(0, 0, reader.ReadElementContentAsInt()).Ticks;
+ break;
+ default:
+ reader.Skip();
+ break;
}
}
}
- private void FetchFromSubtitleNode(XmlReader reader, T item)
+ private void FetchFromSubtitleNode(XmlReader parentReader, T item)
{
+ if (parentReader.IsEmptyElement)
+ {
+ parentReader.Read();
+ return;
+ }
+
+ using var reader = parentReader.ReadSubtree();
reader.MoveToContent();
reader.Read();
// Loop through each element
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
{
- if (reader.NodeType == XmlNodeType.Element)
+ if (reader.NodeType != XmlNodeType.Element)
{
- switch (reader.Name)
- {
- case "language":
- _ = reader.ReadElementContentAsString();
- if (item is Video video)
- {
- video.HasSubtitles = true;
- }
-
- break;
-
- default:
- reader.Skip();
- break;
- }
+ reader.Read();
+ continue;
}
- else
+
+ switch (reader.Name)
{
- reader.Read();
+ case "language":
+ _ = reader.ReadElementContentAsString();
+ if (item is Video video)
+ {
+ video.HasSubtitles = true;
+ }
+
+ break;
+ default:
+ reader.Skip();
+ break;
}
}
}
- private void FetchFromRatingsNode(XmlReader reader, T item)
+ private void FetchFromRatingsNode(XmlReader parentReader, T item)
{
+ if (parentReader.IsEmptyElement)
+ {
+ parentReader.Read();
+ return;
+ }
+
+ using var reader = parentReader.ReadSubtree();
reader.MoveToContent();
reader.Read();
@@ -1196,102 +931,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers
}
}
- /// <summary>
- /// Gets the persons from a XML node.
- /// </summary>
- /// <param name="reader">The <see cref="XmlReader"/>.</param>
- /// <returns>IEnumerable{PersonInfo}.</returns>
- private PersonInfo GetPersonFromXmlNode(XmlReader reader)
- {
- 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;
-
- 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 "role":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- role = val;
- }
-
- break;
- }
-
- case "type":
- {
- var val = reader.ReadElementContentAsString();
- if (!Enum.TryParse(val, true, out type))
- {
- type = PersonKind.Actor;
- }
-
- break;
- }
-
- case "order":
- case "sortorder":
- {
- var val = reader.ReadElementContentAsString();
-
- if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal))
- {
- sortOrder = intVal;
- }
-
- break;
- }
-
- case "thumb":
- {
- var val = reader.ReadElementContentAsString();
-
- if (!string.IsNullOrWhiteSpace(val))
- {
- imageUrl = val;
- }
-
- break;
- }
-
- default:
- reader.Skip();
- break;
- }
- }
- else
- {
- reader.Read();
- }
- }
-
- return new PersonInfo
- {
- Name = name.Trim(),
- Role = role,
- Type = type,
- SortOrder = sortOrder,
- ImageUrl = imageUrl
- };
- }
-
internal XmlReaderSettings GetXmlReaderSettings()
=> new XmlReaderSettings()
{
@@ -1302,24 +941,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers
};
/// <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>() : value.Split(separator, StringSplitOptions.RemoveEmptyEntries);
- }
-
- /// <summary>
/// Parses the <see cref="ImageType"/> from the NFO aspect property.
/// </summary>
/// <param name="aspect">The NFO aspect property.</param>
diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
index d2f349ad7..044efb51e 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
@@ -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;
diff --git a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
index ecfed6873..16ea5e3ea 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
@@ -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;
diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs
index 51d5f932b..e13f0d997 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/SeasonNfoParser.cs
@@ -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;
diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
index f22b861eb..dbcfe7997 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
@@ -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();
diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
index 82e1dc860..8fa22fad9 100644
--- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs
@@ -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");
}
}
diff --git a/debian/jellyfin.init b/debian/jellyfin.init
index 7f5642bac..784536d87 100644
--- a/debian/jellyfin.init
+++ b/debian/jellyfin.init
@@ -1,3 +1,4 @@
+#!/bin/sh
### BEGIN INIT INFO
# Provides: Jellyfin Media Server
# Required-Start: $local_fs $network
diff --git a/fedora/jellyfin.service b/fedora/jellyfin.service
index 1b3f8032c..01accdc0c 100644
--- a/fedora/jellyfin.service
+++ b/fedora/jellyfin.service
@@ -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
diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec
index d9d3d4869..e78368906 100644
--- a/fedora/jellyfin.spec
+++ b/fedora/jellyfin.spec
@@ -75,6 +75,7 @@ dotnet publish --configuration Release --self-contained --runtime %{dotnet_runti
%{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir}
%{__cp} -r Jellyfin.Server/bin/Release/net7.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin
%{__install} -D %{SOURCE10} %{buildroot}%{_bindir}/jellyfin
+sed -i -e 's|/usr/lib64|%{_libdir}|g' %{buildroot}%{_bindir}/jellyfin
# Jellyfin config
%{__install} -D Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json
diff --git a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
index 126c0503e..03f90da8e 100644
--- a/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
+++ b/src/Jellyfin.Drawing.Skia/SkiaEncoder.cs
@@ -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>
diff --git a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
index 6dff7aa9b..4aff26c16 100644
--- a/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
+++ b/src/Jellyfin.Drawing.Skia/StripCollageBuilder.cs
@@ -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;
}
diff --git a/src/Jellyfin.Drawing/ImageProcessor.cs b/src/Jellyfin.Drawing/ImageProcessor.cs
index 4f16e294b..65a8f4e83 100644
--- a/src/Jellyfin.Drawing/ImageProcessor.cs
+++ b/src/Jellyfin.Drawing/ImageProcessor.cs
@@ -108,22 +108,10 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
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)
{
ItemImageInfo originalImage = options.Image;
@@ -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));
}
diff --git a/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs b/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs
index a5bdb42d8..198ad5a27 100644
--- a/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs
+++ b/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs
@@ -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());
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs
index 09eb22328..07061cfc7 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs
@@ -31,6 +31,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("/media/music/Foo B.A.R./epic.flac", false)]
[InlineData("/media/music/Foo B.A.R", false)]
[InlineData("/media/music/Foo B.A.R.", false)]
+ [InlineData("/movies/.zfs/snapshot/AutoM-2023-09", true)]
public void PathIgnored(string path, bool expected)
{
Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path));
diff --git a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
index 3dc62afaf..5ddbd30d1 100644
--- a/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/AuthHelper.cs
@@ -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);
diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs
new file mode 100644
index 000000000..38c64547c
--- /dev/null
+++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/PersonsControllerTests.cs
@@ -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);
+ }
+}
diff --git a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
index 55bc43455..1c87d11f1 100644
--- a/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
+++ b/tests/Jellyfin.Server.Integration.Tests/JellyfinApplicationFactory.cs
@@ -2,7 +2,6 @@ using System;
using System.Collections.Concurrent;
using System.Globalization;
using System.IO;
-using System.Threading;
using Emby.Server.Implementations;
using Jellyfin.Server.Extensions;
using Jellyfin.Server.Helpers;
@@ -105,7 +104,7 @@ namespace Jellyfin.Server.Integration.Tests
var appHost = (TestAppHost)testServer.Services.GetRequiredService<IApplicationHost>();
appHost.ServiceProvider = testServer.Services;
appHost.InitializeServices().GetAwaiter().GetResult();
- appHost.RunStartupTasksAsync(CancellationToken.None).GetAwaiter().GetResult();
+ appHost.RunStartupTasksAsync().GetAwaiter().GetResult();
return testServer;
}
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
index f63bc0e1b..c0d06116b 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
@@ -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);
}