mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-05-24 02:02:29 -04:00
Merge branch 'master' into chromecast-config
# Conflicts: # Emby.Server.Implementations/ApplicationHost.cs
This commit is contained in:
commit
6bd6fb6e0a
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@ -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
|
||||
|
13
.github/workflows/repo-stale.yaml
vendored
13
.github/workflows/repo-stale.yaml
vendored
@ -2,16 +2,17 @@ name: Stale Check
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
- cron: '30 */12 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: write
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
issues:
|
||||
name: Check issues
|
||||
name: Check for stale issues
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ contains(github.repository, 'jellyfin/') }}
|
||||
steps:
|
||||
@ -26,11 +27,11 @@ jobs:
|
||||
exempt-issue-labels: regression,security,roadmap,future,feature,enhancement,confirmed
|
||||
stale-issue-label: stale
|
||||
stale-issue-message: |-
|
||||
This issue has gone 120 days without comment. To avoid abandoned issues, it will be closed in 21 days if there are no new comments.
|
||||
This issue has gone 120 days without an update and will be closed within 21 days if there is no new activity. To prevent this issue from being closed, please confirm the issue has not already been fixed by providing updated examples or logs.
|
||||
|
||||
If you're the original submitter of this issue, please comment confirming if this issue still affects you in the latest release or master branch, or close the issue if it has been fixed. If you're another user also affected by this bug, please comment confirming so. Either action will remove the stale label.
|
||||
|
||||
This bot exists to prevent issues from becoming stale and forgotten. Jellyfin is always moving forward, and bugs are often fixed as side effects of other changes. We therefore ask that bug report authors remain vigilant about their issues to ensure they are closed if fixed, or re-confirmed - perhaps with fresh logs or reproduction examples - regularly. If you have any questions you can reach us on [Matrix or Social Media](https://docs.jellyfin.org/general/getting-help.html).
|
||||
If you have any questions you can use one of several ways to [contact us](https://jellyfin.org/contact).
|
||||
close-issue-message: |-
|
||||
This issue was closed due to inactivity.
|
||||
|
||||
prs-conflicts:
|
||||
name: Check PRs with merge conflicts
|
||||
|
@ -238,3 +238,4 @@
|
||||
- [Jakob Kukla](https://github.com/jakobkukla)
|
||||
- [Utku Özdemir](https://github.com/utkuozdemir)
|
||||
- [JPUC1143](https://github.com/Jpuc1143/)
|
||||
- [0x25CBFC4F](https://github.com/0x25CBFC4F)
|
||||
|
@ -19,12 +19,13 @@
|
||||
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
|
||||
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.2" />
|
||||
<PackageVersion Include="FsCheck.Xunit" Version="2.16.6" />
|
||||
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" />
|
||||
<PackageVersion Include="IDisposableAnalyzers" Version="4.0.7" />
|
||||
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
|
||||
<PackageVersion Include="libse" Version="3.6.13" />
|
||||
<PackageVersion Include="LrcParser" Version="2023.524.0" />
|
||||
<PackageVersion Include="MetaBrainz.MusicBrainz" Version="5.0.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.11" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="7.0.12" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.HttpOverrides" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.11" />
|
||||
<PackageVersion Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" />
|
||||
@ -65,14 +66,13 @@
|
||||
<PackageVersion Include="Serilog.Sinks.Async" Version="1.5.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="4.1.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.0.2" />
|
||||
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.0" />
|
||||
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
|
||||
<PackageVersion Include="SharpFuzz" Version="2.1.1" />
|
||||
<PackageVersion Include="SkiaSharp" Version="2.88.5" />
|
||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.5" />
|
||||
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.5" />
|
||||
<PackageVersion Include="SkiaSharp.Svg" Version="1.60.0" />
|
||||
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="2.88.6" />
|
||||
<PackageVersion Include="HarfBuzzSharp.NativeAssets.Linux" Version="7.3.0" />
|
||||
<PackageVersion Include="SkiaSharp" Version="2.88.6" />
|
||||
<PackageVersion Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" />
|
||||
<PackageVersion Include="StyleCop.Analyzers" Version="1.2.0-beta.507" />
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.ReDoc" Version="6.5.0" />
|
||||
|
@ -4,7 +4,7 @@
|
||||
# https://github.com/multiarch/qemu-user-static#binfmt_misc-register
|
||||
ARG DOTNET_VERSION=7.0
|
||||
|
||||
FROM node:lts-alpine as web-builder
|
||||
FROM node:20-alpine as web-builder
|
||||
ARG JELLYFIN_WEB_VERSION=master
|
||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
|
||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
|
@ -5,7 +5,7 @@
|
||||
ARG DOTNET_VERSION=7.0
|
||||
|
||||
|
||||
FROM node:lts-alpine as web-builder
|
||||
FROM node:20-alpine as web-builder
|
||||
ARG JELLYFIN_WEB_VERSION=master
|
||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
|
||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
|
@ -5,7 +5,7 @@
|
||||
ARG DOTNET_VERSION=7.0
|
||||
|
||||
|
||||
FROM node:lts-alpine as web-builder
|
||||
FROM node:20-alpine as web-builder
|
||||
ARG JELLYFIN_WEB_VERSION=master
|
||||
RUN apk add curl git zlib zlib-dev autoconf g++ make libpng-dev gifsicle alpine-sdk automake libtool make gcc musl-dev nasm python3 \
|
||||
&& curl -L https://github.com/jellyfin/jellyfin-web/archive/${JELLYFIN_WEB_VERSION}.tar.gz | tar zxf - \
|
||||
|
@ -228,7 +228,7 @@ namespace Emby.Dlna
|
||||
try
|
||||
{
|
||||
return _fileSystem.GetFilePaths(path)
|
||||
.Where(i => string.Equals(Path.GetExtension(i), ".xml", StringComparison.OrdinalIgnoreCase))
|
||||
.Where(i => Path.GetExtension(i.AsSpan()).Equals(".xml", StringComparison.OrdinalIgnoreCase))
|
||||
.Select(i => ParseProfileFile(i, type))
|
||||
.Where(i => i is not null)
|
||||
.ToList()!; // We just filtered out all the nulls
|
||||
|
@ -43,7 +43,7 @@ namespace Emby.Naming.ExternalFiles
|
||||
return null;
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path);
|
||||
var extension = Path.GetExtension(path.AsSpan());
|
||||
if (!(_type == DlnaProfileType.Subtitle && _namingOptions.SubtitleFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
&& !(_type == DlnaProfileType.Audio && _namingOptions.AudioFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
|
@ -26,19 +26,18 @@ namespace Emby.Naming.Video
|
||||
return false;
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(path);
|
||||
var extension = Path.GetExtension(path.AsSpan());
|
||||
|
||||
if (!options.StubFileExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
path = Path.GetFileNameWithoutExtension(path);
|
||||
var token = Path.GetExtension(path).TrimStart('.');
|
||||
var token = Path.GetExtension(Path.GetFileNameWithoutExtension(path.AsSpan())).TrimStart('.');
|
||||
|
||||
foreach (var rule in options.StubTypes)
|
||||
{
|
||||
if (string.Equals(rule.Token, token, StringComparison.OrdinalIgnoreCase))
|
||||
if (token.Equals(rule.Token, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
stubType = rule.StubType;
|
||||
return true;
|
||||
|
@ -61,7 +61,7 @@ namespace Emby.Photos
|
||||
item.SetImagePath(ImageType.Primary, item.Path);
|
||||
|
||||
// Examples: https://github.com/mono/taglib-sharp/blob/a5f6949a53d09ce63ee7495580d6802921a21f14/tests/fixtures/TagLib.Tests.Images/NullOrientationTest.cs
|
||||
if (_includeExtensions.Contains(Path.GetExtension(item.Path), StringComparison.OrdinalIgnoreCase))
|
||||
if (_includeExtensions.Contains(Path.GetExtension(item.Path.AsSpan()), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
|
@ -8,7 +8,6 @@ using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Events;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@ -19,14 +18,8 @@ namespace Emby.Server.Implementations.AppBase
|
||||
/// </summary>
|
||||
public abstract class BaseConfigurationManager : IConfigurationManager
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
private readonly ConcurrentDictionary<string, object> _configurations = new ConcurrentDictionary<string, object>();
|
||||
|
||||
/// <summary>
|
||||
/// The _configuration sync lock.
|
||||
/// </summary>
|
||||
private readonly object _configurationSyncLock = new object();
|
||||
private readonly ConcurrentDictionary<string, object> _configurations = new();
|
||||
private readonly object _configurationSyncLock = new();
|
||||
|
||||
private ConfigurationStore[] _configurationStores = Array.Empty<ConfigurationStore>();
|
||||
private IConfigurationFactory[] _configurationFactories = Array.Empty<IConfigurationFactory>();
|
||||
@ -42,12 +35,13 @@ namespace Emby.Server.Implementations.AppBase
|
||||
/// <param name="applicationPaths">The application paths.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
/// <param name="xmlSerializer">The XML serializer.</param>
|
||||
/// <param name="fileSystem">The file system.</param>
|
||||
protected BaseConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem)
|
||||
protected BaseConfigurationManager(
|
||||
IApplicationPaths applicationPaths,
|
||||
ILoggerFactory loggerFactory,
|
||||
IXmlSerializer xmlSerializer)
|
||||
{
|
||||
CommonApplicationPaths = applicationPaths;
|
||||
XmlSerializer = xmlSerializer;
|
||||
_fileSystem = fileSystem;
|
||||
Logger = loggerFactory.CreateLogger<BaseConfigurationManager>();
|
||||
|
||||
UpdateCachePath();
|
||||
@ -272,7 +266,7 @@ namespace Emby.Server.Implementations.AppBase
|
||||
{
|
||||
var file = Path.Combine(path, Guid.NewGuid().ToString());
|
||||
File.WriteAllText(file, string.Empty);
|
||||
_fileSystem.DeleteFile(file);
|
||||
File.Delete(file);
|
||||
}
|
||||
|
||||
private string GetConfigurationFile(string key)
|
||||
|
@ -12,7 +12,6 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using System.Reflection;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Dlna;
|
||||
using Emby.Dlna.Main;
|
||||
@ -112,7 +111,7 @@ namespace Emby.Server.Implementations
|
||||
/// <summary>
|
||||
/// Class CompositionRoot.
|
||||
/// </summary>
|
||||
public abstract class ApplicationHost : IServerApplicationHost, IAsyncDisposable, IDisposable
|
||||
public abstract class ApplicationHost : IServerApplicationHost, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The disposable parts.
|
||||
@ -120,14 +119,12 @@ namespace Emby.Server.Implementations
|
||||
private readonly ConcurrentDictionary<IDisposable, byte> _disposableParts = new();
|
||||
private readonly DeviceId _deviceId;
|
||||
|
||||
private readonly IFileSystem _fileSystemManager;
|
||||
private readonly IConfiguration _startupConfig;
|
||||
private readonly IXmlSerializer _xmlSerializer;
|
||||
private readonly IStartupOptions _startupOptions;
|
||||
private readonly IPluginManager _pluginManager;
|
||||
|
||||
private List<Type> _creatingInstances;
|
||||
private ISessionManager _sessionManager;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets all concrete types.
|
||||
@ -135,7 +132,7 @@ namespace Emby.Server.Implementations
|
||||
/// <value>All concrete types.</value>
|
||||
private Type[] _allConcreteTypes;
|
||||
|
||||
private bool _disposed = false;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ApplicationHost"/> class.
|
||||
@ -154,10 +151,8 @@ namespace Emby.Server.Implementations
|
||||
LoggerFactory = loggerFactory;
|
||||
_startupOptions = options;
|
||||
_startupConfig = startupConfig;
|
||||
_fileSystemManager = new ManagedFileSystem(LoggerFactory.CreateLogger<ManagedFileSystem>(), applicationPaths);
|
||||
|
||||
Logger = LoggerFactory.CreateLogger<ApplicationHost>();
|
||||
_fileSystemManager.AddShortcutHandler(new MbLinkShortcutHandler());
|
||||
_deviceId = new DeviceId(ApplicationPaths, LoggerFactory);
|
||||
|
||||
ApplicationVersion = typeof(ApplicationHost).Assembly.GetName().Version;
|
||||
@ -165,13 +160,15 @@ namespace Emby.Server.Implementations
|
||||
ApplicationUserAgent = Name.Replace(' ', '-') + "/" + ApplicationVersionString;
|
||||
|
||||
_xmlSerializer = new MyXmlSerializer();
|
||||
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer, _fileSystemManager);
|
||||
ConfigurationManager = new ServerConfigurationManager(ApplicationPaths, LoggerFactory, _xmlSerializer);
|
||||
_pluginManager = new PluginManager(
|
||||
LoggerFactory.CreateLogger<PluginManager>(),
|
||||
this,
|
||||
ConfigurationManager.Configuration,
|
||||
ApplicationPaths.PluginsPath,
|
||||
ApplicationVersion);
|
||||
|
||||
_disposableParts.TryAdd((PluginManager)_pluginManager, byte.MinValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -186,23 +183,16 @@ namespace Emby.Server.Implementations
|
||||
|
||||
public bool CoreStartupHasCompleted { get; private set; }
|
||||
|
||||
public virtual bool CanLaunchWebBrowser => Environment.UserInteractive
|
||||
&& !_startupOptions.IsService
|
||||
&& (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="INetworkManager"/> singleton instance.
|
||||
/// </summary>
|
||||
public INetworkManager NetManager { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance has changes that require the entire application to restart.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance has pending application restart; otherwise, <c>false</c>.</value>
|
||||
/// <inheritdoc />
|
||||
public bool HasPendingRestart { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsShuttingDown { get; private set; }
|
||||
public bool ShouldRestart { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the logger.
|
||||
@ -406,11 +396,9 @@ namespace Emby.Server.Implementations
|
||||
/// <summary>
|
||||
/// Runs the startup tasks.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns><see cref="Task" />.</returns>
|
||||
public async Task RunStartupTasksAsync(CancellationToken cancellationToken)
|
||||
public async Task RunStartupTasksAsync()
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
Logger.LogInformation("Running startup tasks");
|
||||
|
||||
Resolve<ITaskManager>().AddTasks(GetExports<IScheduledTask>(false));
|
||||
@ -424,8 +412,6 @@ namespace Emby.Server.Implementations
|
||||
|
||||
var entryPoints = GetExports<IServerEntryPoint>();
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var stopWatch = new Stopwatch();
|
||||
stopWatch.Start();
|
||||
|
||||
@ -435,8 +421,6 @@ namespace Emby.Server.Implementations
|
||||
Logger.LogInformation("Core startup complete");
|
||||
CoreStartupHasCompleted = true;
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
stopWatch.Restart();
|
||||
|
||||
await Task.WhenAll(StartEntryPoints(entryPoints, false)).ConfigureAwait(false);
|
||||
@ -509,7 +493,11 @@ namespace Emby.Server.Implementations
|
||||
serviceCollection.AddSingleton(_pluginManager);
|
||||
serviceCollection.AddSingleton<IApplicationPaths>(ApplicationPaths);
|
||||
|
||||
serviceCollection.AddSingleton(_fileSystemManager);
|
||||
serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>();
|
||||
serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>();
|
||||
|
||||
serviceCollection.AddScoped<ISystemManager, SystemManager>();
|
||||
|
||||
serviceCollection.AddSingleton<TmdbClientManager>();
|
||||
|
||||
serviceCollection.AddSingleton(NetManager);
|
||||
@ -633,8 +621,6 @@ namespace Emby.Server.Implementations
|
||||
var localizationManager = (LocalizationManager)Resolve<ILocalizationManager>();
|
||||
await localizationManager.LoadAll().ConfigureAwait(false);
|
||||
|
||||
_sessionManager = Resolve<ISessionManager>();
|
||||
|
||||
SetStaticProperties();
|
||||
|
||||
FindParts();
|
||||
@ -685,7 +671,7 @@ namespace Emby.Server.Implementations
|
||||
BaseItem.ProviderManager = Resolve<IProviderManager>();
|
||||
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
|
||||
BaseItem.ItemRepository = Resolve<IItemRepository>();
|
||||
BaseItem.FileSystem = _fileSystemManager;
|
||||
BaseItem.FileSystem = Resolve<IFileSystem>();
|
||||
BaseItem.UserDataManager = Resolve<IUserDataManager>();
|
||||
BaseItem.ChannelManager = Resolve<IChannelManager>();
|
||||
Video.LiveTvManager = Resolve<ILiveTvManager>();
|
||||
@ -855,38 +841,6 @@ namespace Emby.Server.Implementations
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restarts this instance.
|
||||
/// </summary>
|
||||
public void Restart()
|
||||
{
|
||||
if (IsShuttingDown)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsShuttingDown = true;
|
||||
_pluginManager.UnloadAssemblies();
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await _sessionManager.SendServerRestartNotification(CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error sending server restart notification");
|
||||
}
|
||||
|
||||
Logger.LogInformation("Calling RestartInternal");
|
||||
|
||||
RestartInternal();
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract void RestartInternal();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the composable part assemblies.
|
||||
/// </summary>
|
||||
@ -942,50 +896,6 @@ namespace Emby.Server.Implementations
|
||||
|
||||
protected abstract IEnumerable<Assembly> GetAssembliesWithPartsInternal();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the system status.
|
||||
/// </summary>
|
||||
/// <param name="request">Where this request originated.</param>
|
||||
/// <returns>SystemInfo.</returns>
|
||||
public SystemInfo GetSystemInfo(HttpRequest request)
|
||||
{
|
||||
return new SystemInfo
|
||||
{
|
||||
HasPendingRestart = HasPendingRestart,
|
||||
IsShuttingDown = IsShuttingDown,
|
||||
Version = ApplicationVersionString,
|
||||
WebSocketPortNumber = HttpPort,
|
||||
CompletedInstallations = Resolve<IInstallationManager>().CompletedInstallations.ToArray(),
|
||||
Id = SystemId,
|
||||
ProgramDataPath = ApplicationPaths.ProgramDataPath,
|
||||
WebPath = ApplicationPaths.WebPath,
|
||||
LogPath = ApplicationPaths.LogDirectoryPath,
|
||||
ItemsByNamePath = ApplicationPaths.InternalMetadataPath,
|
||||
InternalMetadataPath = ApplicationPaths.InternalMetadataPath,
|
||||
CachePath = ApplicationPaths.CachePath,
|
||||
CanLaunchWebBrowser = CanLaunchWebBrowser,
|
||||
TranscodingTempPath = ConfigurationManager.GetTranscodePath(),
|
||||
ServerName = FriendlyName,
|
||||
LocalAddress = GetSmartApiUrl(request),
|
||||
SupportsLibraryMonitor = true,
|
||||
PackageName = _startupOptions.PackageName,
|
||||
CastReceiverApplications = ConfigurationManager.Configuration.CastReceiverApplications
|
||||
};
|
||||
}
|
||||
|
||||
public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
|
||||
{
|
||||
return new PublicSystemInfo
|
||||
{
|
||||
Version = ApplicationVersionString,
|
||||
ProductName = ApplicationProductName,
|
||||
Id = SystemId,
|
||||
ServerName = FriendlyName,
|
||||
LocalAddress = GetSmartApiUrl(request),
|
||||
StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public string GetSmartApiUrl(IPAddress remoteAddr)
|
||||
{
|
||||
@ -1066,30 +976,6 @@ namespace Emby.Server.Implementations
|
||||
}.ToString().TrimEnd('/');
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Shutdown()
|
||||
{
|
||||
if (IsShuttingDown)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsShuttingDown = true;
|
||||
|
||||
try
|
||||
{
|
||||
await _sessionManager.SendServerShutdownNotification(CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error sending server shutdown notification");
|
||||
}
|
||||
|
||||
ShutdownInternal();
|
||||
}
|
||||
|
||||
protected abstract void ShutdownInternal();
|
||||
|
||||
public IEnumerable<Assembly> GetApiPluginAssemblies()
|
||||
{
|
||||
var assemblies = _allConcreteTypes
|
||||
@ -1153,52 +1039,5 @@ namespace Emby.Server.Implementations
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await DisposeAsyncCore().ConfigureAwait(false);
|
||||
Dispose(false);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to perform asynchronous cleanup of managed resources or for cascading calls to <see cref="DisposeAsync"/>.
|
||||
/// </summary>
|
||||
/// <returns>A ValueTask.</returns>
|
||||
protected virtual async ValueTask DisposeAsyncCore()
|
||||
{
|
||||
var type = GetType();
|
||||
|
||||
Logger.LogInformation("Disposing {Type}", type.Name);
|
||||
|
||||
foreach (var (part, _) in _disposableParts)
|
||||
{
|
||||
var partType = part.GetType();
|
||||
if (partType == type)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Logger.LogInformation("Disposing {Type}", partType.Name);
|
||||
|
||||
try
|
||||
{
|
||||
part.Dispose();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error disposing {Type}", partType.Name);
|
||||
}
|
||||
}
|
||||
|
||||
if (_sessionManager is not null)
|
||||
{
|
||||
// used for closing websockets
|
||||
foreach (var session in _sessionManager.Sessions)
|
||||
{
|
||||
await session.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,6 @@ using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@ -22,11 +21,13 @@ namespace Emby.Server.Implementations.Configuration
|
||||
/// Initializes a new instance of the <see cref="ServerConfigurationManager" /> class.
|
||||
/// </summary>
|
||||
/// <param name="applicationPaths">The application paths.</param>
|
||||
/// <param name="loggerFactory">The paramref name="loggerFactory" factory.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
/// <param name="xmlSerializer">The XML serializer.</param>
|
||||
/// <param name="fileSystem">The file system.</param>
|
||||
public ServerConfigurationManager(IApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IXmlSerializer xmlSerializer, IFileSystem fileSystem)
|
||||
: base(applicationPaths, loggerFactory, xmlSerializer, fileSystem)
|
||||
public ServerConfigurationManager(
|
||||
IApplicationPaths applicationPaths,
|
||||
ILoggerFactory loggerFactory,
|
||||
IXmlSerializer xmlSerializer)
|
||||
: base(applicationPaths, loggerFactory, xmlSerializer)
|
||||
{
|
||||
UpdateMetadataPath();
|
||||
}
|
||||
|
@ -15,10 +15,6 @@ namespace Emby.Server.Implementations.IO
|
||||
/// </summary>
|
||||
public class ManagedFileSystem : IFileSystem
|
||||
{
|
||||
private readonly ILogger<ManagedFileSystem> _logger;
|
||||
|
||||
private readonly List<IShortcutHandler> _shortcutHandlers = new List<IShortcutHandler>();
|
||||
private readonly string _tempPath;
|
||||
private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows();
|
||||
private static readonly char[] _invalidPathCharacters =
|
||||
{
|
||||
@ -29,23 +25,24 @@ namespace Emby.Server.Implementations.IO
|
||||
(char)31, ':', '*', '?', '\\', '/'
|
||||
};
|
||||
|
||||
private readonly ILogger<ManagedFileSystem> _logger;
|
||||
private readonly List<IShortcutHandler> _shortcutHandlers;
|
||||
private readonly string _tempPath;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ManagedFileSystem"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">The <see cref="ILogger"/> instance to use.</param>
|
||||
/// <param name="applicationPaths">The <see cref="IApplicationPaths"/> instance to use.</param>
|
||||
/// <param name="shortcutHandlers">the <see cref="IShortcutHandler"/>'s to use.</param>
|
||||
public ManagedFileSystem(
|
||||
ILogger<ManagedFileSystem> logger,
|
||||
IApplicationPaths applicationPaths)
|
||||
IApplicationPaths applicationPaths,
|
||||
IEnumerable<IShortcutHandler> shortcutHandlers)
|
||||
{
|
||||
_logger = logger;
|
||||
_tempPath = applicationPaths.TempDirectory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void AddShortcutHandler(IShortcutHandler handler)
|
||||
{
|
||||
_shortcutHandlers.Add(handler);
|
||||
_shortcutHandlers = shortcutHandlers.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -106,15 +103,17 @@ namespace Emby.Server.Implementations.IO
|
||||
return filePath;
|
||||
}
|
||||
|
||||
var filePathSpan = filePath.AsSpan();
|
||||
|
||||
// relative path
|
||||
if (firstChar == '\\')
|
||||
{
|
||||
filePath = filePath.Substring(1);
|
||||
filePathSpan = filePathSpan.Slice(1);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Path.GetFullPath(Path.Combine(folderPath, filePath));
|
||||
return Path.GetFullPath(Path.Join(folderPath, filePathSpan));
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
|
@ -89,6 +89,10 @@ namespace Emby.Server.Implementations.Library
|
||||
// bts sync files
|
||||
"**/*.bts",
|
||||
"**/*.sync",
|
||||
|
||||
// zfs
|
||||
"**/.zfs/**",
|
||||
"**/.zfs"
|
||||
};
|
||||
|
||||
private static readonly GlobOptions _globOptions = new GlobOptions
|
||||
|
@ -46,7 +46,6 @@ using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Library;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Tasks;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||
using EpisodeInfo = Emby.Naming.TV.EpisodeInfo;
|
||||
@ -839,19 +838,12 @@ namespace Emby.Server.Implementations.Library
|
||||
{
|
||||
var path = Person.GetPath(name);
|
||||
var id = GetItemByNameId<Person>(path);
|
||||
if (GetItemById(id) is not Person item)
|
||||
if (GetItemById(id) is Person item)
|
||||
{
|
||||
item = new Person
|
||||
{
|
||||
Name = name,
|
||||
Id = id,
|
||||
DateCreated = DateTime.UtcNow,
|
||||
DateModified = DateTime.UtcNow,
|
||||
Path = path
|
||||
};
|
||||
return item;
|
||||
}
|
||||
|
||||
return item;
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -1162,7 +1154,7 @@ namespace Emby.Server.Implementations.Library
|
||||
Name = Path.GetFileName(dir),
|
||||
|
||||
Locations = _fileSystem.GetFilePaths(dir, false)
|
||||
.Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
|
||||
.Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
|
||||
.Select(i =>
|
||||
{
|
||||
try
|
||||
@ -2900,9 +2892,18 @@ namespace Emby.Server.Implementations.Library
|
||||
var saveEntity = false;
|
||||
var personEntity = GetPerson(person.Name);
|
||||
|
||||
// if PresentationUniqueKey is empty it's likely a new item.
|
||||
if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey))
|
||||
if (personEntity is null)
|
||||
{
|
||||
var path = Person.GetPath(person.Name);
|
||||
personEntity = new Person()
|
||||
{
|
||||
Name = person.Name,
|
||||
Id = GetItemByNameId<Person>(path),
|
||||
DateCreated = DateTime.UtcNow,
|
||||
DateModified = DateTime.UtcNow,
|
||||
Path = path
|
||||
};
|
||||
|
||||
personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey();
|
||||
saveEntity = true;
|
||||
}
|
||||
@ -3135,7 +3136,7 @@ namespace Emby.Server.Implementations.Library
|
||||
}
|
||||
|
||||
var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true)
|
||||
.Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
|
||||
.Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase))
|
||||
.FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrEmpty(shortcut))
|
||||
|
@ -94,9 +94,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
|
||||
if (AudioFileParser.IsAudioFile(args.Path, _namingOptions))
|
||||
{
|
||||
var extension = Path.GetExtension(args.Path);
|
||||
var extension = Path.GetExtension(args.Path.AsSpan());
|
||||
|
||||
if (string.Equals(extension, ".cue", StringComparison.OrdinalIgnoreCase))
|
||||
if (extension.Equals(".cue", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// if audio file exists of same name, return null
|
||||
return null;
|
||||
@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio
|
||||
|
||||
if (item is not null)
|
||||
{
|
||||
item.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase);
|
||||
item.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
item.IsInMixedFolder = true;
|
||||
}
|
||||
|
@ -263,7 +263,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
return false;
|
||||
}
|
||||
|
||||
return directoryService.GetFilePaths(fullPath).Any(i => string.Equals(Path.GetExtension(i), ".vob", StringComparison.OrdinalIgnoreCase));
|
||||
return directoryService.GetFilePaths(fullPath).Any(i => Path.GetExtension(i.AsSpan()).Equals(".vob", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -32,9 +32,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
||||
return GetBook(args);
|
||||
}
|
||||
|
||||
var extension = Path.GetExtension(args.Path);
|
||||
var extension = Path.GetExtension(args.Path.AsSpan());
|
||||
|
||||
if (extension is not null && _validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
if (_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// It's a book
|
||||
return new Book
|
||||
@ -51,12 +51,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books
|
||||
{
|
||||
var bookFiles = args.FileSystemChildren.Where(f =>
|
||||
{
|
||||
var fileExtension = Path.GetExtension(f.FullName)
|
||||
?? string.Empty;
|
||||
var fileExtension = Path.GetExtension(f.FullName.AsSpan());
|
||||
|
||||
return _validExtensions.Contains(
|
||||
fileExtension,
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}).ToList();
|
||||
|
||||
// Don't return a Book if there is more (or less) than one document in the directory
|
||||
|
@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@ -25,7 +23,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
private readonly NamingOptions _namingOptions;
|
||||
private readonly IDirectoryService _directoryService;
|
||||
|
||||
private static readonly HashSet<string> _ignoreFiles = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
private static readonly string[] _ignoreFiles = new[]
|
||||
{
|
||||
"folder",
|
||||
"thumb",
|
||||
@ -56,7 +54,7 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
/// </summary>
|
||||
/// <param name="args">The args.</param>
|
||||
/// <returns>Trailer.</returns>
|
||||
protected override Photo Resolve(ItemResolveArgs args)
|
||||
protected override Photo? Resolve(ItemResolveArgs args)
|
||||
{
|
||||
if (!args.IsDirectory)
|
||||
{
|
||||
@ -68,10 +66,11 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
{
|
||||
if (IsImageFile(args.Path, _imageProcessor))
|
||||
{
|
||||
var filename = Path.GetFileNameWithoutExtension(args.Path);
|
||||
var filename = Path.GetFileNameWithoutExtension(args.Path.AsSpan());
|
||||
|
||||
// Make sure the image doesn't belong to a video file
|
||||
var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path));
|
||||
var files = _directoryService.GetFiles(Path.GetDirectoryName(args.Path)
|
||||
?? throw new InvalidOperationException("Path can't be a root directory."));
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
@ -92,32 +91,32 @@ namespace Emby.Server.Implementations.Library.Resolvers
|
||||
return null;
|
||||
}
|
||||
|
||||
internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, string imageFilename)
|
||||
internal static bool IsOwnedByMedia(NamingOptions namingOptions, string file, ReadOnlySpan<char> imageFilename)
|
||||
{
|
||||
return VideoResolver.IsVideoFile(file, namingOptions) && IsOwnedByResolvedMedia(file, imageFilename);
|
||||
}
|
||||
|
||||
internal static bool IsOwnedByResolvedMedia(string file, string imageFilename)
|
||||
internal static bool IsOwnedByResolvedMedia(ReadOnlySpan<char> file, ReadOnlySpan<char> imageFilename)
|
||||
=> imageFilename.StartsWith(Path.GetFileNameWithoutExtension(file), StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
internal static bool IsImageFile(string path, IImageProcessor imageProcessor)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(path);
|
||||
|
||||
var extension = Path.GetExtension(path.AsSpan()).TrimStart('.');
|
||||
if (!imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var filename = Path.GetFileNameWithoutExtension(path);
|
||||
|
||||
if (_ignoreFiles.Contains(filename))
|
||||
if (_ignoreFiles.Any(i => filename.StartsWith(i, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_ignoreFiles.Any(i => filename.IndexOf(i, StringComparison.OrdinalIgnoreCase) != -1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string extension = Path.GetExtension(path).TrimStart('.');
|
||||
return imageProcessor.SupportedInputFormats.Contains(extension, StringComparison.OrdinalIgnoreCase);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -94,14 +94,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
|
||||
else if (!string.IsNullOrWhiteSpace(extInf) && !trimmedLine.StartsWith('#'))
|
||||
{
|
||||
var channel = GetChannelnfo(extInf, tunerHostId, trimmedLine);
|
||||
if (string.IsNullOrWhiteSpace(channel.Id))
|
||||
{
|
||||
channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
else
|
||||
{
|
||||
channel.Id = channelIdPrefix + channel.Id.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
channel.Id = channelIdPrefix + trimmedLine.GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
|
||||
channel.Path = trimmedLine;
|
||||
channels.Add(channel);
|
||||
|
@ -1 +1,43 @@
|
||||
{}
|
||||
{
|
||||
"Albums": "এলবাম",
|
||||
"Application": "আবেদন",
|
||||
"AppDeviceValues": "এপ্: {0}, ডিভাইচ: {1}",
|
||||
"Artists": "শিল্পী",
|
||||
"Channels": "চেনেলস",
|
||||
"Default": "ডিফল্ট",
|
||||
"AuthenticationSucceededWithUserName": "{0} সফলভাবে প্রমাণিত",
|
||||
"Books": "পুস্তক",
|
||||
"Movies": "চলচ্চিত্ৰ",
|
||||
"CameraImageUploadedFrom": "একটি নতুন ক্যামেরা চিত্র আপলোড করা হয়েছে {0}",
|
||||
"Collections": "সংগ্রহ",
|
||||
"HeaderFavoriteShows": "প্রিয় শোসমূহ",
|
||||
"Latest": "শেহতীয়া",
|
||||
"MessageApplicationUpdated": "জেলিফিন চাইভাৰ আপডেট কৰা হৈছে",
|
||||
"MixedContent": "মিশ্ৰিত সমগ্ৰতা",
|
||||
"NewVersionIsAvailable": "ডাউনলোড কৰিবলৈ জেলিফিন চাইভাৰৰ এটা নতুন সংস্কৰণ উপলব্ধ আছে.",
|
||||
"NotificationOptionCameraImageUploaded": "কেমেৰাৰ চিত্ৰ আপল'ড কৰা হ'ল",
|
||||
"External": "বাহ্যিক",
|
||||
"Favorites": "পছন্দসই",
|
||||
"Folders": "ফোল্ডাৰ",
|
||||
"Forced": "বলপূর্বক",
|
||||
"Genres": "শ্রেণী",
|
||||
"HeaderAlbumArtists": "অ্যালবাম শিল্পী",
|
||||
"HeaderContinueWatching": "দেখা চালিয়ে যান",
|
||||
"FailedLoginAttemptWithUserName": "লগইন ব্যর্থ চেষ্টা কৰা হৈছে থেকে {0}",
|
||||
"HeaderFavoriteAlbums": "প্রিয় অ্যালবামসমূহ",
|
||||
"HeaderFavoriteArtists": "প্রিয় শিল্পীসমূহ",
|
||||
"HeaderFavoriteEpisodes": "প্রিয় পর্বসমূহ",
|
||||
"HeaderFavoriteSongs": "প্ৰিয় গীত",
|
||||
"HeaderLiveTV": "প্ৰতিবেদন টিভি",
|
||||
"HeaderNextUp": "পৰৱৰ্তী অংশ",
|
||||
"HeaderRecordingGroups": "অলংকৰণ গোষ্ঠীসমূহ",
|
||||
"HearingImpaired": "শ্ৰবণ অক্ষম",
|
||||
"HomeVideos": "ঘৰৰ ভিডিঅ'সমূহ",
|
||||
"Inherit": "উত্তপ্ত কৰা",
|
||||
"MessageServerConfigurationUpdated": "চাইভাৰ কনফিগাৰেশ্যন আপডেট কৰা হৈছে",
|
||||
"NotificationOptionApplicationUpdateAvailable": "অ্যাপ্লিকেশ্যন আপডেট উপলব্ধ",
|
||||
"NotificationOptionApplicationUpdateInstalled": "অ্যাপ্লিকেশ্যন আপডেট ইনষ্টল কৰা হ'ল",
|
||||
"NotificationOptionAudioPlayback": "অডিঅ' প্লেবেক আৰম্ভ হ'ল",
|
||||
"NotificationOptionAudioPlaybackStopped": "অডিঅ' প্লেবেক আঁতৰ হ'ল",
|
||||
"NotificationOptionInstallationFailed": "ইনষ্টলেশ্যন ব্যৰ্থতা"
|
||||
}
|
||||
|
52
Emby.Server.Implementations/Localization/Core/chr.json
Normal file
52
Emby.Server.Implementations/Localization/Core/chr.json
Normal file
@ -0,0 +1,52 @@
|
||||
{
|
||||
"ChapterNameValue": "Didanedi {0}",
|
||||
"HeaderAlbumArtists": "Didanidanolisgisgi",
|
||||
"HeaderFavoriteAlbums": "Dvganidi didanidisgisgi",
|
||||
"HeaderLiveTV": "Anigadi didanidisgosgi",
|
||||
"HeaderRecordingGroups": "Didanisquodiisgisgi",
|
||||
"HomeVideos": "Diganadi dinagadisgisgi",
|
||||
"Inherit": "Anigwe",
|
||||
"MessageApplicationUpdatedTo": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe anigadi {0}",
|
||||
"MixedContent": "Ganinidi dininoladisgisgi",
|
||||
"Movies": "Anidvnisgisgi",
|
||||
"MusicVideos": "Danodisgisgi didanidisgosgi",
|
||||
"NotificationOptionAudioPlayback": "Didanidigwe diganuyisgisgi anigadi",
|
||||
"NotificationOptionInstallationFailed": "Diudvdi anadvnatisgisgi",
|
||||
"NotificationOptionPluginUninstalled": "Ditsigvhnidv anawvdisgisgi",
|
||||
"Albums": "Anigawidaniyv",
|
||||
"Application": "Didanvyi",
|
||||
"Artists": "Dinidaniyi",
|
||||
"AuthenticationSucceededWithUserName": "{0} Sesoquonisdi nagadani",
|
||||
"Books": "Didanedi",
|
||||
"CameraImageUploadedFrom": "Anigawidaniyv nasgi didagwalanvyi {0}",
|
||||
"Channels": "Diganadasgi",
|
||||
"Collections": "Diganadisgi",
|
||||
"Default": "Dinadi",
|
||||
"DeviceOfflineWithName": "{0} Aniyvolehvi nasgi",
|
||||
"External": "Amohdi",
|
||||
"Favorites": "Nvdayelvdisgi",
|
||||
"Folders": "Didanididisgi",
|
||||
"Forced": "Ganedi",
|
||||
"Genres": "Diganadisgi",
|
||||
"HeaderContinueWatching": "Uwoditsu asdanidisgisgi",
|
||||
"HeaderFavoriteArtists": "Dvganidi dinidanolisgisgi",
|
||||
"HeaderFavoriteEpisodes": "Dvganidi didanidilisgadisgisgi",
|
||||
"HeaderFavoriteShows": "Dvganidi didanididanolisgisgi)",
|
||||
"HeaderFavoriteSongs": "Dvganidi danodisgisgi",
|
||||
"HeaderNextUp": "Anidvli uwodoli",
|
||||
"HearingImpaired": "Anitsunidi talunidisgisgi",
|
||||
"ItemAddedWithName": "{0} Dinigwe anididanidisgi",
|
||||
"Latest": "Uwodoli",
|
||||
"MessageApplicationUpdated": "Tsenigwidinonvhi Jellyfin Server tsadanidigwe",
|
||||
"MessageServerConfigurationUpdated": "Sedanidvdi anigadi diganidinonvhi",
|
||||
"Music": "Danodisgisgi",
|
||||
"NameSeasonUnknown": "Tsunita anidvdisgi",
|
||||
"NewVersionIsAvailable": "Danodigwe anigadi Jellyfin Server tsadanidigwe adisdi uwodvdi diganidinonvhi.",
|
||||
"NotificationOptionApplicationUpdateAvailable": "Disisdi tsadanidigwe udvdi",
|
||||
"NotificationOptionApplicationUpdateInstalled": "Disisdi tsadanidigwe digawvdi",
|
||||
"NotificationOptionAudioPlaybackStopped": "Didanidigwe diganuyisgisgi digawvdi",
|
||||
"NotificationOptionCameraImageUploaded": "Asdayi adininisgisgi diganuyisgisgi",
|
||||
"NotificationOptionNewLibraryContent": "Danodisgisgi anigadi digawvdi",
|
||||
"NotificationOptionPluginError": "Ditsigvhnidv anadvnatisgisgi",
|
||||
"NotificationOptionPluginInstalled": "Ditsigvhnidv digawvdi"
|
||||
}
|
@ -15,13 +15,13 @@
|
||||
"Favorites": "Favoritter",
|
||||
"Folders": "Mapper",
|
||||
"Genres": "Genrer",
|
||||
"HeaderAlbumArtists": "Albums kunstnere",
|
||||
"HeaderAlbumArtists": "Albumkunstnere",
|
||||
"HeaderContinueWatching": "Fortsæt afspilning",
|
||||
"HeaderFavoriteAlbums": "Favorit albummer",
|
||||
"HeaderFavoriteArtists": "Favorit kunstnere",
|
||||
"HeaderFavoriteEpisodes": "Favorit afsnit",
|
||||
"HeaderFavoriteShows": "Favorit serier",
|
||||
"HeaderFavoriteSongs": "Favorit sange",
|
||||
"HeaderFavoriteAlbums": "Favoritalbummer",
|
||||
"HeaderFavoriteArtists": "Favoritkunstnere",
|
||||
"HeaderFavoriteEpisodes": "Yndlingsafsnit",
|
||||
"HeaderFavoriteShows": "Yndlingsserier",
|
||||
"HeaderFavoriteSongs": "Yndlingssange",
|
||||
"HeaderLiveTV": "Live-TV",
|
||||
"HeaderNextUp": "Næste",
|
||||
"HeaderRecordingGroups": "Optagelsesgrupper",
|
||||
@ -34,8 +34,8 @@
|
||||
"Latest": "Seneste",
|
||||
"MessageApplicationUpdated": "Jellyfin Server er blevet opdateret",
|
||||
"MessageApplicationUpdatedTo": "Jellyfin Server er blevet opdateret til {0}",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Server konfiguration sektion {0} er blevet opdateret",
|
||||
"MessageServerConfigurationUpdated": "Server konfigurationen er blevet opdateret",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "Serverkonfiguration sektion {0} er blevet opdateret",
|
||||
"MessageServerConfigurationUpdated": "Serverkonfigurationen er blevet opdateret",
|
||||
"MixedContent": "Blandet indhold",
|
||||
"Movies": "Film",
|
||||
"Music": "Musik",
|
||||
@ -51,7 +51,7 @@
|
||||
"NotificationOptionCameraImageUploaded": "Kamerabillede uploadet",
|
||||
"NotificationOptionInstallationFailed": "Installationen mislykkedes",
|
||||
"NotificationOptionNewLibraryContent": "Nyt indhold tilføjet",
|
||||
"NotificationOptionPluginError": "Plugin fejl",
|
||||
"NotificationOptionPluginError": "Plugin-fejl",
|
||||
"NotificationOptionPluginInstalled": "Plugin blev installeret",
|
||||
"NotificationOptionPluginUninstalled": "Plugin blev afinstalleret",
|
||||
"NotificationOptionPluginUpdateInstalled": "Opdatering til plugin blev installeret",
|
||||
@ -92,26 +92,26 @@
|
||||
"ValueHasBeenAddedToLibrary": "{0} er blevet tilføjet til dit mediebibliotek",
|
||||
"ValueSpecialEpisodeName": "Special - {0}",
|
||||
"VersionNumber": "Version {0}",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata konfigurationen.",
|
||||
"TaskDownloadMissingSubtitlesDescription": "Søger på internettet efter manglende undertekster baseret på metadata-konfigurationen.",
|
||||
"TaskDownloadMissingSubtitles": "Hent manglende undertekster",
|
||||
"TaskUpdatePluginsDescription": "Henter og installerer opdateringer for plugins, som er indstillet til at blive opdateret automatisk.",
|
||||
"TaskUpdatePlugins": "Opdater Plugins",
|
||||
"TaskCleanLogsDescription": "Sletter log filer som er mere end {0} dage gamle.",
|
||||
"TaskCleanLogs": "Ryd Log mappe",
|
||||
"TaskRefreshLibraryDescription": "Scanner dit medie bibliotek for nye filer og opdateret metadata.",
|
||||
"TaskRefreshLibrary": "Scan Medie Bibliotek",
|
||||
"TaskCleanCacheDescription": "Sletter cache filer som systemet ikke længere bruger.",
|
||||
"TaskCleanCache": "Ryd Cache mappe",
|
||||
"TasksChannelsCategory": "Internet Kanaler",
|
||||
"TaskCleanLogsDescription": "Sletter log-filer som er mere end {0} dage gamle.",
|
||||
"TaskCleanLogs": "Ryd Log-mappe",
|
||||
"TaskRefreshLibraryDescription": "Scanner dit mediebibliotek for nye filer og opdateret metadata.",
|
||||
"TaskRefreshLibrary": "Scan Mediebibliotek",
|
||||
"TaskCleanCacheDescription": "Sletter cache-filer som systemet ikke længere bruger.",
|
||||
"TaskCleanCache": "Ryd Cache-mappe",
|
||||
"TasksChannelsCategory": "Internetkanaler",
|
||||
"TasksApplicationCategory": "Applikation",
|
||||
"TasksLibraryCategory": "Bibliotek",
|
||||
"TasksMaintenanceCategory": "Vedligeholdelse",
|
||||
"TaskRefreshChapterImages": "Udtræk kapitel billeder",
|
||||
"TaskRefreshChapterImagesDescription": "Lav miniaturebilleder for videoer der har kapitler.",
|
||||
"TaskRefreshChannelsDescription": "Opdater internet kanal information.",
|
||||
"TaskRefreshChapterImages": "Udtræk kapitelbilleder",
|
||||
"TaskRefreshChapterImagesDescription": "Laver miniaturebilleder for videoer, der har kapitler.",
|
||||
"TaskRefreshChannelsDescription": "Opdaterer information for internetkanal.",
|
||||
"TaskRefreshChannels": "Opdater Kanaler",
|
||||
"TaskCleanTranscodeDescription": "Fjern transcode filer som er mere end 1 dag gammel.",
|
||||
"TaskCleanTranscode": "Tøm Transcode mappen",
|
||||
"TaskCleanTranscodeDescription": "Fjerner transcode-filer, som er mere end 1 dag gammel.",
|
||||
"TaskCleanTranscode": "Tøm Transcode-mappen",
|
||||
"TaskRefreshPeople": "Opdater Personer",
|
||||
"TaskRefreshPeopleDescription": "Opdaterer metadata for skuespillere og instruktører i dit mediebibliotek.",
|
||||
"TaskCleanActivityLogDescription": "Sletter linjer i aktivitetsloggen ældre end den konfigurerede alder.",
|
||||
@ -121,8 +121,8 @@
|
||||
"Default": "Standard",
|
||||
"TaskOptimizeDatabaseDescription": "Komprimerer databasen og frigør plads. Denne handling køres efter at have scannet mediebiblioteket, eller efter at have lavet ændringer til databasen, for at højne ydeevnen.",
|
||||
"TaskOptimizeDatabase": "Optimér database",
|
||||
"TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS playlister. Denne opgave kan tage lang tid.",
|
||||
"TaskKeyframeExtractor": "Nøglebillede udtræk",
|
||||
"TaskKeyframeExtractorDescription": "Udtrækker billeder fra videofiler for at lave mere præcise HLS-playlister. Denne opgave kan tage lang tid.",
|
||||
"TaskKeyframeExtractor": "Udtræk af nøglebillede",
|
||||
"External": "Ekstern",
|
||||
"HearingImpaired": "Hørehæmmet"
|
||||
}
|
||||
|
@ -3,9 +3,9 @@
|
||||
"AppDeviceValues": "Aplicación: {0}, Dispositivo: {1}",
|
||||
"Application": "Aplicación",
|
||||
"Artists": "Artistas",
|
||||
"AuthenticationSucceededWithUserName": "{0} identificado correctamente",
|
||||
"AuthenticationSucceededWithUserName": "{0} autenticado correctamente",
|
||||
"Books": "Libros",
|
||||
"CameraImageUploadedFrom": "Se ha subido una nueva imagen de cámara desde {0}",
|
||||
"CameraImageUploadedFrom": "Se ha subido una nueva imagen por cámara desde {0}",
|
||||
"Channels": "Canales",
|
||||
"ChapterNameValue": "Capítulo {0}",
|
||||
"Collections": "Colecciones",
|
||||
|
@ -105,8 +105,8 @@
|
||||
"TaskRefreshPeople": "Actualiser les acteurs",
|
||||
"TaskCleanLogsDescription": "Supprime les journaux de plus de {0} jours.",
|
||||
"TaskCleanLogs": "Nettoyer le répertoire des journaux",
|
||||
"TaskRefreshLibraryDescription": "Scanne votre médiathèque pour trouver les nouveaux fichiers et actualise les métadonnées.",
|
||||
"TaskRefreshLibrary": "Scanner la médiathèque",
|
||||
"TaskRefreshLibraryDescription": "Analyser sa médiathèque pour trouver les nouveaux fichiers et actualiser les métadonnées.",
|
||||
"TaskRefreshLibrary": "Analyser la médiathèque",
|
||||
"TaskRefreshChapterImagesDescription": "Crée des vignettes pour les vidéos ayant des chapitres.",
|
||||
"TaskRefreshChapterImages": "Extraire les images de chapitre",
|
||||
"TaskCleanCacheDescription": "Supprime les fichiers de cache dont le système n'a plus besoin.",
|
||||
|
@ -3,5 +3,125 @@
|
||||
"TaskOptimizeDatabase": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಆಪ್ಟಿಮೈಜ್ ಮಾಡಿ",
|
||||
"TaskOptimizeDatabaseDescription": "ಡೇಟಾಬೇಸ್ ಅನ್ನು ಕಾಂಪ್ಯಾಕ್ಟ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮುಕ್ತ ಜಾಗವನ್ನು ಮೊಟಕುಗೊಳಿಸುತ್ತದೆ. ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡಿದ ನಂತರ ಈ ಕಾರ್ಯವನ್ನು ನಡೆಸುವುದು ಅಥವಾ ಡೇಟಾಬೇಸ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಸೂಚಿಸುವ ಇತರ ಬದಲಾವಣೆಗಳನ್ನು ಮಾಡುವುದರಿಂದ ಕಾರ್ಯಕ್ಷಮತೆಯನ್ನು ಸುಧಾರಿಸಬಹುದು.",
|
||||
"TaskKeyframeExtractor": "ಕೀಫ್ರೇಮ್ ಎಕ್ಸ್ಟ್ರಾಕ್ಟರ್",
|
||||
"TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್ಗಳಿಂದ ಕೀಫ್ರೇಮ್ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು."
|
||||
"TaskKeyframeExtractorDescription": "ಹೆಚ್ಚು ನಿಖರವಾದ HLS ಪ್ಲೇಪಟ್ಟಿಗಳನ್ನು ರಚಿಸಲು ವೀಡಿಯೊ ಫೈಲ್ಗಳಿಂದ ಕೀಫ್ರೇಮ್ಗಳನ್ನು ಹೊರತೆಗೆಯುತ್ತದೆ. ಈ ಕಾರ್ಯವು ದೀರ್ಘಕಾಲದವರೆಗೆ ನಡೆಯಬಹುದು.",
|
||||
"ValueHasBeenAddedToLibrary": "{0} ಅನ್ನು ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಗೆ ಸೇರಿಸಲಾಗಿದೆ",
|
||||
"ValueSpecialEpisodeName": "ವಿಶೇಷ - {0}",
|
||||
"TasksLibraryCategory": "ಸಮೊಹ",
|
||||
"TasksApplicationCategory": "ಅಪ್ಲಿಕೇಶನ್",
|
||||
"TasksChannelsCategory": "ಇಂಟರ್ನೆಟ್ ಚಾನೆಲ್ಗಳು",
|
||||
"TaskCleanCache": "ಕ್ಲೀನ್ ಕ್ಯಾಶ ಡೈರೆಕ್ಟರಿ",
|
||||
"TaskCleanCacheDescription": "ಸಿಸ್ಟಮ್ಗೆ ಇನ್ನು ಮುಂದೆ ಅಗತ್ಯವಿಲ್ಲದ ಸಂಗ್ರಹ ಫೈಲ್ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
|
||||
"TaskRefreshLibrary": "ಸ್ಕ್ಯಾನ್ ಮೀಡಿಯಾ ಲೈಬ್ರರಿ",
|
||||
"UserOfflineFromDevice": "{1} ನಿಂದ {0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
|
||||
"Albums": "ಸಂಪುಟ",
|
||||
"Application": "ಅಪ್ಲಿಕೇಶನ್",
|
||||
"AppDeviceValues": "ಅಪ್ಲಿಕೇಶನ್: {0}, ಸಾಧನ: {1}",
|
||||
"Artists": "ಕಲಾವಿದರು",
|
||||
"AuthenticationSucceededWithUserName": "{0} ಯಶಸ್ವಿಯಾಗಿ ದೃಢೀಕರಿಸಲಾಗಿದೆ",
|
||||
"Books": "ಪುಸ್ತಕಗಳು",
|
||||
"ChapterNameValue": "ಅಧ್ಯಾಯ {0}",
|
||||
"Collections": "ಸಂಗ್ರಹಣೆಗಳು",
|
||||
"Default": "ಪೂರ್ವನಿಯೋಜಿತ",
|
||||
"DeviceOfflineWithName": "{0} ಸಂಪರ್ಕ ಕಡಿತಗೊಂಡಿದೆ",
|
||||
"DeviceOnlineWithName": "{0} ಸಂಪರ್ಕಗೊಂಡಿದೆ",
|
||||
"External": "ಹೊರಗಿನ",
|
||||
"FailedLoginAttemptWithUserName": "{0} ರಿಂದ ವಿಫಲ ಲಾಗಿನ್ ಪ್ರಯತ್ನ",
|
||||
"Favorites": "ಮೆಚ್ಚಿನವುಗಳು",
|
||||
"Folders": "ಫೋಲ್ಡರ್ಗಳು",
|
||||
"Forced": "ಬಲವಂತವಾಗಿ",
|
||||
"Genres": "ಪ್ರಕಾರಗಳು",
|
||||
"HeaderContinueWatching": "ನೋಡುವುದನ್ನು ಮುಂದುವರಿಸಿ",
|
||||
"HeaderFavoriteAlbums": "ಮೆಚ್ಚಿನ ಸಂಪುಟಗಳು",
|
||||
"HeaderFavoriteArtists": "ಮೆಚ್ಚಿನ ಕಲಾವಿದರು",
|
||||
"HeaderFavoriteShows": "ಮೆಚ್ಚಿನ ಪ್ರದರ್ಶನಗಳು",
|
||||
"HeaderFavoriteSongs": "ಮೆಚ್ಚಿನ ಹಾಡುಗಳು",
|
||||
"HeaderLiveTV": "ನೇರ ದೂರದರ್ಶನ",
|
||||
"HeaderNextUp": "ಮುಂದೆ",
|
||||
"HeaderRecordingGroups": "ರೆಕಾರ್ಡಿಂಗ್ ಗುಂಪುಗಳು",
|
||||
"MessageApplicationUpdated": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
|
||||
"CameraImageUploadedFrom": "ಹೊಸ ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು {0} ನಿಂದ ಅಪ್ಲೋಡ್ ಮಾಡಲಾಗಿದೆ",
|
||||
"Channels": "ಮೂಲಗಳು",
|
||||
"HeaderAlbumArtists": "ಸಂಪುಟ ಕಲಾವಿದರು",
|
||||
"HeaderFavoriteEpisodes": "ಮೆಚ್ಚಿನ ಸಂಚಿಕೆಗಳು",
|
||||
"HearingImpaired": "ಮೂಗ",
|
||||
"ItemAddedWithName": "{0} ಅನ್ನು ಸಂಕಲನಕ್ಕೆ ಸೇರಿಸಲಾಗಿದೆ",
|
||||
"MessageApplicationUpdatedTo": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಅನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ",
|
||||
"MessageNamedServerConfigurationUpdatedWithValue": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ವಿಭಾಗ {0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
|
||||
"NewVersionIsAvailable": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ನ ಹೊಸ ಆವೃತ್ತಿಯು ಡೌನ್ಲೋಡ್ಗೆ ಲಭ್ಯವಿದೆ.",
|
||||
"NotificationOptionAudioPlayback": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ",
|
||||
"NotificationOptionCameraImageUploaded": "ಕ್ಯಾಮರಾ ಚಿತ್ರವನ್ನು ಅಪ್ಲೋಡ್ ಮಾಡಲಾಗಿದೆ",
|
||||
"NotificationOptionPluginUninstalled": "ಪ್ಲಗಿನ್ ಅನ್ಇನ್ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ",
|
||||
"NotificationOptionUserLockedOut": "ಬಳಕೆದಾರರು ಲಾಕ್ ಔಟ್ ಆಗಿದ್ದಾರೆ",
|
||||
"NotificationOptionVideoPlaybackStopped": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ",
|
||||
"PluginUninstalledWithName": "{0} ಅನ್ನು ಅನ್ಇನ್ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ",
|
||||
"ScheduledTaskFailedWithName": "{0} ವಿಫಲವಾಗಿದೆ",
|
||||
"ScheduledTaskStartedWithName": "{0} ಪ್ರಾರಂಭವಾಯಿತು",
|
||||
"ServerNameNeedsToBeRestarted": "{0} ಅನ್ನು ಮರುಪ್ರಾರಂಭಿಸಬೇಕಾಗಿದೆ",
|
||||
"UserCreatedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ರಚಿಸಲಾಗಿದೆ",
|
||||
"UserLockedOutWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಲಾಕ್ ಮಾಡಲಾಗಿದೆ",
|
||||
"UserOnlineFromDevice": "{1} ನಿಂದ {0} ಆನ್ಲೈನ್ನಲ್ಲಿದೆ",
|
||||
"UserPasswordChangedWithName": "{0} ಬಳಕೆದಾರರಿಗಾಗಿ ಪಾಸ್ವರ್ಡ್ ಅನ್ನು ಬದಲಾಯಿಸಲಾಗಿದೆ",
|
||||
"UserPolicyUpdatedWithName": "ಬಳಕೆದಾರರ ನೀತಿಯನ್ನು {0} ಗೆ ನವೀಕರಿಸಲಾಗಿದೆ",
|
||||
"UserStartedPlayingItemWithValues": "{2} ರಂದು {0} ಆಡುತ್ತಿದೆ {1}",
|
||||
"UserStoppedPlayingItemWithValues": "{0} ಅವರು {1} ಅನ್ನು {2} ನಲ್ಲಿ ಆಡುವುದನ್ನು ಮುಗಿಸಿದ್ದಾರೆ",
|
||||
"VersionNumber": "ಆವೃತ್ತಿ {0}",
|
||||
"TasksMaintenanceCategory": "ನಿರ್ವಹಣೆ",
|
||||
"TaskCleanActivityLog": "ಕ್ಲೀನ್ ಚಟುವಟಿಕೆ ಲಾಗ್",
|
||||
"TaskCleanActivityLogDescription": "ಕಾನ್ಫಿಗರ್ ಮಾಡಿದ ವಯಸ್ಸಿಗಿಂತ ಹಳೆಯದಾದ ಚಟುವಟಿಕೆ ಲಾಗ್ ನಮೂದುಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
|
||||
"TaskRefreshChapterImages": "ಅಧ್ಯಾಯ ಚಿತ್ರಗಳನ್ನು ಹೊರತೆಗೆಯಿರಿ",
|
||||
"TaskRefreshChapterImagesDescription": "ಅಧ್ಯಾಯಗಳನ್ನು ಹೊಂದಿರುವ ವೀಡಿಯೊಗಳಿಗಾಗಿ ಥಂಬ್ನೇಲ್ಗಳನ್ನು ರಚಿಸುತ್ತದೆ.",
|
||||
"TaskRefreshLibraryDescription": "ಹೊಸ ಫೈಲ್ಗಳಿಗಾಗಿ ನಿಮ್ಮ ಮೀಡಿಯಾ ಲೈಬ್ರರಿಯನ್ನು ಸ್ಕ್ಯಾನ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಮೆಟಾಡೇಟಾವನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ.",
|
||||
"TaskCleanLogsDescription": "{0} ದಿನಗಳಿಗಿಂತ ಹಳೆಯದಾದ ಲಾಗ್ ಫೈಲ್ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
|
||||
"TaskUpdatePluginsDescription": "ಸ್ವಯಂಚಾಲಿತವಾಗಿ ನವೀಕರಿಸಲು ಕಾನ್ಫಿಗರ್ ಮಾಡಲಾದ ಪ್ಲಗಿನ್ಗಳಿಗಾಗಿ ನವೀಕರಣಗಳನ್ನು ಡೌನ್ಲೋಡ್ ಮಾಡುತ್ತದೆ ಮತ್ತು ಸ್ಥಾಪಿಸುತ್ತದೆ.",
|
||||
"TaskCleanTranscodeDescription": "ಒಂದು ದಿನಕ್ಕಿಂತ ಹಳೆಯದಾದ ಟ್ರಾನ್ಸ್ಕೋಡ್ ಫೈಲ್ಗಳನ್ನು ಅಳಿಸುತ್ತದೆ.",
|
||||
"TaskDownloadMissingSubtitles": "ಕಾಣೆಯಾದ ಉಪಶೀರ್ಷಿಕೆಗಳನ್ನು ಡೌನ್ಲೋಡ್ ಮಾಡಿ",
|
||||
"Shows": "ಧಾರವಾಹಿಗಳು",
|
||||
"Songs": "ಹಾಡುಗಳು",
|
||||
"StartupEmbyServerIsLoading": "ಜೆಲ್ಲಿಫಿನ್ ಸರ್ವರ್ ಲೋಡ್ ಆಗುತ್ತಿದೆ. ದಯವಿಟ್ಟು ಸ್ವಲ್ಪ ಸಮಯದ ನಂತರ ಮತ್ತೆ ಪ್ರಯತ್ನಿಸಿ.",
|
||||
"UserDeletedWithName": "ಬಳಕೆದಾರ {0} ಅನ್ನು ಅಳಿಸಲಾಗಿದೆ",
|
||||
"UserDownloadingItemWithValues": "{0} ಡೌನ್ಲೋಡ್ ಆಗುತ್ತಿದೆ {1}",
|
||||
"SubtitleDownloadFailureFromForItem": "ಉಪಶೀರ್ಷಿಕೆಗಳು {0} ನಿಂದ {1} ಗಾಗಿ ಡೌನ್ಲೋಡ್ ಮಾಡಲು ವಿಫಲವಾಗಿವೆ",
|
||||
"Sync": "ಹೊಂದಿಕೆ",
|
||||
"System": "ವ್ಯವಸ್ಥೆ",
|
||||
"TvShows": "ದೂರದರ್ಶನ ಕಾರ್ಯಕ್ರಮಗಳು",
|
||||
"Undefined": "ವ್ಯಾಖ್ಯಾನಿಸಲಾಗಿಲ್ಲ",
|
||||
"User": "ಬಳಕೆದಾರ",
|
||||
"HomeVideos": "ಮುಖಪುಟ ವೀಡಿಯೊಗಳು",
|
||||
"Inherit": "ಪಾರಂಪರ್ಯವಾಗಿ",
|
||||
"ItemRemovedWithName": "{0} ಅನ್ನು ಸಂಕಲನದಿಂದ ತೆಗೆದುಹಾಕಲಾಗಿದೆ",
|
||||
"LabelIpAddressValue": "IP ವಿಳಾಸ: {0}",
|
||||
"LabelRunningTimeValue": "ಅವಧಿ: {0}",
|
||||
"Latest": "ಹೊಸದಾದ",
|
||||
"MessageServerConfigurationUpdated": "ಸರ್ವರ್ ಕಾನ್ಫಿಗರೇಶನ್ ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
|
||||
"MixedContent": "ಮಿಶ್ರ ವಿಷಯ",
|
||||
"Movies": "ಚಲನಚಿತ್ರಗಳು",
|
||||
"Music": "ಸಂಗೀತ",
|
||||
"MusicVideos": "ಸಂಗೀತ ವೀಡಿಯೊಗಳು",
|
||||
"NameInstallFailed": "{0} ಸ್ಥಾಪನೆ ವಿಫಲವಾಗಿದೆ",
|
||||
"NameSeasonNumber": "ಸೀಸನ್ {0}",
|
||||
"NameSeasonUnknown": "ಸೀಸನ್ ತಿಳಿದಿಲ್ಲ",
|
||||
"NotificationOptionApplicationUpdateAvailable": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣ ಲಭ್ಯವಿದೆ",
|
||||
"NotificationOptionApplicationUpdateInstalled": "ಅಪ್ಲಿಕೇಶನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
|
||||
"NotificationOptionAudioPlaybackStopped": "ಆಡಿಯೋ ಪ್ಲೇಬ್ಯಾಕ್ ನಿಲ್ಲಿಸಲಾಗಿದೆ",
|
||||
"NotificationOptionInstallationFailed": "ಸ್ಥಾಪನ ವೈಫಲ್ಯ",
|
||||
"NotificationOptionNewLibraryContent": "ಹೊಸ ವಿಷಯವನ್ನು ಒಳಗೊಂಡಿದೆ",
|
||||
"NotificationOptionPluginError": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
|
||||
"NotificationOptionPluginInstalled": "ಪ್ಲಗಿನ್ ವೈಫಲ್ಯ",
|
||||
"NotificationOptionPluginUpdateInstalled": "ಪ್ಲಗಿನ್ ನವೀಕರಣವನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
|
||||
"NotificationOptionServerRestartRequired": "ಸರ್ವರ್ ಮರುಪ್ರಾರಂಭದ ಅಗತ್ಯವಿದೆ",
|
||||
"NotificationOptionTaskFailed": "ನಿಗದಿತ ಕಾರ್ಯ ವೈಫಲ್ಯ",
|
||||
"NotificationOptionVideoPlayback": "ವೀಡಿಯೊ ಪ್ಲೇಬ್ಯಾಕ್ ಪ್ರಾರಂಭವಾಗಿದೆ",
|
||||
"Photos": "ಚಿತ್ರಗಳು",
|
||||
"Playlists": "ಪ್ಲೇಪಟ್ಟಿಗಳು",
|
||||
"Plugin": "ಪ್ಲಗಿನ್",
|
||||
"PluginInstalledWithName": "{0} ಅನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿದೆ",
|
||||
"PluginUpdatedWithName": "{0} ಅನ್ನು ನವೀಕರಿಸಲಾಗಿದೆ",
|
||||
"ProviderValue": "ಒದಗಿಸುವವರು: {0}",
|
||||
"TaskCleanLogs": "ಕ್ಲೀನ್ ಲಾಗ್ ಡೈರೆಕ್ಟರಿ",
|
||||
"TaskRefreshPeople": "ಜನರನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
|
||||
"TaskRefreshPeopleDescription": "ನಿಮ್ಮ ಮಾಧ್ಯಮ ಲೈಬ್ರರಿಯಲ್ಲಿ ನಟರು ಮತ್ತು ನಿರ್ದೇಶಕರಿಗಾಗಿ ಮೆಟಾಡೇಟಾವನ್ನು ನವೀಕರಿಸಿ.",
|
||||
"TaskUpdatePlugins": "ಪ್ಲಗಿನ್ಗಳನ್ನು ನವೀಕರಿಸಿ",
|
||||
"TaskCleanTranscode": "ಟ್ರಾನ್ಸ್ಕೋಡ್ ಡೈರೆಕ್ಟರಿಯನ್ನು ಸ್ವಚ್ಛಗೊಳಿಸಿ",
|
||||
"TaskRefreshChannels": "ಚಾನಲ್ಗಳನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡಿ",
|
||||
"TaskRefreshChannelsDescription": "ಇಂಟರ್ನೆಟ್ ಚಾನಲ್ ಮಾಹಿತಿಯನ್ನು ರಿಫ್ರೆಶ್ ಮಾಡುತ್ತದೆ."
|
||||
}
|
||||
|
@ -121,5 +121,7 @@
|
||||
"TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്കാൻ ചെയ്തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്തതിന് ശേഷം ഈ ടാസ്ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.",
|
||||
"TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക",
|
||||
"HearingImpaired": "കേൾവി തകരാറുകൾ",
|
||||
"External": "പുറമേയുള്ള"
|
||||
"External": "പുറമേയുള്ള",
|
||||
"TaskKeyframeExtractorDescription": "കൂടുതൽ കൃത്യമായ HLS പ്ലേലിസ്റ്റുകൾ സൃഷ്ടിക്കുന്നതിന് വീഡിയോ ഫയലുകളിൽ നിന്ന് കീഫ്രെയിമുകൾ എക്സ്ട്രാക്റ്റ് ചെയ്യുന്നു. ഈ പ്രവർത്തനം പൂർത്തിയാവാൻ കുറച്ചധികം സമയം എടുത്തേക്കാം.",
|
||||
"TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ"
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"Albums": "Albums",
|
||||
"Albums": "Album",
|
||||
"AppDeviceValues": "Apl: {0}, Peranti: {1}",
|
||||
"Application": "Aplikasi",
|
||||
"Artists": "Artis-artis",
|
||||
|
@ -29,5 +29,8 @@
|
||||
"Forced": "Pressed",
|
||||
"External": "Outboard",
|
||||
"HeaderFavoriteEpisodes": "Treasured Tales",
|
||||
"HeaderFavoriteShows": "Treasured Tales"
|
||||
"HeaderFavoriteShows": "Treasured Tales",
|
||||
"ChapterNameValue": "Piece {0}",
|
||||
"HeaderFavoriteSongs": "Treasured Chimes",
|
||||
"HeaderNextUp": "Incoming"
|
||||
}
|
||||
|
@ -25,5 +25,14 @@
|
||||
"Channels": "Amashaneli",
|
||||
"Books": "Izincwadi",
|
||||
"Artists": "Abadlali",
|
||||
"Albums": "Ama-albhamu"
|
||||
"Albums": "Ama-albhamu",
|
||||
"CameraImageUploadedFrom": "Kulandelayo lwesithonjana sekhamera selithunyelwe kusuka ku {0}",
|
||||
"HeaderFavoriteArtists": "Abasethi Abathandekayo",
|
||||
"HeaderFavoriteEpisodes": "Izilimi Ezithandekayo",
|
||||
"HeaderFavoriteShows": "Izisho Ezithandekayo",
|
||||
"External": "Kwezifungo",
|
||||
"FailedLoginAttemptWithUserName": "Ukushayiswa kwesithombe sokungena okungekho {0}",
|
||||
"HeaderContinueWatching": "Buyela Ukubona",
|
||||
"HeaderFavoriteAlbums": "Izimpahla Ezithandwayo",
|
||||
"HeaderAlbumArtists": "Abasethi wenkulumo"
|
||||
}
|
||||
|
@ -222,7 +222,7 @@ namespace Emby.Server.Implementations.MediaEncoder
|
||||
{
|
||||
var deadImages = images
|
||||
.Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase)
|
||||
.Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i), StringComparison.OrdinalIgnoreCase))
|
||||
.Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase))
|
||||
.ToList();
|
||||
|
||||
foreach (var image in deadImages)
|
||||
|
@ -327,9 +327,9 @@ namespace Emby.Server.Implementations.Playlists
|
||||
// this is probably best done as a metadata provider
|
||||
// saving a file over itself will require some work to prevent this from happening when not needed
|
||||
var playlistPath = item.Path;
|
||||
var extension = Path.GetExtension(playlistPath);
|
||||
var extension = Path.GetExtension(playlistPath.AsSpan());
|
||||
|
||||
if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase))
|
||||
if (extension.Equals(".wpl", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var playlist = new WplPlaylist();
|
||||
foreach (var child in item.GetLinkedChildren())
|
||||
@ -362,8 +362,7 @@ namespace Emby.Server.Implementations.Playlists
|
||||
string text = new WplContent().ToText(playlist);
|
||||
File.WriteAllText(playlistPath, text);
|
||||
}
|
||||
|
||||
if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase))
|
||||
else if (extension.Equals(".zpl", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var playlist = new ZplPlaylist();
|
||||
foreach (var child in item.GetLinkedChildren())
|
||||
@ -396,8 +395,7 @@ namespace Emby.Server.Implementations.Playlists
|
||||
string text = new ZplContent().ToText(playlist);
|
||||
File.WriteAllText(playlistPath, text);
|
||||
}
|
||||
|
||||
if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase))
|
||||
else if (extension.Equals(".m3u", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var playlist = new M3uPlaylist
|
||||
{
|
||||
@ -428,8 +426,7 @@ namespace Emby.Server.Implementations.Playlists
|
||||
string text = new M3uContent().ToText(playlist);
|
||||
File.WriteAllText(playlistPath, text);
|
||||
}
|
||||
|
||||
if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase))
|
||||
else if (extension.Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var playlist = new M3uPlaylist();
|
||||
playlist.IsExtended = true;
|
||||
@ -458,8 +455,7 @@ namespace Emby.Server.Implementations.Playlists
|
||||
string text = new M3uContent().ToText(playlist);
|
||||
File.WriteAllText(playlistPath, text);
|
||||
}
|
||||
|
||||
if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase))
|
||||
else if (extension.Equals(".pls", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var playlist = new PlsPlaylist();
|
||||
foreach (var child in item.GetLinkedChildren())
|
||||
|
@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@ -11,7 +10,6 @@ using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Emby.Server.Implementations.Library;
|
||||
using Jellyfin.Extensions;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using Jellyfin.Extensions.Json.Converters;
|
||||
using MediaBrowser.Common;
|
||||
@ -30,7 +28,7 @@ namespace Emby.Server.Implementations.Plugins
|
||||
/// <summary>
|
||||
/// Defines the <see cref="PluginManager" />.
|
||||
/// </summary>
|
||||
public class PluginManager : IPluginManager
|
||||
public sealed class PluginManager : IPluginManager, IDisposable
|
||||
{
|
||||
private const string MetafileName = "meta.json";
|
||||
|
||||
@ -191,15 +189,6 @@ namespace Emby.Server.Implementations.Plugins
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UnloadAssemblies()
|
||||
{
|
||||
foreach (var assemblyLoadContext in _assemblyLoadContexts)
|
||||
{
|
||||
assemblyLoadContext.Unload();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates all the plugin instances.
|
||||
/// </summary>
|
||||
@ -441,6 +430,15 @@ namespace Emby.Server.Implementations.Plugins
|
||||
return SaveManifest(manifest, path);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
foreach (var assemblyLoadContext in _assemblyLoadContexts)
|
||||
{
|
||||
assemblyLoadContext.Unload();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reconciles the manifest against any properties that exist locally in a pre-packaged meta.json found at the path.
|
||||
/// If no file is found, no reconciliation occurs.
|
||||
|
@ -36,6 +36,7 @@ using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Session;
|
||||
using MediaBrowser.Model.SyncPlay;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
|
||||
|
||||
@ -44,7 +45,7 @@ namespace Emby.Server.Implementations.Session
|
||||
/// <summary>
|
||||
/// Class SessionManager.
|
||||
/// </summary>
|
||||
public class SessionManager : ISessionManager, IDisposable
|
||||
public sealed class SessionManager : ISessionManager, IAsyncDisposable
|
||||
{
|
||||
private readonly IUserDataManager _userDataManager;
|
||||
private readonly ILogger<SessionManager> _logger;
|
||||
@ -57,11 +58,9 @@ namespace Emby.Server.Implementations.Session
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IDeviceManager _deviceManager;
|
||||
|
||||
/// <summary>
|
||||
/// The active connections.
|
||||
/// </summary>
|
||||
private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly CancellationTokenRegistration _shutdownCallback;
|
||||
private readonly ConcurrentDictionary<string, SessionInfo> _activeConnections
|
||||
= new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private Timer _idleTimer;
|
||||
|
||||
@ -79,7 +78,8 @@ namespace Emby.Server.Implementations.Session
|
||||
IImageProcessor imageProcessor,
|
||||
IServerApplicationHost appHost,
|
||||
IDeviceManager deviceManager,
|
||||
IMediaSourceManager mediaSourceManager)
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IHostApplicationLifetime hostApplicationLifetime)
|
||||
{
|
||||
_logger = logger;
|
||||
_eventManager = eventManager;
|
||||
@ -92,6 +92,7 @@ namespace Emby.Server.Implementations.Session
|
||||
_appHost = appHost;
|
||||
_deviceManager = deviceManager;
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_shutdownCallback = hostApplicationLifetime.ApplicationStopping.Register(OnApplicationStopping);
|
||||
|
||||
_deviceManager.DeviceOptionsUpdated += OnDeviceManagerDeviceOptionsUpdated;
|
||||
}
|
||||
@ -151,36 +152,6 @@ namespace Emby.Server.Implementations.Session
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and optionally managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
_idleTimer?.Dispose();
|
||||
}
|
||||
|
||||
_idleTimer = null;
|
||||
|
||||
_deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private void CheckDisposed()
|
||||
{
|
||||
if (_disposed)
|
||||
@ -980,28 +951,28 @@ namespace Emby.Server.Implementations.Session
|
||||
|
||||
private bool OnPlaybackStopped(User user, BaseItem item, long? positionTicks, bool playbackFailed)
|
||||
{
|
||||
bool playedToCompletion = false;
|
||||
|
||||
if (!playbackFailed)
|
||||
if (playbackFailed)
|
||||
{
|
||||
var data = _userDataManager.GetUserData(user, item);
|
||||
|
||||
if (positionTicks.HasValue)
|
||||
{
|
||||
playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the client isn't able to report this, then we'll just have to make an assumption
|
||||
data.PlayCount++;
|
||||
data.Played = item.SupportsPlayedStatus;
|
||||
data.PlaybackPositionTicks = 0;
|
||||
playedToCompletion = true;
|
||||
}
|
||||
|
||||
_userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None);
|
||||
return false;
|
||||
}
|
||||
|
||||
var data = _userDataManager.GetUserData(user, item);
|
||||
bool playedToCompletion;
|
||||
if (positionTicks.HasValue)
|
||||
{
|
||||
playedToCompletion = _userDataManager.UpdatePlayState(item, data, positionTicks.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the client isn't able to report this, then we'll just have to make an assumption
|
||||
data.PlayCount++;
|
||||
data.Played = item.SupportsPlayedStatus;
|
||||
data.PlaybackPositionTicks = 0;
|
||||
playedToCompletion = true;
|
||||
}
|
||||
|
||||
_userDataManager.SaveUserData(user, item, data, UserDataSaveReason.PlaybackFinished, CancellationToken.None);
|
||||
|
||||
return playedToCompletion;
|
||||
}
|
||||
|
||||
@ -1330,32 +1301,6 @@ namespace Emby.Server.Implementations.Session
|
||||
return SendMessageToSessions(Sessions, SessionMessageType.RestartRequired, string.Empty, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the server shutdown notification.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task SendServerShutdownNotification(CancellationToken cancellationToken)
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
return SendMessageToSessions(Sessions, SessionMessageType.ServerShuttingDown, string.Empty, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends the server restart notification.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
public Task SendServerRestartNotification(CancellationToken cancellationToken)
|
||||
{
|
||||
CheckDisposed();
|
||||
|
||||
_logger.LogDebug("Beginning SendServerRestartNotification");
|
||||
|
||||
return SendMessageToSessions(Sessions, SessionMessageType.ServerRestarting, string.Empty, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the additional user.
|
||||
/// </summary>
|
||||
@ -1833,5 +1778,53 @@ namespace Emby.Server.Implementations.Session
|
||||
|
||||
return SendMessageToSessions(sessions, name, data, cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var session in _activeConnections.Values)
|
||||
{
|
||||
await session.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (_idleTimer is not null)
|
||||
{
|
||||
await _idleTimer.DisposeAsync().ConfigureAwait(false);
|
||||
_idleTimer = null;
|
||||
}
|
||||
|
||||
await _shutdownCallback.DisposeAsync().ConfigureAwait(false);
|
||||
|
||||
_deviceManager.DeviceOptionsUpdated -= OnDeviceManagerDeviceOptionsUpdated;
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
private async void OnApplicationStopping()
|
||||
{
|
||||
_logger.LogInformation("Sending shutdown notifications");
|
||||
try
|
||||
{
|
||||
var messageType = _appHost.ShouldRestart ? SessionMessageType.ServerRestarting : SessionMessageType.ServerShuttingDown;
|
||||
|
||||
await SendMessageToSessions(Sessions, messageType, string.Empty, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error sending server shutdown notifications");
|
||||
}
|
||||
|
||||
// Close open websockets to allow Kestrel to shut down cleanly
|
||||
foreach (var session in _activeConnections.Values)
|
||||
{
|
||||
await session.DisposeAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_activeConnections.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
109
Emby.Server.Implementations/SystemManager.cs
Normal file
109
Emby.Server.Implementations/SystemManager.cs
Normal file
@ -0,0 +1,109 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Updates;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Model.System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
|
||||
namespace Emby.Server.Implementations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public class SystemManager : ISystemManager
|
||||
{
|
||||
private readonly IHostApplicationLifetime _applicationLifetime;
|
||||
private readonly IServerApplicationHost _applicationHost;
|
||||
private readonly IServerApplicationPaths _applicationPaths;
|
||||
private readonly IServerConfigurationManager _configurationManager;
|
||||
private readonly IStartupOptions _startupOptions;
|
||||
private readonly IInstallationManager _installationManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SystemManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="applicationLifetime">Instance of <see cref="IHostApplicationLifetime"/>.</param>
|
||||
/// <param name="applicationHost">Instance of <see cref="IServerApplicationHost"/>.</param>
|
||||
/// <param name="applicationPaths">Instance of <see cref="IServerApplicationPaths"/>.</param>
|
||||
/// <param name="configurationManager">Instance of <see cref="IServerConfigurationManager"/>.</param>
|
||||
/// <param name="startupOptions">Instance of <see cref="IStartupOptions"/>.</param>
|
||||
/// <param name="installationManager">Instance of <see cref="IInstallationManager"/>.</param>
|
||||
public SystemManager(
|
||||
IHostApplicationLifetime applicationLifetime,
|
||||
IServerApplicationHost applicationHost,
|
||||
IServerApplicationPaths applicationPaths,
|
||||
IServerConfigurationManager configurationManager,
|
||||
IStartupOptions startupOptions,
|
||||
IInstallationManager installationManager)
|
||||
{
|
||||
_applicationLifetime = applicationLifetime;
|
||||
_applicationHost = applicationHost;
|
||||
_applicationPaths = applicationPaths;
|
||||
_configurationManager = configurationManager;
|
||||
_startupOptions = startupOptions;
|
||||
_installationManager = installationManager;
|
||||
}
|
||||
|
||||
private bool CanLaunchWebBrowser => Environment.UserInteractive
|
||||
&& !_startupOptions.IsService
|
||||
&& (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS());
|
||||
|
||||
/// <inheritdoc />
|
||||
public SystemInfo GetSystemInfo(HttpRequest request)
|
||||
{
|
||||
return new SystemInfo
|
||||
{
|
||||
HasPendingRestart = _applicationHost.HasPendingRestart,
|
||||
IsShuttingDown = _applicationLifetime.ApplicationStopping.IsCancellationRequested,
|
||||
Version = _applicationHost.ApplicationVersionString,
|
||||
WebSocketPortNumber = _applicationHost.HttpPort,
|
||||
CompletedInstallations = _installationManager.CompletedInstallations.ToArray(),
|
||||
Id = _applicationHost.SystemId,
|
||||
ProgramDataPath = _applicationPaths.ProgramDataPath,
|
||||
WebPath = _applicationPaths.WebPath,
|
||||
LogPath = _applicationPaths.LogDirectoryPath,
|
||||
ItemsByNamePath = _applicationPaths.InternalMetadataPath,
|
||||
InternalMetadataPath = _applicationPaths.InternalMetadataPath,
|
||||
CachePath = _applicationPaths.CachePath,
|
||||
CanLaunchWebBrowser = CanLaunchWebBrowser,
|
||||
TranscodingTempPath = _configurationManager.GetTranscodePath(),
|
||||
ServerName = _applicationHost.FriendlyName,
|
||||
LocalAddress = _applicationHost.GetSmartApiUrl(request),
|
||||
SupportsLibraryMonitor = true,
|
||||
PackageName = _startupOptions.PackageName,
|
||||
CastReceiverApplications = _configurationManager.Configuration.CastReceiverApplications
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public PublicSystemInfo GetPublicSystemInfo(HttpRequest request)
|
||||
{
|
||||
return new PublicSystemInfo
|
||||
{
|
||||
Version = _applicationHost.ApplicationVersionString,
|
||||
ProductName = _applicationHost.Name,
|
||||
Id = _applicationHost.SystemId,
|
||||
ServerName = _applicationHost.FriendlyName,
|
||||
LocalAddress = _applicationHost.GetSmartApiUrl(request),
|
||||
StartupWizardCompleted = _configurationManager.CommonConfiguration.IsStartupWizardCompleted
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Restart() => ShutdownInternal(true);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Shutdown() => ShutdownInternal(false);
|
||||
|
||||
private void ShutdownInternal(bool restart)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
_applicationHost.ShouldRestart = restart;
|
||||
_applicationLifetime.StopApplication();
|
||||
});
|
||||
}
|
||||
}
|
@ -504,8 +504,7 @@ namespace Emby.Server.Implementations.Updates
|
||||
|
||||
private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken)
|
||||
{
|
||||
var extension = Path.GetExtension(package.SourceUrl);
|
||||
if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase))
|
||||
if (!Path.GetExtension(package.SourceUrl.AsSpan()).Equals(".zip", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl);
|
||||
return;
|
||||
|
@ -45,6 +45,8 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
private const string DefaultEventEncoderPreset = "superfast";
|
||||
private const TranscodingJobType TranscodingJobType = MediaBrowser.Controller.MediaEncoding.TranscodingJobType.Hls;
|
||||
|
||||
private readonly Version _minFFmpegFlacInMp4 = new Version(6, 0);
|
||||
|
||||
private readonly ILibraryManager _libraryManager;
|
||||
private readonly IUserManager _userManager;
|
||||
private readonly IDlnaManager _dlnaManager;
|
||||
@ -1705,16 +1707,31 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
var audioCodec = _encodingHelper.GetAudioEncoder(state);
|
||||
var bitStreamArgs = EncodingHelper.GetAudioBitStreamArguments(state, state.Request.SegmentContainer, state.MediaSource.Container);
|
||||
|
||||
// opus, dts, truehd and flac (in FFmpeg 5 and older) are experimental in mp4 muxer
|
||||
var strictArgs = string.Empty;
|
||||
var actualOutputAudioCodec = state.ActualOutputAudioCodec;
|
||||
if (string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase)
|
||||
|| (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
|
||||
&& _mediaEncoder.EncoderVersion < _minFFmpegFlacInMp4))
|
||||
{
|
||||
strictArgs = " -strict -2";
|
||||
}
|
||||
|
||||
if (!state.IsOutputVideo)
|
||||
{
|
||||
if (EncodingHelper.IsCopyCodec(audioCodec))
|
||||
{
|
||||
return "-acodec copy -strict -2" + bitStreamArgs;
|
||||
}
|
||||
|
||||
var audioTranscodeParams = string.Empty;
|
||||
|
||||
audioTranscodeParams += "-acodec " + audioCodec + bitStreamArgs;
|
||||
// -vn to drop any video streams
|
||||
audioTranscodeParams += "-vn";
|
||||
|
||||
if (EncodingHelper.IsCopyCodec(audioCodec))
|
||||
{
|
||||
return audioTranscodeParams + " -acodec copy" + bitStreamArgs + strictArgs;
|
||||
}
|
||||
|
||||
audioTranscodeParams += " -acodec " + audioCodec + bitStreamArgs + strictArgs;
|
||||
|
||||
var audioBitrate = state.OutputAudioBitrate;
|
||||
var audioChannels = state.OutputAudioChannels;
|
||||
@ -1742,21 +1759,9 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
audioTranscodeParams += " -ar " + state.OutputAudioSampleRate.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
audioTranscodeParams += " -vn";
|
||||
return audioTranscodeParams;
|
||||
}
|
||||
|
||||
// dts, flac, opus and truehd are experimental in mp4 muxer
|
||||
var strictArgs = string.Empty;
|
||||
var actualOutputAudioCodec = state.ActualOutputAudioCodec;
|
||||
if (string.Equals(actualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(actualOutputAudioCodec, "opus", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(actualOutputAudioCodec, "dts", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(actualOutputAudioCodec, "truehd", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
strictArgs = " -strict -2";
|
||||
}
|
||||
|
||||
if (EncodingHelper.IsCopyCodec(audioCodec))
|
||||
{
|
||||
var videoCodec = _encodingHelper.GetVideoEncoder(state, _encodingOptions);
|
||||
@ -2041,9 +2046,9 @@ public class DynamicHlsController : BaseJellyfinApiController
|
||||
return null;
|
||||
}
|
||||
|
||||
var playlistFilename = Path.GetFileNameWithoutExtension(playlist);
|
||||
var playlistFilename = Path.GetFileNameWithoutExtension(playlist.AsSpan());
|
||||
|
||||
var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length);
|
||||
var indexString = Path.GetFileNameWithoutExtension(file.Name.AsSpan()).Slice(playlistFilename.Length);
|
||||
|
||||
return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ public class HlsSegmentController : BaseJellyfinApiController
|
||||
public ActionResult GetHlsAudioSegmentLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string segmentId)
|
||||
{
|
||||
// TODO: Deprecate with new iOS app
|
||||
var file = segmentId + Path.GetExtension(Request.Path);
|
||||
var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
|
||||
var transcodePath = _serverConfigurationManager.GetTranscodePath();
|
||||
file = Path.GetFullPath(Path.Combine(transcodePath, file));
|
||||
var fileDir = Path.GetDirectoryName(file);
|
||||
@ -85,11 +85,12 @@ public class HlsSegmentController : BaseJellyfinApiController
|
||||
[SuppressMessage("Microsoft.Performance", "CA1801:ReviewUnusedParameters", MessageId = "itemId", Justification = "Required for ServiceStack")]
|
||||
public ActionResult GetHlsPlaylistLegacy([FromRoute, Required] string itemId, [FromRoute, Required] string playlistId)
|
||||
{
|
||||
var file = playlistId + Path.GetExtension(Request.Path);
|
||||
var file = string.Concat(playlistId, Path.GetExtension(Request.Path.Value.AsSpan()));
|
||||
var transcodePath = _serverConfigurationManager.GetTranscodePath();
|
||||
file = Path.GetFullPath(Path.Combine(transcodePath, file));
|
||||
var fileDir = Path.GetDirectoryName(file);
|
||||
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8")
|
||||
if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture)
|
||||
|| Path.GetExtension(file.AsSpan()).Equals(".m3u8", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return BadRequest("Invalid segment.");
|
||||
}
|
||||
@ -138,7 +139,7 @@ public class HlsSegmentController : BaseJellyfinApiController
|
||||
[FromRoute, Required] string segmentId,
|
||||
[FromRoute, Required] string segmentContainer)
|
||||
{
|
||||
var file = segmentId + Path.GetExtension(Request.Path);
|
||||
var file = string.Concat(segmentId, Path.GetExtension(Request.Path.Value.AsSpan()));
|
||||
var transcodeFolderPath = _serverConfigurationManager.GetTranscodePath();
|
||||
|
||||
file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file));
|
||||
|
@ -7,6 +7,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
@ -78,6 +79,9 @@ public class ImageController : BaseJellyfinApiController
|
||||
_appPaths = appPaths;
|
||||
}
|
||||
|
||||
private static Stream GetFromBase64Stream(Stream inputStream)
|
||||
=> new CryptoStream(inputStream, new FromBase64Transform(), CryptoStreamMode.Read);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the user image.
|
||||
/// </summary>
|
||||
@ -116,8 +120,8 @@ public class ImageController : BaseJellyfinApiController
|
||||
return BadRequest("Incorrect ContentType.");
|
||||
}
|
||||
|
||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||
await using (memoryStream.ConfigureAwait(false))
|
||||
var stream = GetFromBase64Stream(Request.Body);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
// Handle image/png; charset=utf-8
|
||||
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||
@ -130,7 +134,7 @@ public class ImageController : BaseJellyfinApiController
|
||||
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
|
||||
|
||||
await _providerManager
|
||||
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
|
||||
.SaveImage(stream, mimeType, user.ProfileImage.Path)
|
||||
.ConfigureAwait(false);
|
||||
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
||||
|
||||
@ -176,8 +180,8 @@ public class ImageController : BaseJellyfinApiController
|
||||
return BadRequest("Incorrect ContentType.");
|
||||
}
|
||||
|
||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||
await using (memoryStream.ConfigureAwait(false))
|
||||
var stream = GetFromBase64Stream(Request.Body);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
// Handle image/png; charset=utf-8
|
||||
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||
@ -190,7 +194,7 @@ public class ImageController : BaseJellyfinApiController
|
||||
user.ProfileImage = new Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + extension));
|
||||
|
||||
await _providerManager
|
||||
.SaveImage(memoryStream, mimeType, user.ProfileImage.Path)
|
||||
.SaveImage(stream, mimeType, user.ProfileImage.Path)
|
||||
.ConfigureAwait(false);
|
||||
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
|
||||
|
||||
@ -372,12 +376,12 @@ public class ImageController : BaseJellyfinApiController
|
||||
return BadRequest("Incorrect ContentType.");
|
||||
}
|
||||
|
||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||
await using (memoryStream.ConfigureAwait(false))
|
||||
var stream = GetFromBase64Stream(Request.Body);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
// Handle image/png; charset=utf-8
|
||||
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
||||
await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
||||
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return NoContent();
|
||||
@ -416,12 +420,12 @@ public class ImageController : BaseJellyfinApiController
|
||||
return BadRequest("Incorrect ContentType.");
|
||||
}
|
||||
|
||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||
await using (memoryStream.ConfigureAwait(false))
|
||||
var stream = GetFromBase64Stream(Request.Body);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
// Handle image/png; charset=utf-8
|
||||
var mimeType = Request.ContentType?.Split(';').FirstOrDefault();
|
||||
await _providerManager.SaveImage(item, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
||||
await _providerManager.SaveImage(item, stream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
|
||||
await item.UpdateToRepositoryAsync(ItemUpdateType.ImageUpdate, CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
return NoContent();
|
||||
@ -1792,8 +1796,8 @@ public class ImageController : BaseJellyfinApiController
|
||||
return BadRequest("Incorrect ContentType.");
|
||||
}
|
||||
|
||||
var memoryStream = await GetMemoryStream(Request.Body).ConfigureAwait(false);
|
||||
await using (memoryStream.ConfigureAwait(false))
|
||||
var stream = GetFromBase64Stream(Request.Body);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
var filePath = Path.Combine(_appPaths.DataPath, "splashscreen-upload" + extension);
|
||||
var brandingOptions = _serverConfigurationManager.GetConfiguration<BrandingOptions>("branding");
|
||||
@ -1803,7 +1807,7 @@ public class ImageController : BaseJellyfinApiController
|
||||
var fs = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous);
|
||||
await using (fs.ConfigureAwait(false))
|
||||
{
|
||||
await memoryStream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
|
||||
await stream.CopyToAsync(fs, CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return NoContent();
|
||||
@ -1833,15 +1837,6 @@ public class ImageController : BaseJellyfinApiController
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
|
||||
{
|
||||
using var reader = new StreamReader(inputStream);
|
||||
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
|
||||
|
||||
var bytes = Convert.FromBase64String(text);
|
||||
return new MemoryStream(bytes, 0, bytes.Length, false, true);
|
||||
}
|
||||
|
||||
private ImageInfo? GetImageInfo(BaseItem item, ItemImageInfo info, int? imageIndex)
|
||||
{
|
||||
int? width = null;
|
||||
|
@ -23,7 +23,6 @@ using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Session;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
@ -48,7 +47,6 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
private readonly IMediaSourceManager _mediaSourceManager;
|
||||
private readonly IConfigurationManager _configurationManager;
|
||||
private readonly TranscodingJobHelper _transcodingJobHelper;
|
||||
private readonly ISessionManager _sessionManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LiveTvController"/> class.
|
||||
@ -61,7 +59,6 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
/// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
|
||||
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
|
||||
/// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/> class.</param>
|
||||
/// <param name="sessionManager">Instance of the <see cref="ISessionManager"/> interface.</param>
|
||||
public LiveTvController(
|
||||
ILiveTvManager liveTvManager,
|
||||
IUserManager userManager,
|
||||
@ -70,8 +67,7 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
IDtoService dtoService,
|
||||
IMediaSourceManager mediaSourceManager,
|
||||
IConfigurationManager configurationManager,
|
||||
TranscodingJobHelper transcodingJobHelper,
|
||||
ISessionManager sessionManager)
|
||||
TranscodingJobHelper transcodingJobHelper)
|
||||
{
|
||||
_liveTvManager = liveTvManager;
|
||||
_userManager = userManager;
|
||||
@ -81,7 +77,6 @@ public class LiveTvController : BaseJellyfinApiController
|
||||
_mediaSourceManager = mediaSourceManager;
|
||||
_configurationManager = configurationManager;
|
||||
_transcodingJobHelper = transcodingJobHelper;
|
||||
_sessionManager = sessionManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -6,6 +6,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@ -405,9 +406,8 @@ public class SubtitleController : BaseJellyfinApiController
|
||||
[FromBody, Required] UploadSubtitleDto body)
|
||||
{
|
||||
var video = (Video)_libraryManager.GetItemById(itemId);
|
||||
var data = Convert.FromBase64String(body.Data);
|
||||
var memoryStream = new MemoryStream(data, 0, data.Length, false, true);
|
||||
await using (memoryStream.ConfigureAwait(false))
|
||||
var stream = new CryptoStream(Request.Body, new FromBase64Transform(), CryptoStreamMode.Read);
|
||||
await using (stream.ConfigureAwait(false))
|
||||
{
|
||||
await _subtitleManager.UploadSubtitle(
|
||||
video,
|
||||
@ -417,7 +417,7 @@ public class SubtitleController : BaseJellyfinApiController
|
||||
Language = body.Language,
|
||||
IsForced = body.IsForced,
|
||||
IsHearingImpaired = body.IsHearingImpaired,
|
||||
Stream = memoryStream
|
||||
Stream = stream
|
||||
}).ConfigureAwait(false);
|
||||
_providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
|
||||
|
||||
|
@ -4,14 +4,12 @@ using System.ComponentModel.DataAnnotations;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Api.Attributes;
|
||||
using Jellyfin.Api.Constants;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Common.Extensions;
|
||||
using MediaBrowser.Common.Net;
|
||||
using MediaBrowser.Controller;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Net;
|
||||
using MediaBrowser.Model.System;
|
||||
@ -27,32 +25,36 @@ namespace Jellyfin.Api.Controllers;
|
||||
/// </summary>
|
||||
public class SystemController : BaseJellyfinApiController
|
||||
{
|
||||
private readonly ILogger<SystemController> _logger;
|
||||
private readonly IServerApplicationHost _appHost;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
private readonly IFileSystem _fileSystem;
|
||||
private readonly INetworkManager _network;
|
||||
private readonly ILogger<SystemController> _logger;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly ISystemManager _systemManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SystemController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
/// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
|
||||
/// <param name="appPaths">Instance of <see cref="IServerApplicationPaths"/> interface.</param>
|
||||
/// <param name="appHost">Instance of <see cref="IServerApplicationHost"/> interface.</param>
|
||||
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
|
||||
/// <param name="network">Instance of <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="logger">Instance of <see cref="ILogger{SystemController}"/> interface.</param>
|
||||
/// <param name="networkManager">Instance of <see cref="INetworkManager"/> interface.</param>
|
||||
/// <param name="systemManager">Instance of <see cref="ISystemManager"/> interface.</param>
|
||||
public SystemController(
|
||||
IServerConfigurationManager serverConfigurationManager,
|
||||
ILogger<SystemController> logger,
|
||||
IServerApplicationHost appHost,
|
||||
IServerApplicationPaths appPaths,
|
||||
IFileSystem fileSystem,
|
||||
INetworkManager network,
|
||||
ILogger<SystemController> logger)
|
||||
INetworkManager networkManager,
|
||||
ISystemManager systemManager)
|
||||
{
|
||||
_appPaths = serverConfigurationManager.ApplicationPaths;
|
||||
_appHost = appHost;
|
||||
_fileSystem = fileSystem;
|
||||
_network = network;
|
||||
_logger = logger;
|
||||
_appHost = appHost;
|
||||
_appPaths = appPaths;
|
||||
_fileSystem = fileSystem;
|
||||
_networkManager = networkManager;
|
||||
_systemManager = systemManager;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -66,9 +68,7 @@ public class SystemController : BaseJellyfinApiController
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public ActionResult<SystemInfo> GetSystemInfo()
|
||||
{
|
||||
return _appHost.GetSystemInfo(Request);
|
||||
}
|
||||
=> _systemManager.GetSystemInfo(Request);
|
||||
|
||||
/// <summary>
|
||||
/// Gets public information about the server.
|
||||
@ -78,9 +78,7 @@ public class SystemController : BaseJellyfinApiController
|
||||
[HttpGet("Info/Public")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<PublicSystemInfo> GetPublicSystemInfo()
|
||||
{
|
||||
return _appHost.GetPublicSystemInfo(Request);
|
||||
}
|
||||
=> _systemManager.GetPublicSystemInfo(Request);
|
||||
|
||||
/// <summary>
|
||||
/// Pings the system.
|
||||
@ -91,9 +89,7 @@ public class SystemController : BaseJellyfinApiController
|
||||
[HttpPost("Ping", Name = "PostPingSystem")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<string> PingSystem()
|
||||
{
|
||||
return _appHost.Name;
|
||||
}
|
||||
=> _appHost.Name;
|
||||
|
||||
/// <summary>
|
||||
/// Restarts the application.
|
||||
@ -107,11 +103,7 @@ public class SystemController : BaseJellyfinApiController
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public ActionResult RestartApplication()
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
_appHost.Restart();
|
||||
});
|
||||
_systemManager.Restart();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@ -127,11 +119,7 @@ public class SystemController : BaseJellyfinApiController
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public ActionResult ShutdownApplication()
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(100).ConfigureAwait(false);
|
||||
await _appHost.Shutdown().ConfigureAwait(false);
|
||||
});
|
||||
_systemManager.Shutdown();
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
@ -189,7 +177,7 @@ public class SystemController : BaseJellyfinApiController
|
||||
return new EndPointInfo
|
||||
{
|
||||
IsLocal = HttpContext.IsLocal(),
|
||||
IsInNetwork = _network.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP())
|
||||
IsInNetwork = _networkManager.IsInLocalNetwork(HttpContext.GetNormalizedRemoteIP())
|
||||
};
|
||||
}
|
||||
|
||||
@ -227,7 +215,7 @@ public class SystemController : BaseJellyfinApiController
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
public ActionResult<IEnumerable<WakeOnLanInfo>> GetWakeOnLanInfo()
|
||||
{
|
||||
var result = _network.GetMacAddresses()
|
||||
var result = _networkManager.GetMacAddresses()
|
||||
.Select(i => new WakeOnLanInfo(i));
|
||||
return Ok(result);
|
||||
}
|
||||
|
@ -200,13 +200,6 @@ public class DynamicHlsHelper
|
||||
|
||||
if (state.VideoStream is not null && state.VideoRequest is not null)
|
||||
{
|
||||
// Provide a workaround for the case issue between flac and fLaC.
|
||||
var flacWaPlaylist = ApplyFlacCaseWorkaround(state, basicPlaylist.ToString());
|
||||
if (!string.IsNullOrEmpty(flacWaPlaylist))
|
||||
{
|
||||
builder.Append(flacWaPlaylist);
|
||||
}
|
||||
|
||||
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
|
||||
|
||||
// Provide SDR HEVC entrance for backward compatibility.
|
||||
@ -236,14 +229,7 @@ public class DynamicHlsHelper
|
||||
}
|
||||
|
||||
var sdrTotalBitrate = sdrOutputAudioBitrate + sdrOutputVideoBitrate;
|
||||
var sdrPlaylist = AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
|
||||
|
||||
// Provide a workaround for the case issue between flac and fLaC.
|
||||
flacWaPlaylist = ApplyFlacCaseWorkaround(state, sdrPlaylist.ToString());
|
||||
if (!string.IsNullOrEmpty(flacWaPlaylist))
|
||||
{
|
||||
builder.Append(flacWaPlaylist);
|
||||
}
|
||||
AppendPlaylist(builder, state, sdrVideoUrl, sdrTotalBitrate, subtitleGroup);
|
||||
|
||||
// Restore the video codec
|
||||
state.OutputVideoCodec = "copy";
|
||||
@ -274,13 +260,6 @@ public class DynamicHlsHelper
|
||||
state.VideoStream.Level = originalLevel;
|
||||
var newPlaylist = ReplacePlaylistCodecsField(basicPlaylist, playlistCodecsField, newPlaylistCodecsField);
|
||||
builder.Append(newPlaylist);
|
||||
|
||||
// Provide a workaround for the case issue between flac and fLaC.
|
||||
flacWaPlaylist = ApplyFlacCaseWorkaround(state, newPlaylist);
|
||||
if (!string.IsNullOrEmpty(flacWaPlaylist))
|
||||
{
|
||||
builder.Append(flacWaPlaylist);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -767,16 +746,4 @@ public class DynamicHlsHelper
|
||||
newValue.ToString(),
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private string ApplyFlacCaseWorkaround(StreamState state, string srcPlaylist)
|
||||
{
|
||||
if (!string.Equals(state.ActualOutputAudioCodec, "flac", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var newPlaylist = srcPlaylist.Replace(",flac\"", ",fLaC\"", StringComparison.Ordinal);
|
||||
|
||||
return newPlaylist.Contains(",fLaC\"", StringComparison.Ordinal) ? newPlaylist : string.Empty;
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,9 @@ using System.Text;
|
||||
namespace Jellyfin.Api.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Hls Codec string helpers.
|
||||
/// Helpers to generate HLS codec strings according to
|
||||
/// <a href="https://datatracker.ietf.org/doc/html/rfc6381#section-3.3">RFC 6381 section 3.3</a>
|
||||
/// and the <a href="https://mp4ra.org">MP4 Registration Authority</a>.
|
||||
/// </summary>
|
||||
public static class HlsCodecStringHelpers
|
||||
{
|
||||
@ -27,7 +29,7 @@ public static class HlsCodecStringHelpers
|
||||
/// <summary>
|
||||
/// Codec name for FLAC.
|
||||
/// </summary>
|
||||
public const string FLAC = "flac";
|
||||
public const string FLAC = "fLaC";
|
||||
|
||||
/// <summary>
|
||||
/// Codec name for ALAC.
|
||||
@ -37,7 +39,7 @@ public static class HlsCodecStringHelpers
|
||||
/// <summary>
|
||||
/// Codec name for OPUS.
|
||||
/// </summary>
|
||||
public const string OPUS = "opus";
|
||||
public const string OPUS = "Opus";
|
||||
|
||||
/// <summary>
|
||||
/// Gets a MP3 codec string.
|
||||
|
@ -191,6 +191,11 @@ public static class StreamingHelpers
|
||||
state.OutputAudioBitrate = encodingHelper.GetAudioBitrateParam(streamingRequest.AudioBitRate, streamingRequest.AudioCodec, state.AudioStream, state.OutputAudioChannels) ?? 0;
|
||||
}
|
||||
|
||||
if (outputAudioCodec.StartsWith("pcm_", StringComparison.Ordinal))
|
||||
{
|
||||
containerInternal = ".pcm";
|
||||
}
|
||||
|
||||
state.OutputAudioCodec = outputAudioCodec;
|
||||
state.OutputContainer = (containerInternal ?? string.Empty).TrimStart('.');
|
||||
state.OutputAudioChannels = encodingHelper.GetNumAudioChannelsParam(state, state.AudioStream, state.OutputAudioCodec);
|
||||
@ -243,7 +248,7 @@ public static class StreamingHelpers
|
||||
? GetOutputFileExtension(state, mediaSource)
|
||||
: ("." + state.OutputContainer);
|
||||
|
||||
state.OutputFilePath = GetOutputFilePath(state, ext!, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
|
||||
state.OutputFilePath = GetOutputFilePath(state, ext, serverConfigurationManager, streamingRequest.DeviceId, streamingRequest.PlaySessionId);
|
||||
|
||||
return state;
|
||||
}
|
||||
@ -418,11 +423,11 @@ public static class StreamingHelpers
|
||||
/// <returns>System.String.</returns>
|
||||
private static string? GetOutputFileExtension(StreamState state, MediaSourceInfo? mediaSource)
|
||||
{
|
||||
var ext = Path.GetExtension(state.RequestedUrl);
|
||||
var ext = Path.GetExtension(state.RequestedUrl.AsSpan());
|
||||
|
||||
if (!string.IsNullOrEmpty(ext))
|
||||
if (ext.IsEmpty)
|
||||
{
|
||||
return ext;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Try to infer based on the desired video codec
|
||||
@ -504,7 +509,7 @@ public static class StreamingHelpers
|
||||
/// <param name="deviceId">The device id.</param>
|
||||
/// <param name="playSessionId">The play session id.</param>
|
||||
/// <returns>The complete file path, including the folder, for the transcoding file.</returns>
|
||||
private static string GetOutputFilePath(StreamState state, string outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId)
|
||||
private static string GetOutputFilePath(StreamState state, string? outputFileExtension, IServerConfigurationManager serverConfigurationManager, string? deviceId, string? playSessionId)
|
||||
{
|
||||
var data = $"{state.MediaPath}-{state.UserAgent}-{deviceId!}-{playSessionId!}";
|
||||
|
||||
|
@ -538,7 +538,7 @@ public class TranscodingJobHelper : IDisposable
|
||||
await _attachmentExtractor.ExtractAllAttachments(state.MediaPath, state.MediaSource, attachmentPath, cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (state.SubtitleStream.IsExternal && string.Equals(Path.GetExtension(state.SubtitleStream.Path), ".mks", StringComparison.OrdinalIgnoreCase))
|
||||
if (state.SubtitleStream.IsExternal && Path.GetExtension(state.SubtitleStream.Path.AsSpan()).Equals(".mks", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string subtitlePath = state.SubtitleStream.Path;
|
||||
string subtitlePathArgument = string.Format(CultureInfo.InvariantCulture, "file:\"{0}\"", subtitlePath.Replace("\"", "\\\"", StringComparison.Ordinal));
|
||||
|
@ -33,8 +33,7 @@ public class RobotsRedirectionMiddleware
|
||||
/// <returns>The async task.</returns>
|
||||
public async Task Invoke(HttpContext httpContext)
|
||||
{
|
||||
var localPath = httpContext.Request.Path.ToString();
|
||||
if (string.Equals(localPath, "/robots.txt", StringComparison.OrdinalIgnoreCase))
|
||||
if (httpContext.Request.Path.Equals("/robots.txt", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_logger.LogDebug("Redirecting robots.txt request to web/robots.txt");
|
||||
httpContext.Response.Redirect("web/robots.txt");
|
||||
|
@ -94,4 +94,40 @@ public enum PersonKind
|
||||
/// A person who was the illustrator.
|
||||
/// </summary>
|
||||
Illustrator,
|
||||
|
||||
/// <summary>
|
||||
/// A person responsible for drawing the art.
|
||||
/// </summary>
|
||||
Penciller,
|
||||
|
||||
/// <summary>
|
||||
/// A person responsible for inking the pencil art.
|
||||
/// </summary>
|
||||
Inker,
|
||||
|
||||
/// <summary>
|
||||
/// A person responsible for applying color to drawings.
|
||||
/// </summary>
|
||||
Colorist,
|
||||
|
||||
/// <summary>
|
||||
/// A person responsible for drawing text and speech bubbles.
|
||||
/// </summary>
|
||||
Letterer,
|
||||
|
||||
/// <summary>
|
||||
/// A person responsible for drawing the cover art.
|
||||
/// </summary>
|
||||
CoverArtist,
|
||||
|
||||
/// <summary>
|
||||
/// A person contributing to a resource by revising or elucidating the content, e.g., adding an introduction, notes, or other critical matter.
|
||||
/// An editor may also prepare a resource for production, publication, or distribution.
|
||||
/// </summary>
|
||||
Editor,
|
||||
|
||||
/// <summary>
|
||||
/// A person who renders a text from one language into another.
|
||||
/// </summary>
|
||||
Translator
|
||||
}
|
||||
|
@ -49,14 +49,13 @@ namespace Jellyfin.Server.Implementations.Security
|
||||
/// <summary>
|
||||
/// Gets the authorization.
|
||||
/// </summary>
|
||||
/// <param name="httpReq">The HTTP req.</param>
|
||||
/// <param name="httpContext">The HTTP context.</param>
|
||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||
private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpReq)
|
||||
private async Task<AuthorizationInfo> GetAuthorization(HttpContext httpContext)
|
||||
{
|
||||
var auth = GetAuthorizationDictionary(httpReq);
|
||||
var authInfo = await GetAuthorizationInfoFromDictionary(auth, httpReq.Request.Headers, httpReq.Request.Query).ConfigureAwait(false);
|
||||
var authInfo = await GetAuthorizationInfo(httpContext.Request).ConfigureAwait(false);
|
||||
|
||||
httpReq.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
|
||||
httpContext.Request.HttpContext.Items["AuthorizationInfo"] = authInfo;
|
||||
return authInfo;
|
||||
}
|
||||
|
||||
@ -80,7 +79,6 @@ namespace Jellyfin.Server.Implementations.Security
|
||||
auth.TryGetValue("Token", out token);
|
||||
}
|
||||
|
||||
#pragma warning disable CA1508 // string.IsNullOrEmpty(token) is always false.
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
token = headers["X-Emby-Token"];
|
||||
@ -118,7 +116,6 @@ namespace Jellyfin.Server.Implementations.Security
|
||||
// Request doesn't contain a token.
|
||||
return authInfo;
|
||||
}
|
||||
#pragma warning restore CA1508
|
||||
|
||||
authInfo.HasToken = true;
|
||||
var dbContext = await _jellyfinDbProvider.CreateDbContextAsync().ConfigureAwait(false);
|
||||
@ -219,24 +216,7 @@ namespace Jellyfin.Server.Implementations.Security
|
||||
/// <summary>
|
||||
/// Gets the auth.
|
||||
/// </summary>
|
||||
/// <param name="httpReq">The HTTP req.</param>
|
||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||
private static Dictionary<string, string>? GetAuthorizationDictionary(HttpContext httpReq)
|
||||
{
|
||||
var auth = httpReq.Request.Headers["X-Emby-Authorization"];
|
||||
|
||||
if (string.IsNullOrEmpty(auth))
|
||||
{
|
||||
auth = httpReq.Request.Headers[HeaderNames.Authorization];
|
||||
}
|
||||
|
||||
return auth.Count > 0 ? GetAuthorization(auth[0]) : null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the auth.
|
||||
/// </summary>
|
||||
/// <param name="httpReq">The HTTP req.</param>
|
||||
/// <param name="httpReq">The HTTP request.</param>
|
||||
/// <returns>Dictionary{System.StringSystem.String}.</returns>
|
||||
private static Dictionary<string, string>? GetAuthorizationDictionary(HttpRequest httpReq)
|
||||
{
|
||||
|
@ -21,7 +21,6 @@ using MediaBrowser.Controller.Events;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Net;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.Cryptography;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Users;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -36,7 +35,6 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
{
|
||||
private readonly IDbContextFactory<JellyfinDbContext> _dbProvider;
|
||||
private readonly IEventManager _eventManager;
|
||||
private readonly ICryptoProvider _cryptoProvider;
|
||||
private readonly INetworkManager _networkManager;
|
||||
private readonly IApplicationHost _appHost;
|
||||
private readonly IImageProcessor _imageProcessor;
|
||||
@ -55,7 +53,6 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
/// </summary>
|
||||
/// <param name="dbProvider">The database provider.</param>
|
||||
/// <param name="eventManager">The event manager.</param>
|
||||
/// <param name="cryptoProvider">The cryptography provider.</param>
|
||||
/// <param name="networkManager">The network manager.</param>
|
||||
/// <param name="appHost">The application host.</param>
|
||||
/// <param name="imageProcessor">The image processor.</param>
|
||||
@ -64,7 +61,6 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
public UserManager(
|
||||
IDbContextFactory<JellyfinDbContext> dbProvider,
|
||||
IEventManager eventManager,
|
||||
ICryptoProvider cryptoProvider,
|
||||
INetworkManager networkManager,
|
||||
IApplicationHost appHost,
|
||||
IImageProcessor imageProcessor,
|
||||
@ -73,7 +69,6 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
{
|
||||
_dbProvider = dbProvider;
|
||||
_eventManager = eventManager;
|
||||
_cryptoProvider = cryptoProvider;
|
||||
_networkManager = networkManager;
|
||||
_appHost = appHost;
|
||||
_imageProcessor = imageProcessor;
|
||||
@ -393,7 +388,7 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
}
|
||||
|
||||
var user = Users.FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
|
||||
var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint)
|
||||
var authResult = await AuthenticateLocalUser(username, password, user)
|
||||
.ConfigureAwait(false);
|
||||
var authenticationProvider = authResult.AuthenticationProvider;
|
||||
var success = authResult.Success;
|
||||
@ -803,8 +798,7 @@ namespace Jellyfin.Server.Implementations.Users
|
||||
private async Task<(IAuthenticationProvider? AuthenticationProvider, string Username, bool Success)> AuthenticateLocalUser(
|
||||
string username,
|
||||
string password,
|
||||
User? user,
|
||||
string remoteEndPoint)
|
||||
User? user)
|
||||
{
|
||||
bool success = false;
|
||||
IAuthenticationProvider? authenticationProvider = null;
|
||||
|
@ -102,9 +102,6 @@ namespace Jellyfin.Server
|
||||
base.RegisterServices(serviceCollection);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void RestartInternal() => Program.Restart();
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal()
|
||||
{
|
||||
@ -114,8 +111,5 @@ namespace Jellyfin.Server
|
||||
// Jellyfin.Server.Implementations
|
||||
yield return typeof(JellyfinDbContext).Assembly;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void ShutdownInternal() => Program.Shutdown();
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,7 @@ namespace Jellyfin.Server.Extensions
|
||||
serviceCollection.AddSingleton<IAuthorizationHandler, FirstTimeSetupHandler>();
|
||||
serviceCollection.AddSingleton<IAuthorizationHandler, AnonymousLanAccessHandler>();
|
||||
serviceCollection.AddSingleton<IAuthorizationHandler, SyncPlayAccessHandler>();
|
||||
serviceCollection.AddSingleton<IAuthorizationHandler, LocalAccessOrRequiresElevationHandler>();
|
||||
|
||||
return serviceCollection.AddAuthorizationCore(options =>
|
||||
{
|
||||
|
@ -73,8 +73,7 @@ namespace Jellyfin.Server.Migrations.Routines
|
||||
var queryResult = connection.Query("SELECT DISTINCT OfficialRating FROM TypedBaseItems");
|
||||
foreach (var entry in queryResult)
|
||||
{
|
||||
var ratingString = entry.GetString(0);
|
||||
if (string.IsNullOrEmpty(ratingString))
|
||||
if (!entry.TryGetString(0, out var ratingString) || string.IsNullOrEmpty(ratingString))
|
||||
{
|
||||
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating IS NULL OR OfficialRating='';");
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CommandLine;
|
||||
using Emby.Server.Implementations;
|
||||
@ -42,7 +41,6 @@ namespace Jellyfin.Server
|
||||
public const string LoggingConfigFileSystem = "logging.json";
|
||||
|
||||
private static readonly ILoggerFactory _loggerFactory = new SerilogLoggerFactory();
|
||||
private static CancellationTokenSource _tokenSource = new();
|
||||
private static long _startTimestamp;
|
||||
private static ILogger _logger = NullLogger.Instance;
|
||||
private static bool _restartOnShutdown;
|
||||
@ -65,36 +63,9 @@ namespace Jellyfin.Server
|
||||
.MapResult(StartApp, ErrorParsingArguments);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shuts down the application.
|
||||
/// </summary>
|
||||
internal static void Shutdown()
|
||||
{
|
||||
if (!_tokenSource.IsCancellationRequested)
|
||||
{
|
||||
_tokenSource.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restarts the application.
|
||||
/// </summary>
|
||||
internal static void Restart()
|
||||
{
|
||||
_restartOnShutdown = true;
|
||||
|
||||
Shutdown();
|
||||
}
|
||||
|
||||
private static async Task StartApp(StartupOptions options)
|
||||
{
|
||||
_startTimestamp = Stopwatch.GetTimestamp();
|
||||
|
||||
// Log all uncaught exceptions to std error
|
||||
static void UnhandledExceptionToConsole(object sender, UnhandledExceptionEventArgs e) =>
|
||||
Console.Error.WriteLine("Unhandled Exception\n" + e.ExceptionObject);
|
||||
AppDomain.CurrentDomain.UnhandledException += UnhandledExceptionToConsole;
|
||||
|
||||
ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options);
|
||||
|
||||
// $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
|
||||
@ -112,38 +83,10 @@ namespace Jellyfin.Server
|
||||
StartupHelpers.InitializeLoggingFramework(startupConfig, appPaths);
|
||||
_logger = _loggerFactory.CreateLogger("Main");
|
||||
|
||||
// Log uncaught exceptions to the logging instead of std error
|
||||
AppDomain.CurrentDomain.UnhandledException -= UnhandledExceptionToConsole;
|
||||
// Use the logging framework for uncaught exceptions instead of std error
|
||||
AppDomain.CurrentDomain.UnhandledException += (_, e)
|
||||
=> _logger.LogCritical((Exception)e.ExceptionObject, "Unhandled Exception");
|
||||
|
||||
// Intercept Ctrl+C and Ctrl+Break
|
||||
Console.CancelKeyPress += (_, e) =>
|
||||
{
|
||||
if (_tokenSource.IsCancellationRequested)
|
||||
{
|
||||
return; // Already shutting down
|
||||
}
|
||||
|
||||
e.Cancel = true;
|
||||
_logger.LogInformation("Ctrl+C, shutting down");
|
||||
Environment.ExitCode = 128 + 2;
|
||||
Shutdown();
|
||||
};
|
||||
|
||||
// Register a SIGTERM handler
|
||||
AppDomain.CurrentDomain.ProcessExit += (_, _) =>
|
||||
{
|
||||
if (_tokenSource.IsCancellationRequested)
|
||||
{
|
||||
return; // Already shutting down
|
||||
}
|
||||
|
||||
_logger.LogInformation("Received a SIGTERM signal, shutting down");
|
||||
Environment.ExitCode = 128 + 15;
|
||||
Shutdown();
|
||||
};
|
||||
|
||||
_logger.LogInformation(
|
||||
"Jellyfin version: {Version}",
|
||||
Assembly.GetEntryAssembly()!.GetName().Version!.ToString(3));
|
||||
@ -173,12 +116,10 @@ namespace Jellyfin.Server
|
||||
|
||||
do
|
||||
{
|
||||
_restartOnShutdown = false;
|
||||
await StartServer(appPaths, options, startupConfig).ConfigureAwait(false);
|
||||
|
||||
if (_restartOnShutdown)
|
||||
{
|
||||
_tokenSource = new CancellationTokenSource();
|
||||
_startTimestamp = Stopwatch.GetTimestamp();
|
||||
}
|
||||
} while (_restartOnShutdown);
|
||||
@ -186,7 +127,7 @@ namespace Jellyfin.Server
|
||||
|
||||
private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig)
|
||||
{
|
||||
var appHost = new CoreAppHost(
|
||||
using var appHost = new CoreAppHost(
|
||||
appPaths,
|
||||
_loggerFactory,
|
||||
options,
|
||||
@ -196,6 +137,7 @@ namespace Jellyfin.Server
|
||||
try
|
||||
{
|
||||
host = Host.CreateDefaultBuilder()
|
||||
.UseConsoleLifetime()
|
||||
.ConfigureServices(services => appHost.Init(services))
|
||||
.ConfigureWebHostDefaults(webHostBuilder => webHostBuilder.ConfigureWebHostBuilder(appHost, startupConfig, appPaths, _logger))
|
||||
.ConfigureAppConfiguration(config => config.ConfigureAppConfiguration(options, appPaths, startupConfig))
|
||||
@ -210,7 +152,7 @@ namespace Jellyfin.Server
|
||||
|
||||
try
|
||||
{
|
||||
await host.StartAsync(_tokenSource.Token).ConfigureAwait(false);
|
||||
await host.StartAsync().ConfigureAwait(false);
|
||||
|
||||
if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket())
|
||||
{
|
||||
@ -219,22 +161,18 @@ namespace Jellyfin.Server
|
||||
StartupHelpers.SetUnixSocketPermissions(startupConfig, socketPath, _logger);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is not TaskCanceledException)
|
||||
catch (Exception)
|
||||
{
|
||||
_logger.LogError("Kestrel failed to start! This is most likely due to an invalid address or port bind - correct your bind configuration in network.xml and try again");
|
||||
throw;
|
||||
}
|
||||
|
||||
await appHost.RunStartupTasksAsync(_tokenSource.Token).ConfigureAwait(false);
|
||||
await appHost.RunStartupTasksAsync().ConfigureAwait(false);
|
||||
|
||||
_logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp));
|
||||
|
||||
// Block main thread until shutdown
|
||||
await Task.Delay(-1, _tokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// Don't throw on cancellation
|
||||
await host.WaitForShutdownAsync().ConfigureAwait(false);
|
||||
_restartOnShutdown = appHost.ShouldRestart;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -257,7 +195,6 @@ namespace Jellyfin.Server
|
||||
}
|
||||
}
|
||||
|
||||
await appHost.DisposeAsync().ConfigureAwait(false);
|
||||
host?.Dispose();
|
||||
}
|
||||
}
|
||||
|
@ -15,65 +15,13 @@ namespace MediaBrowser.Common.Extensions
|
||||
/// </summary>
|
||||
/// <param name="process">The process to wait for.</param>
|
||||
/// <param name="timeout">The duration to wait before cancelling waiting for the task.</param>
|
||||
/// <returns>True if the task exited normally, false if the timeout elapsed before the process exited.</returns>
|
||||
/// <exception cref="InvalidOperationException">If <see cref="Process.EnableRaisingEvents"/> is not set to true for the process.</exception>
|
||||
public static async Task<bool> WaitForExitAsync(this Process process, TimeSpan timeout)
|
||||
/// <returns>A task that will complete when the process has exited, cancellation has been requested, or an error occurs.</returns>
|
||||
/// <exception cref="OperationCanceledException">The timeout ended.</exception>
|
||||
public static async Task WaitForExitAsync(this Process process, TimeSpan timeout)
|
||||
{
|
||||
using (var cancelTokenSource = new CancellationTokenSource(timeout))
|
||||
{
|
||||
return await WaitForExitAsync(process, cancelTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously wait for the process to exit.
|
||||
/// </summary>
|
||||
/// <param name="process">The process to wait for.</param>
|
||||
/// <param name="cancelToken">A <see cref="CancellationToken"/> to observe while waiting for the process to exit.</param>
|
||||
/// <returns>True if the task exited normally, false if cancelled before the process exited.</returns>
|
||||
public static async Task<bool> WaitForExitAsync(this Process process, CancellationToken cancelToken)
|
||||
{
|
||||
if (!process.EnableRaisingEvents)
|
||||
{
|
||||
throw new InvalidOperationException("EnableRisingEvents must be enabled to async wait for a task to exit.");
|
||||
}
|
||||
|
||||
// Add an event handler for the process exit event
|
||||
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
process.Exited += (_, _) => tcs.TrySetResult(true);
|
||||
|
||||
// Return immediately if the process has already exited
|
||||
if (process.HasExitedSafe())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Register with the cancellation token then await
|
||||
using (var cancelRegistration = cancelToken.Register(() => tcs.TrySetResult(process.HasExitedSafe())))
|
||||
{
|
||||
return await tcs.Task.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether the associated process has been terminated using
|
||||
/// <see cref="Process.HasExited"/>. This is safe to call even if there is no operating system process
|
||||
/// associated with the <see cref="Process"/>.
|
||||
/// </summary>
|
||||
/// <param name="process">The process to check the exit status for.</param>
|
||||
/// <returns>
|
||||
/// True if the operating system process referenced by the <see cref="Process"/> component has
|
||||
/// terminated, or if there is no associated operating system process; otherwise, false.
|
||||
/// </returns>
|
||||
private static bool HasExitedSafe(this Process process)
|
||||
{
|
||||
try
|
||||
{
|
||||
return process.HasExited;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
return true;
|
||||
await process.WaitForExitAsync(cancelTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace MediaBrowser.Common
|
||||
@ -36,16 +35,15 @@ namespace MediaBrowser.Common
|
||||
string SystemId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance has pending kernel reload.
|
||||
/// Gets a value indicating whether this instance has pending changes requiring a restart.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance has pending kernel reload; otherwise, <c>false</c>.</value>
|
||||
/// <value><c>true</c> if this instance has a pending restart; otherwise, <c>false</c>.</value>
|
||||
bool HasPendingRestart { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is currently shutting down.
|
||||
/// Gets or sets a value indicating whether the application should restart.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance is shutting down; otherwise, <c>false</c>.</value>
|
||||
bool IsShuttingDown { get; }
|
||||
bool ShouldRestart { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the application version.
|
||||
@ -87,11 +85,6 @@ namespace MediaBrowser.Common
|
||||
/// </summary>
|
||||
void NotifyPendingRestart();
|
||||
|
||||
/// <summary>
|
||||
/// Restarts this instance.
|
||||
/// </summary>
|
||||
void Restart();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the exports.
|
||||
/// </summary>
|
||||
@ -123,12 +116,6 @@ namespace MediaBrowser.Common
|
||||
/// <returns>``0.</returns>
|
||||
T Resolve<T>();
|
||||
|
||||
/// <summary>
|
||||
/// Shuts down.
|
||||
/// </summary>
|
||||
/// <returns>A task.</returns>
|
||||
Task Shutdown();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes this instance.
|
||||
/// </summary>
|
||||
|
@ -29,11 +29,6 @@ namespace MediaBrowser.Common.Plugins
|
||||
/// <returns>An IEnumerable{Assembly}.</returns>
|
||||
IEnumerable<Assembly> LoadAssemblies();
|
||||
|
||||
/// <summary>
|
||||
/// Unloads all of the assemblies.
|
||||
/// </summary>
|
||||
void UnloadAssemblies();
|
||||
|
||||
/// <summary>
|
||||
/// Registers the plugin's services with the DI.
|
||||
/// Note: DI is not yet instantiated yet.
|
||||
|
@ -2,7 +2,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Entities;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
@ -70,14 +69,6 @@ namespace MediaBrowser.Controller.Drawing
|
||||
|
||||
string? GetImageCacheTag(User user);
|
||||
|
||||
/// <summary>
|
||||
/// Processes the image.
|
||||
/// </summary>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="toStream">To stream.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task ProcessImage(ImageProcessingOptions options, Stream toStream);
|
||||
|
||||
/// <summary>
|
||||
/// Processes the image.
|
||||
/// </summary>
|
||||
@ -97,7 +88,5 @@ namespace MediaBrowser.Controller.Drawing
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="libraryName">The library name to draw onto the collage.</param>
|
||||
void CreateImageCollage(ImageCollageOptions options, string? libraryName);
|
||||
|
||||
bool SupportsTransparency(string path);
|
||||
}
|
||||
}
|
||||
|
@ -119,7 +119,8 @@ namespace MediaBrowser.Controller.Drawing
|
||||
private bool IsFormatSupported(string originalImagePath)
|
||||
{
|
||||
var ext = Path.GetExtension(originalImagePath);
|
||||
return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, "." + outputFormat, StringComparison.OrdinalIgnoreCase));
|
||||
ext = ext.Replace(".jpeg", ".jpg", StringComparison.OrdinalIgnoreCase);
|
||||
return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, outputFormat.GetExtension(), StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@ -29,7 +30,7 @@ namespace MediaBrowser.Controller.Entities
|
||||
public class CollectionFolder : Folder, ICollectionFolder
|
||||
{
|
||||
private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||
private static readonly Dictionary<string, LibraryOptions> _libraryOptions = new Dictionary<string, LibraryOptions>();
|
||||
private static readonly ConcurrentDictionary<string, LibraryOptions> _libraryOptions = new ConcurrentDictionary<string, LibraryOptions>();
|
||||
private bool _requiresRefresh;
|
||||
|
||||
/// <summary>
|
||||
@ -139,45 +140,26 @@ namespace MediaBrowser.Controller.Entities
|
||||
}
|
||||
|
||||
public static LibraryOptions GetLibraryOptions(string path)
|
||||
{
|
||||
lock (_libraryOptions)
|
||||
{
|
||||
if (!_libraryOptions.TryGetValue(path, out var options))
|
||||
{
|
||||
options = LoadLibraryOptions(path);
|
||||
_libraryOptions[path] = options;
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
=> _libraryOptions.GetOrAdd(path, LoadLibraryOptions);
|
||||
|
||||
public static void SaveLibraryOptions(string path, LibraryOptions options)
|
||||
{
|
||||
lock (_libraryOptions)
|
||||
_libraryOptions[path] = options;
|
||||
|
||||
var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions);
|
||||
foreach (var mediaPath in clone.PathInfos)
|
||||
{
|
||||
_libraryOptions[path] = options;
|
||||
|
||||
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()
|
||||
{
|
||||
|
193
MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs
Normal file
193
MediaBrowser.Controller/Extensions/XmlReaderExtensions.cs
Normal file
@ -0,0 +1,193 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods for <see cref="XmlReader"/> to parse <see cref="BaseItem"/>'s.
|
||||
/// </summary>
|
||||
public static class XmlReaderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Reads a trimmed string from the current node.
|
||||
/// </summary>
|
||||
/// <param name="reader">The <see cref="XmlReader"/>.</param>
|
||||
/// <returns>The trimmed content.</returns>
|
||||
public static string ReadNormalizedString(this XmlReader reader)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(reader);
|
||||
|
||||
return reader.ReadElementContentAsString().Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads an int from the current node.
|
||||
/// </summary>
|
||||
/// <param name="reader">The <see cref="XmlReader"/>.</param>
|
||||
/// <param name="value">The parsed <c>int</c>.</param>
|
||||
/// <returns>A value indicating whether the parsing succeeded.</returns>
|
||||
public static bool TryReadInt(this XmlReader reader, out int value)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(reader);
|
||||
|
||||
return int.TryParse(reader.ReadElementContentAsString(), CultureInfo.InvariantCulture, out value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a <see cref="DateTime"/> from the current node.
|
||||
/// </summary>
|
||||
/// <param name="reader">The <see cref="XmlReader"/>.</param>
|
||||
/// <param name="value">The parsed <see cref="DateTime"/>.</param>
|
||||
/// <returns>A value indicating whether the parsing succeeded.</returns>
|
||||
public static bool TryReadDateTime(this XmlReader reader, out DateTime value)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(reader);
|
||||
|
||||
return DateTime.TryParse(
|
||||
reader.ReadElementContentAsString(),
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
|
||||
out value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a <see cref="DateTime"/> from the current node.
|
||||
/// </summary>
|
||||
/// <param name="reader">The <see cref="XmlReader"/>.</param>
|
||||
/// <param name="formatString">The date format string.</param>
|
||||
/// <param name="value">The parsed <see cref="DateTime"/>.</param>
|
||||
/// <returns>A value indicating whether the parsing succeeded.</returns>
|
||||
public static bool TryReadDateTimeExact(this XmlReader reader, string formatString, out DateTime value)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(reader);
|
||||
ArgumentNullException.ThrowIfNull(formatString);
|
||||
|
||||
return DateTime.TryParseExact(
|
||||
reader.ReadElementContentAsString(),
|
||||
formatString,
|
||||
CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
|
||||
out value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a <see cref="PersonInfo"/> from the xml node.
|
||||
/// </summary>
|
||||
/// <param name="reader">The <see cref="XmlReader"/>.</param>
|
||||
/// <returns>A <see cref="PersonInfo"/>, or <c>null</c> if none is found.</returns>
|
||||
public static PersonInfo? GetPersonFromXmlNode(this XmlReader reader)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(reader);
|
||||
|
||||
if (reader.IsEmptyElement)
|
||||
{
|
||||
reader.Read();
|
||||
return null;
|
||||
}
|
||||
|
||||
var name = string.Empty;
|
||||
var type = PersonKind.Actor; // If type is not specified assume actor
|
||||
var role = string.Empty;
|
||||
int? sortOrder = null;
|
||||
string? imageUrl = null;
|
||||
|
||||
using var subtree = reader.ReadSubtree();
|
||||
subtree.MoveToContent();
|
||||
subtree.Read();
|
||||
|
||||
while (subtree is { EOF: false, ReadState: ReadState.Interactive })
|
||||
{
|
||||
if (subtree.NodeType != XmlNodeType.Element)
|
||||
{
|
||||
subtree.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (subtree.Name)
|
||||
{
|
||||
case "name":
|
||||
case "Name":
|
||||
name = subtree.ReadNormalizedString();
|
||||
break;
|
||||
case "role":
|
||||
case "Role":
|
||||
role = subtree.ReadNormalizedString();
|
||||
break;
|
||||
case "type":
|
||||
case "Type":
|
||||
Enum.TryParse(subtree.ReadElementContentAsString(), true, out type);
|
||||
break;
|
||||
case "order":
|
||||
case "sortorder":
|
||||
case "SortOrder":
|
||||
if (subtree.TryReadInt(out var sortOrderVal))
|
||||
{
|
||||
sortOrder = sortOrderVal;
|
||||
}
|
||||
|
||||
break;
|
||||
case "thumb":
|
||||
imageUrl = subtree.ReadNormalizedString();
|
||||
break;
|
||||
default:
|
||||
subtree.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new PersonInfo
|
||||
{
|
||||
Name = name,
|
||||
Role = role,
|
||||
Type = type,
|
||||
SortOrder = sortOrder,
|
||||
ImageUrl = imageUrl
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to split names of comma or pipe delimited genres and people.
|
||||
/// </summary>
|
||||
/// <param name="reader">The <see cref="XmlReader"/>.</param>
|
||||
/// <returns>IEnumerable{System.String}.</returns>
|
||||
public static IEnumerable<string> GetStringArray(this XmlReader reader)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(reader);
|
||||
var value = reader.ReadElementContentAsString();
|
||||
|
||||
// Only split by comma if there is no pipe in the string
|
||||
// We have to be careful to not split names like Matthew, Jr.
|
||||
var separator = !value.Contains('|', StringComparison.Ordinal)
|
||||
&& !value.Contains(';', StringComparison.Ordinal)
|
||||
? new[] { ',' }
|
||||
: new[] { '|', ';' };
|
||||
|
||||
foreach (var part in value.Trim().Trim(separator).Split(separator))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(part))
|
||||
{
|
||||
yield return part.Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a <see cref="PersonInfo"/> array from the xml node.
|
||||
/// </summary>
|
||||
/// <param name="reader">The <see cref="XmlReader"/>.</param>
|
||||
/// <param name="personKind">The <see cref="PersonKind"/>.</param>
|
||||
/// <returns>The <see cref="IEnumerable{PersonInfo}"/>.</returns>
|
||||
public static IEnumerable<PersonInfo> GetPersonArray(this XmlReader reader, PersonKind personKind)
|
||||
=> reader.GetStringArray()
|
||||
.Select(part => new PersonInfo { Name = part, Type = personKind });
|
||||
}
|
@ -4,7 +4,6 @@
|
||||
|
||||
using System.Net;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Model.System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace MediaBrowser.Controller
|
||||
@ -16,8 +15,6 @@ namespace MediaBrowser.Controller
|
||||
{
|
||||
bool CoreStartupHasCompleted { get; }
|
||||
|
||||
bool CanLaunchWebBrowser { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the HTTP server port.
|
||||
/// </summary>
|
||||
@ -41,15 +38,6 @@ namespace MediaBrowser.Controller
|
||||
/// <value>The name of the friendly.</value>
|
||||
string FriendlyName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the system info.
|
||||
/// </summary>
|
||||
/// <param name="request">The HTTP request.</param>
|
||||
/// <returns>SystemInfo.</returns>
|
||||
SystemInfo GetSystemInfo(HttpRequest request);
|
||||
|
||||
PublicSystemInfo GetPublicSystemInfo(HttpRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a URL specific for the request.
|
||||
/// </summary>
|
||||
|
34
MediaBrowser.Controller/ISystemManager.cs
Normal file
34
MediaBrowser.Controller/ISystemManager.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using MediaBrowser.Model.System;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace MediaBrowser.Controller;
|
||||
|
||||
/// <summary>
|
||||
/// A service for managing the application instance.
|
||||
/// </summary>
|
||||
public interface ISystemManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the system info.
|
||||
/// </summary>
|
||||
/// <param name="request">The HTTP request.</param>
|
||||
/// <returns>The <see cref="SystemInfo"/>.</returns>
|
||||
SystemInfo GetSystemInfo(HttpRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the public system info.
|
||||
/// </summary>
|
||||
/// <param name="request">The HTTP request.</param>
|
||||
/// <returns>The <see cref="PublicSystemInfo"/>.</returns>
|
||||
PublicSystemInfo GetPublicSystemInfo(HttpRequest request);
|
||||
|
||||
/// <summary>
|
||||
/// Starts the application restart process.
|
||||
/// </summary>
|
||||
void Restart();
|
||||
|
||||
/// <summary>
|
||||
/// Starts the application shutdown process.
|
||||
/// </summary>
|
||||
void Shutdown();
|
||||
}
|
@ -48,6 +48,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
private readonly Version _minFFmpegHwaUnsafeOutput = new Version(6, 0);
|
||||
private readonly Version _minFFmpegOclCuTonemapMode = new Version(5, 1, 3);
|
||||
private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1);
|
||||
private readonly Version _minFFmpegVaapiH26xEncA53CcSei = new Version(6, 0);
|
||||
|
||||
private static readonly string[] _videoProfilesH264 = new[]
|
||||
{
|
||||
@ -547,25 +548,25 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
/// <returns>System.Nullable{VideoCodecs}.</returns>
|
||||
public string InferVideoCodec(string url)
|
||||
{
|
||||
var ext = Path.GetExtension(url);
|
||||
var ext = Path.GetExtension(url.AsSpan());
|
||||
|
||||
if (string.Equals(ext, ".asf", StringComparison.OrdinalIgnoreCase))
|
||||
if (ext.Equals(".asf", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "wmv";
|
||||
}
|
||||
|
||||
if (string.Equals(ext, ".webm", StringComparison.OrdinalIgnoreCase))
|
||||
if (ext.Equals(".webm", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// TODO: this may not always mean VP8, as the codec ages
|
||||
return "vp8";
|
||||
}
|
||||
|
||||
if (string.Equals(ext, ".ogg", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ogv", StringComparison.OrdinalIgnoreCase))
|
||||
if (ext.Equals(".ogg", StringComparison.OrdinalIgnoreCase) || ext.Equals(".ogv", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "theora";
|
||||
}
|
||||
|
||||
if (string.Equals(ext, ".m3u8", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".ts", StringComparison.OrdinalIgnoreCase))
|
||||
if (ext.Equals(".m3u8", StringComparison.OrdinalIgnoreCase) || ext.Equals(".ts", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "h264";
|
||||
}
|
||||
@ -1079,10 +1080,10 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
&& state.SubtitleStream.IsExternal)
|
||||
{
|
||||
var subtitlePath = state.SubtitleStream.Path;
|
||||
var subtitleExtension = Path.GetExtension(subtitlePath);
|
||||
var subtitleExtension = Path.GetExtension(subtitlePath.AsSpan());
|
||||
|
||||
if (string.Equals(subtitleExtension, ".sub", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(subtitleExtension, ".sup", StringComparison.OrdinalIgnoreCase))
|
||||
if (subtitleExtension.Equals(".sub", StringComparison.OrdinalIgnoreCase)
|
||||
|| subtitleExtension.Equals(".sup", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var idxFile = Path.ChangeExtension(subtitlePath, ".idx");
|
||||
if (File.Exists(idxFile))
|
||||
@ -2006,6 +2007,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
param += " -svtav1-params:0 rc=1:tune=0:film-grain=0:enable-overlays=1:enable-tf=0";
|
||||
}
|
||||
|
||||
/* Access unit too large: 8192 < 20880 error */
|
||||
if ((string.Equals(videoEncoder, "h264_vaapi", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(videoEncoder, "hevc_vaapi", StringComparison.OrdinalIgnoreCase)) &&
|
||||
_mediaEncoder.EncoderVersion >= _minFFmpegVaapiH26xEncA53CcSei)
|
||||
{
|
||||
param += " -sei -a53_cc";
|
||||
}
|
||||
|
||||
return param;
|
||||
}
|
||||
|
||||
@ -5681,7 +5690,6 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
// Apply -analyzeduration as per the environment variable,
|
||||
// otherwise ffmpeg will break on certain files due to default value is 0.
|
||||
// The default value of -probesize is more than enough, so leave it as is.
|
||||
var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
|
||||
|
||||
if (state.MediaSource.AnalyzeDurationMs > 0)
|
||||
@ -5700,6 +5708,14 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
|
||||
inputModifier = inputModifier.Trim();
|
||||
|
||||
// Apply -probesize if configured
|
||||
var ffmpegProbeSize = _config.GetFFmpegProbeSize();
|
||||
|
||||
if (!string.IsNullOrEmpty(ffmpegProbeSize))
|
||||
{
|
||||
inputModifier += $" -probesize {ffmpegProbeSize}";
|
||||
}
|
||||
|
||||
var userAgentParam = GetUserAgentParam(state);
|
||||
|
||||
if (!string.IsNullOrEmpty(userAgentParam))
|
||||
@ -6024,7 +6040,7 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
var format = string.Empty;
|
||||
var keyFrame = string.Empty;
|
||||
|
||||
if (string.Equals(Path.GetExtension(outputPath), ".mp4", StringComparison.OrdinalIgnoreCase)
|
||||
if (Path.GetExtension(outputPath.AsSpan()).Equals(".mp4", StringComparison.OrdinalIgnoreCase)
|
||||
&& state.BaseRequest.Context == EncodingContext.Streaming)
|
||||
{
|
||||
// Comparison: https://github.com/jansmolders86/mediacenterjs/blob/master/lib/transcoding/desktop.js
|
||||
@ -6233,6 +6249,12 @@ namespace MediaBrowser.Controller.MediaEncoding
|
||||
audioTranscodeParams.Add("-acodec " + GetAudioEncoder(state));
|
||||
}
|
||||
|
||||
if (GetAudioEncoder(state).StartsWith("pcm_", StringComparison.Ordinal))
|
||||
{
|
||||
audioTranscodeParams.Add(string.Concat("-f ", GetAudioEncoder(state).AsSpan(4)));
|
||||
audioTranscodeParams.Add("-ar " + state.BaseRequest.AudioBitRate);
|
||||
}
|
||||
|
||||
if (!string.Equals(outputCodec, "opus", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// opus only supports specific sampling rates
|
||||
|
@ -24,7 +24,7 @@ namespace MediaBrowser.Controller.Resolvers
|
||||
/// </summary>
|
||||
/// <param name="args">The args.</param>
|
||||
/// <returns>BaseItem.</returns>
|
||||
BaseItem ResolvePath(ItemResolveArgs args);
|
||||
BaseItem? ResolvePath(ItemResolveArgs args);
|
||||
}
|
||||
|
||||
public interface IMultiItemResolver
|
||||
|
@ -1,5 +1,3 @@
|
||||
#nullable disable
|
||||
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
||||
@ -23,7 +21,7 @@ namespace MediaBrowser.Controller.Resolvers
|
||||
/// </summary>
|
||||
/// <param name="args">The args.</param>
|
||||
/// <returns>`0.</returns>
|
||||
protected internal virtual T Resolve(ItemResolveArgs args)
|
||||
protected internal virtual T? Resolve(ItemResolveArgs args)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@ -42,7 +40,7 @@ namespace MediaBrowser.Controller.Resolvers
|
||||
/// </summary>
|
||||
/// <param name="args">The args.</param>
|
||||
/// <returns>BaseItem.</returns>
|
||||
public BaseItem ResolvePath(ItemResolveArgs args)
|
||||
public BaseItem? ResolvePath(ItemResolveArgs args)
|
||||
{
|
||||
var item = Resolve(args);
|
||||
|
||||
|
@ -232,20 +232,6 @@ namespace MediaBrowser.Controller.Session
|
||||
/// <returns>Task.</returns>
|
||||
Task SendRestartRequiredNotification(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Sends the server shutdown notification.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task SendServerShutdownNotification(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Sends the server restart notification.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task SendServerRestartNotification(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Adds the additional user.
|
||||
/// </summary>
|
||||
|
@ -9,6 +9,7 @@ using System.Xml;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Extensions;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
@ -128,42 +129,19 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
|
||||
switch (reader.Name)
|
||||
{
|
||||
// DateCreated
|
||||
case "Added":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(val))
|
||||
if (reader.TryReadDateTime(out var dateCreated))
|
||||
{
|
||||
if (DateTime.TryParse(val, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var added))
|
||||
{
|
||||
item.DateCreated = added;
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogWarning("Invalid Added value found: {Value}", val);
|
||||
}
|
||||
item.DateCreated = dateCreated;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "OriginalTitle":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrEmpty(val))
|
||||
{
|
||||
item.OriginalTitle = val;
|
||||
}
|
||||
|
||||
item.OriginalTitle = reader.ReadNormalizedString();
|
||||
break;
|
||||
}
|
||||
|
||||
case "LocalTitle":
|
||||
item.Name = reader.ReadElementContentAsString();
|
||||
item.Name = reader.ReadNormalizedString();
|
||||
break;
|
||||
|
||||
case "CriticRating":
|
||||
{
|
||||
var text = reader.ReadElementContentAsString();
|
||||
@ -177,63 +155,26 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
}
|
||||
|
||||
case "SortTitle":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(val))
|
||||
{
|
||||
item.ForcedSortName = val;
|
||||
}
|
||||
|
||||
item.ForcedSortName = reader.ReadNormalizedString();
|
||||
break;
|
||||
}
|
||||
|
||||
case "Overview":
|
||||
case "Description":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(val))
|
||||
{
|
||||
item.Overview = val;
|
||||
}
|
||||
|
||||
item.Overview = reader.ReadNormalizedString();
|
||||
break;
|
||||
}
|
||||
|
||||
case "Language":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
|
||||
item.PreferredMetadataLanguage = val;
|
||||
|
||||
item.PreferredMetadataLanguage = reader.ReadNormalizedString();
|
||||
break;
|
||||
}
|
||||
|
||||
case "CountryCode":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
|
||||
item.PreferredMetadataCountryCode = val;
|
||||
|
||||
item.PreferredMetadataCountryCode = reader.ReadNormalizedString();
|
||||
break;
|
||||
}
|
||||
|
||||
case "PlaceOfBirth":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(val))
|
||||
var placeOfBirth = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(placeOfBirth) && item is Person person)
|
||||
{
|
||||
if (item is Person person)
|
||||
{
|
||||
person.ProductionLocations = new[] { val };
|
||||
}
|
||||
person.ProductionLocations = new[] { placeOfBirth };
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "LockedFields":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
@ -275,10 +216,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
{
|
||||
if (!reader.IsEmptyElement)
|
||||
{
|
||||
using (var subtree = reader.ReadSubtree())
|
||||
{
|
||||
FetchFromCountriesNode(subtree);
|
||||
}
|
||||
reader.Skip();
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -290,183 +228,84 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
|
||||
case "ContentRating":
|
||||
case "MPAARating":
|
||||
{
|
||||
var rating = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(rating))
|
||||
{
|
||||
item.OfficialRating = rating;
|
||||
}
|
||||
|
||||
item.OfficialRating = reader.ReadNormalizedString();
|
||||
break;
|
||||
}
|
||||
|
||||
case "CustomRating":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(val))
|
||||
{
|
||||
item.CustomRating = val;
|
||||
}
|
||||
|
||||
item.CustomRating = reader.ReadNormalizedString();
|
||||
break;
|
||||
}
|
||||
|
||||
case "RunningTime":
|
||||
{
|
||||
var text = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
var runtimeText = reader.ReadElementContentAsString();
|
||||
if (!string.IsNullOrWhiteSpace(runtimeText))
|
||||
{
|
||||
if (int.TryParse(text.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
|
||||
if (int.TryParse(runtimeText.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
|
||||
{
|
||||
item.RunTimeTicks = TimeSpan.FromMinutes(runtime).Ticks;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "AspectRatio":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(val) && item is IHasAspectRatio hasAspectRatio)
|
||||
var aspectRatio = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(aspectRatio) && item is IHasAspectRatio hasAspectRatio)
|
||||
{
|
||||
hasAspectRatio.AspectRatio = val;
|
||||
hasAspectRatio.AspectRatio = aspectRatio;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "LockData":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(val))
|
||||
{
|
||||
item.IsLocked = string.Equals("true", val, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
item.IsLocked = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
|
||||
break;
|
||||
}
|
||||
|
||||
case "Network":
|
||||
{
|
||||
foreach (var name in SplitNames(reader.ReadElementContentAsString()))
|
||||
foreach (var name in reader.GetStringArray())
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
item.AddStudio(name);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "Director":
|
||||
{
|
||||
foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Director }))
|
||||
foreach (var director in reader.GetPersonArray(PersonKind.Director))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(p.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
itemResult.AddPerson(p);
|
||||
itemResult.AddPerson(director);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "Writer":
|
||||
{
|
||||
foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Writer }))
|
||||
foreach (var writer in reader.GetPersonArray(PersonKind.Writer))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(p.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
itemResult.AddPerson(p);
|
||||
itemResult.AddPerson(writer);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "Actors":
|
||||
{
|
||||
var actors = reader.ReadInnerXml();
|
||||
|
||||
if (actors.Contains('<', StringComparison.Ordinal))
|
||||
foreach (var actor in reader.GetPersonArray(PersonKind.Actor))
|
||||
{
|
||||
// This is one of the mis-named "Actors" full nodes created by MB2
|
||||
// Create a reader and pass it to the persons node processor
|
||||
using var xmlReader = XmlReader.Create(new StringReader($"<Persons>{actors}</Persons>"));
|
||||
FetchDataFromPersonsNode(xmlReader, itemResult);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Old-style piped string
|
||||
foreach (var p in SplitNames(actors).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.Actor }))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(p.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
itemResult.AddPerson(p);
|
||||
}
|
||||
itemResult.AddPerson(actor);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "GuestStars":
|
||||
{
|
||||
foreach (var p in SplitNames(reader.ReadElementContentAsString()).Select(v => new PersonInfo { Name = v.Trim(), Type = PersonKind.GuestStar }))
|
||||
foreach (var guestStar in reader.GetPersonArray(PersonKind.GuestStar))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(p.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
itemResult.AddPerson(p);
|
||||
itemResult.AddPerson(guestStar);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "Trailer":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(val))
|
||||
var trailer = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(trailer))
|
||||
{
|
||||
item.AddTrailerUrl(val);
|
||||
item.AddTrailerUrl(trailer);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "DisplayOrder":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
|
||||
if (item is IHasDisplayOrder hasDisplayOrder)
|
||||
var displayOrder = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(displayOrder) && item is IHasDisplayOrder hasDisplayOrder)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(val))
|
||||
{
|
||||
hasDisplayOrder.DisplayOrder = val;
|
||||
}
|
||||
hasDisplayOrder.DisplayOrder = displayOrder;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "Trailers":
|
||||
{
|
||||
if (!reader.IsEmptyElement)
|
||||
@ -483,20 +322,12 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
}
|
||||
|
||||
case "ProductionYear":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(val))
|
||||
if (reader.TryReadInt(out var productionYear) && productionYear > 1850)
|
||||
{
|
||||
if (int.TryParse(val, out var productionYear) && productionYear > 1850)
|
||||
{
|
||||
item.ProductionYear = productionYear;
|
||||
}
|
||||
item.ProductionYear = productionYear;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "Rating":
|
||||
case "IMDBrating":
|
||||
{
|
||||
@ -517,40 +348,24 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
case "BirthDate":
|
||||
case "PremiereDate":
|
||||
case "FirstAired":
|
||||
{
|
||||
var firstAired = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(firstAired))
|
||||
if (reader.TryReadDateTimeExact("yyyy-MM-dd", out var firstAired))
|
||||
{
|
||||
if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out var airDate) && airDate.Year > 1850)
|
||||
{
|
||||
item.PremiereDate = airDate;
|
||||
item.ProductionYear = airDate.Year;
|
||||
}
|
||||
item.PremiereDate = firstAired;
|
||||
item.ProductionYear = firstAired.Year;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "DeathDate":
|
||||
case "EndDate":
|
||||
{
|
||||
var firstAired = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(firstAired))
|
||||
if (reader.TryReadDateTimeExact("yyyy-MM-dd", out var endDate))
|
||||
{
|
||||
if (DateTime.TryParseExact(firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal | DateTimeStyles.AdjustToUniversal, out var airDate) && airDate.Year > 1850)
|
||||
{
|
||||
item.EndDate = airDate;
|
||||
}
|
||||
item.EndDate = endDate;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "CollectionNumber":
|
||||
var tmdbCollection = reader.ReadElementContentAsString();
|
||||
if (!string.IsNullOrWhiteSpace(tmdbCollection))
|
||||
var tmdbCollection = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(tmdbCollection))
|
||||
{
|
||||
item.SetProviderId(MetadataProvider.TmdbCollection, tmdbCollection);
|
||||
}
|
||||
@ -753,41 +568,6 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
item.Shares = list.ToArray();
|
||||
}
|
||||
|
||||
private void FetchFromCountriesNode(XmlReader reader)
|
||||
{
|
||||
reader.MoveToContent();
|
||||
reader.Read();
|
||||
|
||||
// Loop through each element
|
||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
||||
{
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "Country":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(val))
|
||||
{
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fetches from taglines node.
|
||||
/// </summary>
|
||||
@ -806,17 +586,8 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "Tagline":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(val))
|
||||
{
|
||||
item.Tagline = val;
|
||||
}
|
||||
|
||||
item.Tagline = reader.ReadNormalizedString();
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
@ -847,17 +618,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "Genre":
|
||||
{
|
||||
var genre = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(genre))
|
||||
var genre = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(genre))
|
||||
{
|
||||
item.AddGenre(genre);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
@ -885,17 +652,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "Tag":
|
||||
{
|
||||
var tag = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(tag))
|
||||
var tag = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(tag))
|
||||
{
|
||||
tags.Add(tag);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
@ -929,29 +692,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
{
|
||||
case "Person":
|
||||
case "Actor":
|
||||
{
|
||||
if (reader.IsEmptyElement)
|
||||
var person = reader.GetPersonFromXmlNode();
|
||||
if (person is not null)
|
||||
{
|
||||
reader.Read();
|
||||
continue;
|
||||
}
|
||||
|
||||
using (var subtree = reader.ReadSubtree())
|
||||
{
|
||||
foreach (var person in GetPersonsFromXmlNode(subtree))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(person.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
item.AddPerson(person);
|
||||
}
|
||||
item.AddPerson(person);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
@ -977,17 +724,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "Trailer":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(val))
|
||||
var trailer = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(trailer))
|
||||
{
|
||||
item.AddTrailerUrl(val);
|
||||
item.AddTrailerUrl(trailer);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
@ -1018,17 +761,13 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "Studio":
|
||||
{
|
||||
var studio = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(studio))
|
||||
var studio = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(studio))
|
||||
{
|
||||
item.AddStudio(studio);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
@ -1041,83 +780,6 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the persons from XML node.
|
||||
/// </summary>
|
||||
/// <param name="reader">The reader.</param>
|
||||
/// <returns>IEnumerable{PersonInfo}.</returns>
|
||||
private IEnumerable<PersonInfo> GetPersonsFromXmlNode(XmlReader reader)
|
||||
{
|
||||
var name = string.Empty;
|
||||
var type = PersonKind.Actor; // If type is not specified assume actor
|
||||
var role = string.Empty;
|
||||
int? sortOrder = null;
|
||||
|
||||
reader.MoveToContent();
|
||||
reader.Read();
|
||||
|
||||
// Loop through each element
|
||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
||||
{
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "Name":
|
||||
name = reader.ReadElementContentAsString();
|
||||
break;
|
||||
|
||||
case "Type":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
_ = Enum.TryParse(val, true, out type);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "Role":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(val))
|
||||
{
|
||||
role = val;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "SortOrder":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(val))
|
||||
{
|
||||
if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intVal))
|
||||
{
|
||||
sortOrder = intVal;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Read();
|
||||
}
|
||||
}
|
||||
|
||||
var personInfo = new PersonInfo { Name = name.Trim(), Role = role, Type = type, SortOrder = sortOrder };
|
||||
|
||||
return new[] { personInfo };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get linked child.
|
||||
/// </summary>
|
||||
@ -1138,17 +800,11 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "Path":
|
||||
{
|
||||
linkedItem.Path = reader.ReadElementContentAsString();
|
||||
linkedItem.Path = reader.ReadNormalizedString();
|
||||
break;
|
||||
}
|
||||
|
||||
case "ItemId":
|
||||
{
|
||||
linkedItem.LibraryItemId = reader.ReadElementContentAsString();
|
||||
linkedItem.LibraryItemId = reader.ReadNormalizedString();
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
reader.Skip();
|
||||
break;
|
||||
@ -1189,22 +845,14 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "UserId":
|
||||
{
|
||||
item.UserId = reader.ReadElementContentAsString();
|
||||
item.UserId = reader.ReadNormalizedString();
|
||||
break;
|
||||
}
|
||||
|
||||
case "CanEdit":
|
||||
{
|
||||
item.CanEdit = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
@ -1221,34 +869,5 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used to split names of comma or pipe delimited genres and people.
|
||||
/// </summary>
|
||||
/// <param name="value">The value.</param>
|
||||
/// <returns>IEnumerable{System.String}.</returns>
|
||||
private IEnumerable<string> SplitNames(string value)
|
||||
{
|
||||
// Only split by comma if there is no pipe in the string
|
||||
// We have to be careful to not split names like Matthew, Jr.
|
||||
var separator = !value.Contains('|', StringComparison.Ordinal)
|
||||
&& !value.Contains(';', StringComparison.Ordinal) ? new[] { ',' } : new[] { '|', ';' };
|
||||
|
||||
value = value.Trim().Trim(separator);
|
||||
|
||||
return string.IsNullOrWhiteSpace(value) ? Array.Empty<string>() : Split(value, separator, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides an additional overload for string.split.
|
||||
/// </summary>
|
||||
/// <param name="val">The val.</param>
|
||||
/// <param name="separators">The separators.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <returns>System.String[][].</returns>
|
||||
private string[] Split(string val, char[] separators, StringSplitOptions options)
|
||||
{
|
||||
return val.Split(separators, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Xml;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Extensions;
|
||||
using MediaBrowser.Controller.Playlists;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -30,12 +31,8 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "PlaylistMediaType":
|
||||
{
|
||||
item.PlaylistMediaType = reader.ReadElementContentAsString();
|
||||
|
||||
item.PlaylistMediaType = reader.ReadNormalizedString();
|
||||
break;
|
||||
}
|
||||
|
||||
case "PlaylistItems":
|
||||
|
||||
if (!reader.IsEmptyElement)
|
||||
@ -94,10 +91,8 @@ namespace MediaBrowser.LocalMetadata.Parsers
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
reader.Skip();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
|
@ -1,4 +1,3 @@
|
||||
#nullable disable
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
@ -23,7 +22,7 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.MediaEncoding.Attachments
|
||||
{
|
||||
public class AttachmentExtractor : IAttachmentExtractor, IDisposable
|
||||
public sealed class AttachmentExtractor : IAttachmentExtractor
|
||||
{
|
||||
private readonly ILogger<AttachmentExtractor> _logger;
|
||||
private readonly IApplicationPaths _appPaths;
|
||||
@ -34,8 +33,6 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreLocks =
|
||||
new ConcurrentDictionary<string, SemaphoreSlim>();
|
||||
|
||||
private bool _disposed = false;
|
||||
|
||||
public AttachmentExtractor(
|
||||
ILogger<AttachmentExtractor> logger,
|
||||
IApplicationPaths appPaths,
|
||||
@ -177,22 +174,16 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
|
||||
process.Start();
|
||||
|
||||
var ranToCompletion = await ProcessExtensions.WaitForExitAsync(process, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!ranToCompletion)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogWarning("Killing ffmpeg attachment extraction process");
|
||||
process.Kill();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error killing attachment extraction process");
|
||||
}
|
||||
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
||||
exitCode = process.ExitCode;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
process.Kill(true);
|
||||
exitCode = -1;
|
||||
}
|
||||
|
||||
exitCode = ranToCompletion ? process.ExitCode : -1;
|
||||
}
|
||||
|
||||
var failed = false;
|
||||
@ -296,7 +287,7 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
|
||||
ArgumentException.ThrowIfNullOrEmpty(outputPath);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(outputPath) ?? throw new ArgumentException("Path can't be a root directory.", nameof(outputPath)));
|
||||
|
||||
var processArgs = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
@ -325,22 +316,16 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
|
||||
process.Start();
|
||||
|
||||
var ranToCompletion = await ProcessExtensions.WaitForExitAsync(process, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!ranToCompletion)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogWarning("Killing ffmpeg attachment extraction process");
|
||||
process.Kill();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error killing attachment extraction process");
|
||||
}
|
||||
await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
|
||||
exitCode = process.ExitCode;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
process.Kill(true);
|
||||
exitCode = -1;
|
||||
}
|
||||
|
||||
exitCode = ranToCompletion ? process.ExitCode : -1;
|
||||
}
|
||||
|
||||
var failed = false;
|
||||
@ -391,33 +376,8 @@ namespace MediaBrowser.MediaEncoding.Attachments
|
||||
filename = (mediaPath + attachmentStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("D", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
var prefix = filename.Substring(0, 1);
|
||||
return Path.Combine(_appPaths.DataPath, "attachments", prefix, filename);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Releases unmanaged and - optionally - managed resources.
|
||||
/// </summary>
|
||||
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
var prefix = filename.AsSpan(0, 1);
|
||||
return Path.Join(_appPaths.DataPath, "attachments", prefix, filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -316,10 +316,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
var files = _fileSystem.GetFilePaths(path, recursive);
|
||||
|
||||
var excludeExtensions = new[] { ".c" };
|
||||
|
||||
return files.FirstOrDefault(i => string.Equals(Path.GetFileNameWithoutExtension(i), filename, StringComparison.OrdinalIgnoreCase)
|
||||
&& !excludeExtensions.Contains(Path.GetExtension(i) ?? string.Empty));
|
||||
return files.FirstOrDefault(i => Path.GetFileNameWithoutExtension(i.AsSpan()).Equals(filename, StringComparison.OrdinalIgnoreCase)
|
||||
&& !Path.GetExtension(i.AsSpan()).Equals(".c", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@ -419,6 +417,8 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
var extractChapters = request.MediaType == DlnaProfileType.Video && request.ExtractChapters;
|
||||
var analyzeDuration = string.Empty;
|
||||
var ffmpegAnalyzeDuration = _config.GetFFmpegAnalyzeDuration() ?? string.Empty;
|
||||
var ffmpegProbeSize = _config.GetFFmpegProbeSize() ?? string.Empty;
|
||||
var extraArgs = string.Empty;
|
||||
|
||||
if (request.MediaSource.AnalyzeDurationMs > 0)
|
||||
{
|
||||
@ -429,12 +429,22 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
analyzeDuration = "-analyzeduration " + ffmpegAnalyzeDuration;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(analyzeDuration))
|
||||
{
|
||||
extraArgs = analyzeDuration;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(ffmpegProbeSize))
|
||||
{
|
||||
extraArgs += " -probesize " + ffmpegProbeSize;
|
||||
}
|
||||
|
||||
return GetMediaInfoInternal(
|
||||
GetInputArgument(request.MediaSource.Path, request.MediaSource),
|
||||
request.MediaSource.Path,
|
||||
request.MediaSource.Protocol,
|
||||
extractChapters,
|
||||
analyzeDuration,
|
||||
extraArgs,
|
||||
request.MediaType == DlnaProfileType.Audio,
|
||||
request.MediaSource.VideoType,
|
||||
cancellationToken);
|
||||
@ -640,15 +650,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(inputPath);
|
||||
|
||||
var outputExtension = targetFormat switch
|
||||
{
|
||||
ImageFormat.Bmp => ".bmp",
|
||||
ImageFormat.Gif => ".gif",
|
||||
ImageFormat.Jpg => ".jpg",
|
||||
ImageFormat.Png => ".png",
|
||||
ImageFormat.Webp => ".webp",
|
||||
_ => ".jpg"
|
||||
};
|
||||
var outputExtension = targetFormat?.GetExtension() ?? ".jpg";
|
||||
|
||||
var tempExtractPath = Path.Combine(_configurationManager.ApplicationPaths.TempDirectory, Guid.NewGuid() + outputExtension);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(tempExtractPath));
|
||||
@ -750,11 +752,15 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
timeoutMs = enableHdrExtraction ? DefaultHdrImageExtractionTimeout : DefaultSdrImageExtractionTimeout;
|
||||
}
|
||||
|
||||
ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
|
||||
|
||||
if (!ranToCompletion)
|
||||
try
|
||||
{
|
||||
StopProcess(processWrapper, 1000);
|
||||
await process.WaitForExitAsync(TimeSpan.FromMilliseconds(timeoutMs)).ConfigureAwait(false);
|
||||
ranToCompletion = true;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
process.Kill(true);
|
||||
ranToCompletion = false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
@ -989,7 +995,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
return true;
|
||||
}
|
||||
|
||||
private class ProcessWrapper : IDisposable
|
||||
private sealed class ProcessWrapper : IDisposable
|
||||
{
|
||||
private readonly MediaEncoder _mediaEncoder;
|
||||
|
||||
@ -1032,13 +1038,7 @@ namespace MediaBrowser.MediaEncoding.Encoder
|
||||
_mediaEncoder._runningProcesses.Remove(this);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
process.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
process.Dispose();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
@ -78,6 +78,7 @@ namespace MediaBrowser.MediaEncoding.Probing
|
||||
"She/Her/Hers",
|
||||
"5/8erl in Ehr'n",
|
||||
"Smith/Kotzen",
|
||||
"We;Na",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
|
@ -420,23 +420,16 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
throw;
|
||||
}
|
||||
|
||||
var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
|
||||
|
||||
if (!ranToCompletion)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Killing ffmpeg subtitle conversion process");
|
||||
|
||||
process.Kill();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error killing subtitle conversion process");
|
||||
}
|
||||
await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
|
||||
exitCode = process.ExitCode;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
process.Kill(true);
|
||||
exitCode = -1;
|
||||
}
|
||||
|
||||
exitCode = ranToCompletion ? process.ExitCode : -1;
|
||||
}
|
||||
|
||||
var failed = false;
|
||||
@ -574,23 +567,16 @@ namespace MediaBrowser.MediaEncoding.Subtitles
|
||||
throw;
|
||||
}
|
||||
|
||||
var ranToCompletion = await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
|
||||
|
||||
if (!ranToCompletion)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogWarning("Killing ffmpeg subtitle extraction process");
|
||||
|
||||
process.Kill();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error killing subtitle extraction process");
|
||||
}
|
||||
await process.WaitForExitAsync(TimeSpan.FromMinutes(30)).ConfigureAwait(false);
|
||||
exitCode = process.ExitCode;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
process.Kill(true);
|
||||
exitCode = -1;
|
||||
}
|
||||
|
||||
exitCode = ranToCompletion ? process.ExitCode : -1;
|
||||
}
|
||||
|
||||
var failed = false;
|
||||
|
@ -24,4 +24,21 @@ public static class ImageFormatExtensions
|
||||
ImageFormat.Webp => "image/webp",
|
||||
_ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat))
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns the correct extension for this <see cref="ImageFormat" />.
|
||||
/// </summary>
|
||||
/// <param name="format">This <see cref="ImageFormat" />.</param>
|
||||
/// <exception cref="InvalidEnumArgumentException">The <paramref name="format"/> is an invalid enumeration value.</exception>
|
||||
/// <returns>The correct extension for this <see cref="ImageFormat" />.</returns>
|
||||
public static string GetExtension(this ImageFormat format)
|
||||
=> format switch
|
||||
{
|
||||
ImageFormat.Bmp => ".bmp",
|
||||
ImageFormat.Gif => ".gif",
|
||||
ImageFormat.Jpg => ".jpg",
|
||||
ImageFormat.Png => ".png",
|
||||
ImageFormat.Webp => ".webp",
|
||||
_ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat))
|
||||
};
|
||||
}
|
||||
|
@ -10,8 +10,6 @@ namespace MediaBrowser.Model.IO
|
||||
/// </summary>
|
||||
public interface IFileSystem
|
||||
{
|
||||
void AddShortcutHandler(IShortcutHandler handler);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified filename is shortcut.
|
||||
/// </summary>
|
||||
|
@ -263,7 +263,11 @@ namespace MediaBrowser.Providers.Manager
|
||||
|
||||
var fileStreamOptions = AsyncFile.WriteOptions;
|
||||
fileStreamOptions.Mode = FileMode.Create;
|
||||
fileStreamOptions.PreallocationSize = source.Length;
|
||||
if (source.CanSeek)
|
||||
{
|
||||
fileStreamOptions.PreallocationSize = source.Length;
|
||||
}
|
||||
|
||||
var fs = new FileStream(path, fileStreamOptions);
|
||||
await using (fs.ConfigureAwait(false))
|
||||
{
|
||||
|
@ -204,16 +204,10 @@ namespace MediaBrowser.Providers.MediaInfo
|
||||
? Path.GetExtension(attachmentStream.FileName)
|
||||
: MimeTypes.ToExtension(attachmentStream.MimeType);
|
||||
|
||||
if (string.IsNullOrEmpty(extension))
|
||||
{
|
||||
extension = ".jpg";
|
||||
}
|
||||
|
||||
ImageFormat format = extension switch
|
||||
{
|
||||
".bmp" => ImageFormat.Bmp,
|
||||
".gif" => ImageFormat.Gif,
|
||||
".jpg" => ImageFormat.Jpg,
|
||||
".png" => ImageFormat.Png,
|
||||
".webp" => ImageFormat.Webp,
|
||||
_ => ImageFormat.Jpg
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,11 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Xml;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Extensions;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -81,7 +82,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||
}
|
||||
|
||||
// Extract the last episode number from nfo
|
||||
// Retrieves all title and plot tags from the rest of the nfo and concatenates them with the first episode
|
||||
// This is needed because XBMC metadata uses multiple episodedetails blocks instead of episodenumberend tag
|
||||
var name = new StringBuilder(item.Item.Name);
|
||||
var overview = new StringBuilder(item.Item.Overview);
|
||||
while ((index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase)) != -1)
|
||||
{
|
||||
xml = xmlFile.Substring(0, index + srch.Length);
|
||||
@ -92,12 +96,44 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||
{
|
||||
reader.MoveToContent();
|
||||
|
||||
if (reader.ReadToDescendant("episode") && int.TryParse(reader.ReadElementContentAsString(), out var num))
|
||||
while (!reader.EOF && reader.ReadState == ReadState.Interactive)
|
||||
{
|
||||
item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (reader.NodeType == XmlNodeType.Element)
|
||||
{
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "name":
|
||||
case "title":
|
||||
case "localtitle":
|
||||
name.Append(" / ").Append(reader.ReadElementContentAsString());
|
||||
break;
|
||||
case "episode":
|
||||
{
|
||||
if (int.TryParse(reader.ReadElementContentAsString(), out var num))
|
||||
{
|
||||
item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case "biography":
|
||||
case "plot":
|
||||
case "review":
|
||||
overview.Append(" / ").Append(reader.ReadElementContentAsString());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
reader.Read();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item.Item.Name = name.ToString();
|
||||
item.Item.Overview = overview.ToString();
|
||||
}
|
||||
catch (XmlException)
|
||||
{
|
||||
@ -112,142 +148,53 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "season":
|
||||
if (reader.TryReadInt(out var seasonNumber))
|
||||
{
|
||||
var number = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(number))
|
||||
{
|
||||
if (int.TryParse(number, out var num))
|
||||
{
|
||||
item.ParentIndexNumber = num;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
item.ParentIndexNumber = seasonNumber;
|
||||
}
|
||||
|
||||
break;
|
||||
case "episode":
|
||||
if (reader.TryReadInt(out var episodeNumber))
|
||||
{
|
||||
var number = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(number))
|
||||
{
|
||||
if (int.TryParse(number, out var num))
|
||||
{
|
||||
item.IndexNumber = num;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
item.IndexNumber = episodeNumber;
|
||||
}
|
||||
|
||||
break;
|
||||
case "episodenumberend":
|
||||
if (reader.TryReadInt(out var episodeNumberEnd))
|
||||
{
|
||||
var number = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(number))
|
||||
{
|
||||
if (int.TryParse(number, out var num))
|
||||
{
|
||||
item.IndexNumberEnd = num;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
item.IndexNumberEnd = episodeNumberEnd;
|
||||
}
|
||||
|
||||
break;
|
||||
case "airsbefore_episode":
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
case "airsafter_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.AirsAfterSeasonNumber = rval;
|
||||
}
|
||||
}
|
||||
|
||||
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":
|
||||
{
|
||||
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":
|
||||
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))
|
||||
{
|
||||
item.AirsAfterSeasonNumber = airsAfterSeason;
|
||||
}
|
||||
|
||||
break;
|
||||
case "airsbefore_season":
|
||||
case "displayseason":
|
||||
if (reader.TryReadInt(out var airsBeforeSeason))
|
||||
{
|
||||
item.AirsBeforeSeasonNumber = airsBeforeSeason;
|
||||
}
|
||||
|
||||
break;
|
||||
case "showtitle":
|
||||
{
|
||||
var showtitle = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(showtitle))
|
||||
{
|
||||
item.SeriesName = showtitle;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
item.SeriesName = reader.ReadNormalizedString();
|
||||
break;
|
||||
default:
|
||||
base.FetchDataFromXmlNode(reader, itemResult);
|
||||
break;
|
||||
|
@ -5,6 +5,7 @@ using System.Xml;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
using MediaBrowser.Controller.Extensions;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
@ -113,31 +114,23 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||
}
|
||||
|
||||
case "artist":
|
||||
var artist = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(artist) && item is MusicVideo artistVideo)
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(val) && item is MusicVideo movie)
|
||||
{
|
||||
var list = movie.Artists.ToList();
|
||||
list.Add(val);
|
||||
movie.Artists = list.ToArray();
|
||||
}
|
||||
|
||||
break;
|
||||
var list = artistVideo.Artists.ToList();
|
||||
list.Add(artist);
|
||||
artistVideo.Artists = list.ToArray();
|
||||
}
|
||||
|
||||
break;
|
||||
case "album":
|
||||
var album = reader.ReadNormalizedString();
|
||||
if (!string.IsNullOrEmpty(album) && item is MusicVideo albumVideo)
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(val) && item is MusicVideo movie)
|
||||
{
|
||||
movie.Album = val;
|
||||
}
|
||||
|
||||
break;
|
||||
albumVideo.Album = album;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
base.FetchDataFromXmlNode(reader, itemResult);
|
||||
break;
|
||||
|
@ -1,7 +1,7 @@
|
||||
using System.Globalization;
|
||||
using System.Xml;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Extensions;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -41,32 +41,15 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||
switch (reader.Name)
|
||||
{
|
||||
case "seasonnumber":
|
||||
if (reader.TryReadInt(out var seasonNumber))
|
||||
{
|
||||
var number = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(number))
|
||||
{
|
||||
if (int.TryParse(number, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
|
||||
{
|
||||
item.IndexNumber = num;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
item.IndexNumber = seasonNumber;
|
||||
}
|
||||
|
||||
break;
|
||||
case "seasonname":
|
||||
{
|
||||
var name = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
item.Name = name;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
item.Name = reader.ReadNormalizedString();
|
||||
break;
|
||||
default:
|
||||
base.FetchDataFromXmlNode(reader, itemResult);
|
||||
break;
|
||||
|
@ -1,9 +1,9 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Xml;
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Controller.Entities.TV;
|
||||
using MediaBrowser.Controller.Extensions;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
@ -76,23 +76,11 @@ namespace MediaBrowser.XbmcMetadata.Parsers
|
||||
}
|
||||
|
||||
case "airs_dayofweek":
|
||||
{
|
||||
item.AirDays = TVUtils.GetAirDays(reader.ReadElementContentAsString());
|
||||
break;
|
||||
}
|
||||
|
||||
item.AirDays = TVUtils.GetAirDays(reader.ReadElementContentAsString());
|
||||
break;
|
||||
case "airs_time":
|
||||
{
|
||||
var val = reader.ReadElementContentAsString();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(val))
|
||||
{
|
||||
item.AirTime = val;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
item.AirTime = reader.ReadNormalizedString();
|
||||
break;
|
||||
case "status":
|
||||
{
|
||||
var status = reader.ReadElementContentAsString();
|
||||
|
@ -60,13 +60,13 @@ namespace MediaBrowser.XbmcMetadata.Savers
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return Path.ChangeExtension(item.Path, ".nfo");
|
||||
|
||||
// only allow movie object to read movie.nfo, not owned videos (which will be itemtype video, not movie)
|
||||
if (!item.IsInMixedFolder && item.ItemType == typeof(Movie))
|
||||
{
|
||||
yield return Path.Combine(item.ContainingFolderPath, "movie.nfo");
|
||||
}
|
||||
|
||||
yield return Path.ChangeExtension(item.Path, ".nfo");
|
||||
}
|
||||
}
|
||||
|
||||
|
1
debian/jellyfin.init
vendored
1
debian/jellyfin.init
vendored
@ -1,3 +1,4 @@
|
||||
#!/bin/sh
|
||||
### BEGIN INIT INFO
|
||||
# Provides: Jellyfin Media Server
|
||||
# Required-Start: $local_fs $network
|
||||
|
@ -8,7 +8,7 @@ EnvironmentFile = /etc/sysconfig/jellyfin
|
||||
User = jellyfin
|
||||
Group = jellyfin
|
||||
WorkingDirectory = /var/lib/jellyfin
|
||||
ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS
|
||||
ExecStart = /usr/bin/jellyfin $JELLYFIN_WEB_OPT $JELLYFIN_FFMPEG_OPT $JELLYFIN_SERVICE_OPT $JELLYFIN_NOWEBAPP_OPT $JELLYFIN_ADDITIONAL_OPTS
|
||||
Restart = on-failure
|
||||
TimeoutSec = 15
|
||||
SuccessExitStatus=0 143
|
||||
|
@ -75,6 +75,7 @@ dotnet publish --configuration Release --self-contained --runtime %{dotnet_runti
|
||||
%{__mkdir} -p %{buildroot}%{_libdir}/jellyfin %{buildroot}%{_bindir}
|
||||
%{__cp} -r Jellyfin.Server/bin/Release/net7.0/%{dotnet_runtime}/publish/* %{buildroot}%{_libdir}/jellyfin
|
||||
%{__install} -D %{SOURCE10} %{buildroot}%{_bindir}/jellyfin
|
||||
sed -i -e 's|/usr/lib64|%{_libdir}|g' %{buildroot}%{_bindir}/jellyfin
|
||||
|
||||
# Jellyfin config
|
||||
%{__install} -D Jellyfin.Server/Resources/Configuration/logging.json %{buildroot}%{_sysconfdir}/jellyfin/logging.json
|
||||
|
@ -188,7 +188,7 @@ public class SkiaEncoder : IImageEncoder
|
||||
return path;
|
||||
}
|
||||
|
||||
var tempPath = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + Path.GetExtension(path));
|
||||
var tempPath = Path.Combine(_appPaths.TempDirectory, string.Concat(Guid.NewGuid().ToString(), Path.GetExtension(path.AsSpan())));
|
||||
var directory = Path.GetDirectoryName(tempPath) ?? throw new ResourceNotFoundException($"Provided path ({tempPath}) is not valid.");
|
||||
Directory.CreateDirectory(directory);
|
||||
File.Copy(path, tempPath, true);
|
||||
@ -200,20 +200,10 @@ public class SkiaEncoder : IImageEncoder
|
||||
{
|
||||
if (!orientation.HasValue)
|
||||
{
|
||||
return SKEncodedOrigin.TopLeft;
|
||||
return SKEncodedOrigin.Default;
|
||||
}
|
||||
|
||||
return orientation.Value switch
|
||||
{
|
||||
ImageOrientation.TopRight => SKEncodedOrigin.TopRight,
|
||||
ImageOrientation.RightTop => SKEncodedOrigin.RightTop,
|
||||
ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom,
|
||||
ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop,
|
||||
ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom,
|
||||
ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight,
|
||||
ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft,
|
||||
_ => SKEncodedOrigin.TopLeft
|
||||
};
|
||||
return (SKEncodedOrigin)orientation.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -38,25 +38,25 @@ public partial class StripCollageBuilder
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(outputPath);
|
||||
|
||||
var ext = Path.GetExtension(outputPath);
|
||||
var ext = Path.GetExtension(outputPath.AsSpan());
|
||||
|
||||
if (string.Equals(ext, ".jpg", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(ext, ".jpeg", StringComparison.OrdinalIgnoreCase))
|
||||
if (ext.Equals(".jpg", StringComparison.OrdinalIgnoreCase)
|
||||
|| ext.Equals(".jpeg", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return SKEncodedImageFormat.Jpeg;
|
||||
}
|
||||
|
||||
if (string.Equals(ext, ".webp", StringComparison.OrdinalIgnoreCase))
|
||||
if (ext.Equals(".webp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return SKEncodedImageFormat.Webp;
|
||||
}
|
||||
|
||||
if (string.Equals(ext, ".gif", StringComparison.OrdinalIgnoreCase))
|
||||
if (ext.Equals(".gif", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return SKEncodedImageFormat.Gif;
|
||||
}
|
||||
|
||||
if (string.Equals(ext, ".bmp", StringComparison.OrdinalIgnoreCase))
|
||||
if (ext.Equals(".bmp", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return SKEncodedImageFormat.Bmp;
|
||||
}
|
||||
|
@ -107,22 +107,10 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
|
||||
/// <inheritdoc />
|
||||
public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ProcessImage(ImageProcessingOptions options, Stream toStream)
|
||||
{
|
||||
var file = await ProcessImage(options).ConfigureAwait(false);
|
||||
using var fileStream = AsyncFile.OpenRead(file.Path);
|
||||
await fileStream.CopyToAsync(toStream).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats()
|
||||
=> _imageEncoder.SupportedOutputFormats;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool SupportsTransparency(string path)
|
||||
=> _transparentImageTypes.Contains(Path.GetExtension(path));
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options)
|
||||
{
|
||||
@ -224,7 +212,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
return (cacheFilePath, GetMimeType(outputFormat, cacheFilePath), _fileSystem.GetLastWriteTimeUtc(cacheFilePath));
|
||||
return (cacheFilePath, outputFormat.GetMimeType(), _fileSystem.GetLastWriteTimeUtc(cacheFilePath));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -262,17 +250,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
|
||||
return ImageFormat.Jpg;
|
||||
}
|
||||
|
||||
private string GetMimeType(ImageFormat format, string path)
|
||||
=> format switch
|
||||
{
|
||||
ImageFormat.Bmp => MimeTypes.GetMimeType("i.bmp"),
|
||||
ImageFormat.Gif => MimeTypes.GetMimeType("i.gif"),
|
||||
ImageFormat.Jpg => MimeTypes.GetMimeType("i.jpg"),
|
||||
ImageFormat.Png => MimeTypes.GetMimeType("i.png"),
|
||||
ImageFormat.Webp => MimeTypes.GetMimeType("i.webp"),
|
||||
_ => MimeTypes.GetMimeType(path)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Gets the cache file path based on a set of parameters.
|
||||
/// </summary>
|
||||
@ -374,7 +351,7 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
|
||||
filename.Append(",v=");
|
||||
filename.Append(Version);
|
||||
|
||||
return GetCachePath(ResizedImageCachePath, filename.ToString(), "." + format.ToString().ToLowerInvariant());
|
||||
return GetCachePath(ResizedImageCachePath, filename.ToString(), format.GetExtension());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@ -471,35 +448,6 @@ public sealed class ImageProcessor : IImageProcessor, IDisposable
|
||||
return Task.FromResult((originalImagePath, dateModified));
|
||||
}
|
||||
|
||||
// TODO _mediaEncoder.ConvertImage is not implemented
|
||||
// if (!_imageEncoder.SupportedInputFormats.Contains(inputFormat))
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture);
|
||||
//
|
||||
// string cacheExtension = _mediaEncoder.SupportsEncoder("libwebp") ? ".webp" : ".png";
|
||||
// var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension);
|
||||
//
|
||||
// var file = _fileSystem.GetFileInfo(outputPath);
|
||||
// if (!file.Exists)
|
||||
// {
|
||||
// await _mediaEncoder.ConvertImage(originalImagePath, outputPath).ConfigureAwait(false);
|
||||
// dateModified = _fileSystem.GetLastWriteTimeUtc(outputPath);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// dateModified = file.LastWriteTimeUtc;
|
||||
// }
|
||||
//
|
||||
// originalImagePath = outputPath;
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// _logger.LogError(ex, "Image conversion failed for {Path}", originalImagePath);
|
||||
// }
|
||||
// }
|
||||
|
||||
return Task.FromResult((originalImagePath, dateModified));
|
||||
}
|
||||
|
||||
|
@ -30,4 +30,17 @@ public static class ImageFormatExtensionsTests
|
||||
[InlineData((ImageFormat)5)]
|
||||
public static void GetMimeType_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format)
|
||||
=> Assert.Throws<InvalidEnumArgumentException>(() => format.GetMimeType());
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(GetAllImageFormats))]
|
||||
public static void GetExtension_Valid_Valid(ImageFormat format)
|
||||
=> Assert.Null(Record.Exception(() => format.GetExtension()));
|
||||
|
||||
[Theory]
|
||||
[InlineData((ImageFormat)int.MinValue)]
|
||||
[InlineData((ImageFormat)int.MaxValue)]
|
||||
[InlineData((ImageFormat)(-1))]
|
||||
[InlineData((ImageFormat)5)]
|
||||
public static void GetExtension_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format)
|
||||
=> Assert.Throws<InvalidEnumArgumentException>(() => format.GetExtension());
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
|
||||
[InlineData("/media/music/Foo B.A.R./epic.flac", false)]
|
||||
[InlineData("/media/music/Foo B.A.R", false)]
|
||||
[InlineData("/media/music/Foo B.A.R.", false)]
|
||||
[InlineData("/movies/.zfs/snapshot/AutoM-2023-09", true)]
|
||||
public void PathIgnored(string path, bool expected)
|
||||
{
|
||||
Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path));
|
||||
|
@ -15,8 +15,8 @@ namespace Jellyfin.Server.Integration.Tests
|
||||
{
|
||||
public static class AuthHelper
|
||||
{
|
||||
public const string AuthHeaderName = "X-Emby-Authorization";
|
||||
public const string DummyAuthHeader = "MediaBrowser Client=\"Jellyfin.Server Integration Tests\", DeviceId=\"69420\", Device=\"Apple II\", Version=\"10.8.0\"";
|
||||
public const string AuthHeaderName = "Authorization";
|
||||
public const string DummyAuthHeader = "MediaBrowser Client=\"Jellyfin.Server%20Integration%20Tests\", DeviceId=\"69420\", Device=\"Apple%20II\", Version=\"10.8.0\"";
|
||||
|
||||
public static async Task<string> CompleteStartupAsync(HttpClient client)
|
||||
{
|
||||
@ -27,16 +27,19 @@ namespace Jellyfin.Server.Integration.Tests
|
||||
using var completeResponse = await client.PostAsync("/Startup/Complete", new ByteArrayContent(Array.Empty<byte>()));
|
||||
Assert.Equal(HttpStatusCode.NoContent, completeResponse.StatusCode);
|
||||
|
||||
using var content = JsonContent.Create(
|
||||
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "/Users/AuthenticateByName");
|
||||
httpRequest.Headers.TryAddWithoutValidation(AuthHeaderName, DummyAuthHeader);
|
||||
httpRequest.Content = JsonContent.Create(
|
||||
new AuthenticateUserByName()
|
||||
{
|
||||
Username = user!.Name,
|
||||
Pw = user.Password,
|
||||
},
|
||||
options: jsonOptions);
|
||||
content.Headers.Add("X-Emby-Authorization", DummyAuthHeader);
|
||||
|
||||
using var authResponse = await client.PostAsync("/Users/AuthenticateByName", content);
|
||||
using var authResponse = await client.SendAsync(httpRequest);
|
||||
authResponse.EnsureSuccessStatusCode();
|
||||
|
||||
var auth = await JsonSerializer.DeserializeAsync<AuthenticationResultDto>(
|
||||
await authResponse.Content.ReadAsStreamAsync(),
|
||||
jsonOptions);
|
||||
|
@ -0,0 +1,26 @@
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace Jellyfin.Server.Integration.Tests.Controllers;
|
||||
|
||||
public class PersonsControllerTests : IClassFixture<JellyfinApplicationFactory>
|
||||
{
|
||||
private readonly JellyfinApplicationFactory _factory;
|
||||
private static string? _accessToken;
|
||||
|
||||
public PersonsControllerTests(JellyfinApplicationFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetPerson_DoesntExist_NotFound()
|
||||
{
|
||||
var client = _factory.CreateClient();
|
||||
client.DefaultRequestHeaders.AddAuthHeader(_accessToken ??= await AuthHelper.CompleteStartupAsync(client));
|
||||
|
||||
using var response = await client.GetAsync($"Persons/DoesntExist");
|
||||
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@ using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using Emby.Server.Implementations;
|
||||
using Jellyfin.Server.Extensions;
|
||||
using Jellyfin.Server.Helpers;
|
||||
@ -105,7 +104,7 @@ namespace Jellyfin.Server.Integration.Tests
|
||||
var appHost = (TestAppHost)testServer.Services.GetRequiredService<IApplicationHost>();
|
||||
appHost.ServiceProvider = testServer.Services;
|
||||
appHost.InitializeServices().GetAwaiter().GetResult();
|
||||
appHost.RunStartupTasksAsync(CancellationToken.None).GetAwaiter().GetResult();
|
||||
appHost.RunStartupTasksAsync().GetAwaiter().GetResult();
|
||||
|
||||
return testServer;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Jellyfin.Data.Enums;
|
||||
@ -114,11 +114,11 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
|
||||
_parser.Fetch(result, "Test Data/Rising.nfo", CancellationToken.None);
|
||||
|
||||
var item = result.Item;
|
||||
Assert.Equal("Rising (1)", item.Name);
|
||||
Assert.Equal("Rising (1) / Rising (2)", item.Name);
|
||||
Assert.Equal(1, item.IndexNumber);
|
||||
Assert.Equal(2, item.IndexNumberEnd);
|
||||
Assert.Equal(1, item.ParentIndexNumber);
|
||||
Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy.", item.Overview);
|
||||
Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy. / Sheppard tries to convince Weir to mount a rescue mission to free Colonel Sumner, Teyla, and the others captured by the Wraith.", item.Overview);
|
||||
Assert.Equal(new DateTime(2004, 7, 16), item.PremiereDate);
|
||||
Assert.Equal(2004, item.ProductionYear);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user