Merge branch 'master' into network-rewrite

pull/8147/head
Shadowghost 1 year ago
commit c042f20224

@ -164,7 +164,7 @@ namespace Emby.Dlna.Eventing
try
{
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
using var response = await _httpClientFactory.CreateClient(NamedClient.DirectIp)
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
}
catch (OperationCanceledException)

@ -1,13 +0,0 @@
#pragma warning disable CS1591
namespace Emby.Server.Implementations.IO
{
public class ExtendedFileSystemInfo
{
public bool IsHidden { get; set; }
public bool IsReadOnly { get; set; }
public bool Exists { get; set; }
}
}

@ -267,25 +267,6 @@ namespace Emby.Server.Implementations.IO
return result;
}
private static ExtendedFileSystemInfo GetExtendedFileSystemInfo(string path)
{
var result = new ExtendedFileSystemInfo();
var info = new FileInfo(path);
if (info.Exists)
{
result.Exists = true;
var attributes = info.Attributes;
result.IsHidden = (attributes & FileAttributes.Hidden) == FileAttributes.Hidden;
result.IsReadOnly = (attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly;
}
return result;
}
/// <summary>
/// Takes a filename and removes invalid characters.
/// </summary>
@ -403,19 +384,18 @@ namespace Emby.Server.Implementations.IO
return;
}
var info = GetExtendedFileSystemInfo(path);
var info = new FileInfo(path);
if (info.Exists && info.IsHidden != isHidden)
if (info.Exists &&
((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) != isHidden)
{
if (isHidden)
{
File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.Hidden);
File.SetAttributes(path, info.Attributes | FileAttributes.Hidden);
}
else
{
var attributes = File.GetAttributes(path);
attributes = RemoveAttribute(attributes, FileAttributes.Hidden);
File.SetAttributes(path, attributes);
File.SetAttributes(path, info.Attributes & ~FileAttributes.Hidden);
}
}
}
@ -428,19 +408,20 @@ namespace Emby.Server.Implementations.IO
return;
}
var info = GetExtendedFileSystemInfo(path);
var info = new FileInfo(path);
if (!info.Exists)
{
return;
}
if (info.IsReadOnly == readOnly && info.IsHidden == isHidden)
if (((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) == readOnly
&& ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) == isHidden)
{
return;
}
var attributes = File.GetAttributes(path);
var attributes = info.Attributes;
if (readOnly)
{
@ -448,7 +429,7 @@ namespace Emby.Server.Implementations.IO
}
else
{
attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly);
attributes &= ~FileAttributes.ReadOnly;
}
if (isHidden)
@ -457,17 +438,12 @@ namespace Emby.Server.Implementations.IO
}
else
{
attributes = RemoveAttribute(attributes, FileAttributes.Hidden);
attributes &= ~FileAttributes.Hidden;
}
File.SetAttributes(path, attributes);
}
private static FileAttributes RemoveAttribute(FileAttributes attributes, FileAttributes attributesToRemove)
{
return attributes & ~attributesToRemove;
}
/// <summary>
/// Swaps the files.
/// </summary>

@ -0,0 +1,28 @@
{
"HeaderAlbumArtists": "Vaimbi vemadambarefu",
"HeaderContinueWatching": "Simudzira kuona",
"HeaderFavoriteSongs": "Nziyo dzaunofarira",
"Albums": "Dambarefu",
"AppDeviceValues": "Apu: {0}, Dhivhaisi: {1}",
"Application": "Purogiramu",
"Artists": "Vaimbi",
"AuthenticationSucceededWithUserName": "apinda",
"Books": "Mabhuku",
"CameraImageUploadedFrom": "Mufananidzo mutsva vabva pakamera {0}",
"Channels": "Machanewo",
"ChapterNameValue": "Chikamu {0}",
"Collections": "Akafanana",
"Default": "Zvakasarudzwa Kare",
"DeviceOfflineWithName": "{0} haasisipo",
"DeviceOnlineWithName": "{0} aripo",
"External": "Zvekunze",
"FailedLoginAttemptWithUserName": "Vatadza kuloga chimboedza kushandisa {0}",
"Favorites": "Zvaunofarira",
"Folders": "Mafoodha",
"Forced": "Zvekumanikidzira",
"Genres": "Mhando",
"HeaderFavoriteAlbums": "Madambarefu aunofarira",
"HeaderFavoriteArtists": "Vaimbi vaunofarira",
"HeaderFavoriteEpisodes": "Maepisodhi aunofarira",
"HeaderFavoriteShows": "Masirisi aunofarira"
}

@ -15,7 +15,7 @@
"Favorites": "我的最愛",
"Folders": "資料夾",
"Genres": "風格",
"HeaderAlbumArtists": "專輯藝人",
"HeaderAlbumArtists": "專輯歌手",
"HeaderContinueWatching": "繼續觀看",
"HeaderFavoriteAlbums": "最愛的專輯",
"HeaderFavoriteArtists": "最愛的藝人",

@ -38,7 +38,15 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
return Task.CompletedTask;
}
if (requirement.RequireAdmin && !context.User.IsInRole(UserRoles.Administrator))
var contextUser = context.User;
if (requirement.RequireAdmin && !contextUser.IsInRole(UserRoles.Administrator))
{
context.Fail();
return Task.CompletedTask;
}
var userId = contextUser.GetUserId();
if (userId.Equals(default))
{
context.Fail();
return Task.CompletedTask;
@ -50,7 +58,7 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
return Task.CompletedTask;
}
var user = _userManager.GetUserById(context.User.GetUserId());
var user = _userManager.GetUserById(userId);
if (user is null)
{
throw new ResourceNotFoundException();

@ -59,10 +59,12 @@ public class SystemController : BaseJellyfinApiController
/// Gets information about the server.
/// </summary>
/// <response code="200">Information retrieved.</response>
/// <response code="403">User does not have permission to retrieve information.</response>
/// <returns>A <see cref="SystemInfo"/> with info about the system.</returns>
[HttpGet("Info")]
[Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult<SystemInfo> GetSystemInfo()
{
return _appHost.GetSystemInfo(Request);
@ -97,10 +99,12 @@ public class SystemController : BaseJellyfinApiController
/// Restarts the application.
/// </summary>
/// <response code="204">Server restarted.</response>
/// <response code="403">User does not have permission to restart server.</response>
/// <returns>No content. Server restarted.</returns>
[HttpPost("Restart")]
[Authorize(Policy = Policies.LocalAccessOrRequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult RestartApplication()
{
Task.Run(async () =>
@ -115,10 +119,12 @@ public class SystemController : BaseJellyfinApiController
/// Shuts down the application.
/// </summary>
/// <response code="204">Server shut down.</response>
/// <response code="403">User does not have permission to shutdown server.</response>
/// <returns>No content. Server shut down.</returns>
[HttpPost("Shutdown")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult ShutdownApplication()
{
Task.Run(async () =>
@ -133,10 +139,12 @@ public class SystemController : BaseJellyfinApiController
/// Gets a list of available server log files.
/// </summary>
/// <response code="200">Information retrieved.</response>
/// <response code="403">User does not have permission to get server logs.</response>
/// <returns>An array of <see cref="LogFile"/> with the available log files.</returns>
[HttpGet("Logs")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult<LogFile[]> GetServerLogs()
{
IEnumerable<FileSystemMetadata> files;
@ -170,10 +178,12 @@ public class SystemController : BaseJellyfinApiController
/// Gets information about the request endpoint.
/// </summary>
/// <response code="200">Information retrieved.</response>
/// <response code="403">User does not have permission to get endpoint information.</response>
/// <returns><see cref="EndPointInfo"/> with information about the endpoint.</returns>
[HttpGet("Endpoint")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
public ActionResult<EndPointInfo> GetEndpointInfo()
{
return new EndPointInfo
@ -188,10 +198,12 @@ public class SystemController : BaseJellyfinApiController
/// </summary>
/// <param name="name">The name of the log file to get.</param>
/// <response code="200">Log file retrieved.</response>
/// <response code="403">User does not have permission to get log files.</response>
/// <returns>The log file.</returns>
[HttpGet("Logs/Log")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesFile(MediaTypeNames.Text.Plain)]
public ActionResult GetLogFile([FromQuery, Required] string name)
{

@ -0,0 +1,120 @@
/*
The MIT License (MIT)
Copyright (c) .NET Foundation and Contributors
All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
using System.IO;
using System.Net.Http;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
namespace Jellyfin.Networking.HappyEyeballs
{
/// <summary>
/// Defines the <see cref="HttpClientExtension"/> class.
///
/// Implementation taken from https://github.com/ppy/osu-framework/pull/4191 .
/// </summary>
public static class HttpClientExtension
{
/// <summary>
/// Gets or sets a value indicating whether the client should use IPv6.
/// </summary>
public static bool UseIPv6 { get; set; } = true;
/// <summary>
/// Implements the httpclient callback method.
/// </summary>
/// <param name="context">The <see cref="SocketsHttpConnectionContext"/> instance.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> instance.</param>
/// <returns>The http steam.</returns>
public static async ValueTask<Stream> OnConnect(SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
if (!UseIPv6)
{
return await AttemptConnection(AddressFamily.InterNetwork, context, cancellationToken).ConfigureAwait(false);
}
using var cancelIPv6 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var tryConnectAsyncIPv6 = AttemptConnection(AddressFamily.InterNetworkV6, context, cancelIPv6.Token);
// GetAwaiter().GetResult() is used instead of .Result as this results in improved exception handling.
// The tasks have already been completed.
// See https://github.com/dotnet/corefx/pull/29792/files#r189415885 for more details.
if (await Task.WhenAny(tryConnectAsyncIPv6, Task.Delay(200, cancelIPv6.Token)).ConfigureAwait(false) == tryConnectAsyncIPv6 && tryConnectAsyncIPv6.IsCompletedSuccessfully)
{
cancelIPv6.Cancel();
return tryConnectAsyncIPv6.GetAwaiter().GetResult();
}
using var cancelIPv4 = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var tryConnectAsyncIPv4 = AttemptConnection(AddressFamily.InterNetwork, context, cancelIPv4.Token);
if (await Task.WhenAny(tryConnectAsyncIPv6, tryConnectAsyncIPv4).ConfigureAwait(false) == tryConnectAsyncIPv6)
{
if (tryConnectAsyncIPv6.IsCompletedSuccessfully)
{
cancelIPv4.Cancel();
return tryConnectAsyncIPv6.GetAwaiter().GetResult();
}
return tryConnectAsyncIPv4.GetAwaiter().GetResult();
}
else
{
if (tryConnectAsyncIPv4.IsCompletedSuccessfully)
{
cancelIPv6.Cancel();
return tryConnectAsyncIPv4.GetAwaiter().GetResult();
}
return tryConnectAsyncIPv6.GetAwaiter().GetResult();
}
}
private static async Task<Stream> AttemptConnection(AddressFamily addressFamily, SocketsHttpConnectionContext context, CancellationToken cancellationToken)
{
// The following socket constructor will create a dual-mode socket on systems where IPV6 is available.
var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp)
{
// Turn off Nagle's algorithm since it degrades performance in most HttpClient scenarios.
NoDelay = true
};
try
{
await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false);
// The stream should take the ownership of the underlying socket,
// closing it when it's disposed.
return new NetworkStream(socket, ownsSocket: true);
}
catch
{
socket.Dispose();
throw;
}
}
}
}

@ -184,9 +184,16 @@ namespace Jellyfin.Networking.Manager
{
Thread.Sleep(2000);
var networkConfig = _configurationManager.GetNetworkConfiguration();
InitialiseLan(networkConfig);
InitialiseInterfaces();
EnforceBindSettings(networkConfig);
if (IsIPv6Enabled && !Socket.OSSupportsIPv6)
{
UpdateSettings(networkConfig);
}
else
{
InitialiseInterfaces();
InitialiseLan(networkConfig);
EnforceBindSettings(networkConfig);
}
NetworkChanged?.Invoke(this, EventArgs.Empty);
}
@ -519,6 +526,7 @@ namespace Jellyfin.Networking.Manager
ArgumentNullException.ThrowIfNull(configuration);
var config = (NetworkConfiguration)configuration;
HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6;
InitialiseLan(config);
InitialiseRemote(config);

@ -22,8 +22,7 @@ namespace Jellyfin.Server.Migrations
private static readonly Type[] _preStartupMigrationTypes =
{
typeof(PreStartupRoutines.CreateNetworkConfiguration),
typeof(PreStartupRoutines.MigrateMusicBrainzTimeout),
typeof(PreStartupRoutines.MigrateRatingLevels)
typeof(PreStartupRoutines.MigrateMusicBrainzTimeout)
};
/// <summary>
@ -41,7 +40,8 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.MigrateDisplayPreferencesDb),
typeof(Routines.RemoveDownloadImagesInAdvance),
typeof(Routines.MigrateAuthenticationDb),
typeof(Routines.FixPlaylistOwner)
typeof(Routines.FixPlaylistOwner),
typeof(Routines.MigrateRatingLevels)
};
/// <summary>

@ -1,86 +0,0 @@
using System;
using System.Globalization;
using System.IO;
using Emby.Server.Implementations;
using MediaBrowser.Controller;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
namespace Jellyfin.Server.Migrations.PreStartupRoutines
{
/// <summary>
/// Migrate rating levels to new rating level system.
/// </summary>
internal class MigrateRatingLevels : IMigrationRoutine
{
private const string DbFilename = "library.db";
private readonly ILogger<MigrateRatingLevels> _logger;
private readonly IServerApplicationPaths _applicationPaths;
public MigrateRatingLevels(ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory)
{
_applicationPaths = applicationPaths;
_logger = loggerFactory.CreateLogger<MigrateRatingLevels>();
}
/// <inheritdoc/>
public Guid Id => Guid.Parse("{67445D54-B895-4B24-9F4C-35CE0690EA07}");
/// <inheritdoc/>
public string Name => "MigrateRatingLevels";
/// <inheritdoc/>
public bool PerformOnNewInstall => false;
/// <inheritdoc/>
public void Perform()
{
var dataPath = _applicationPaths.DataPath;
var dbPath = Path.Combine(dataPath, DbFilename);
using (var connection = SQLite3.Open(
dbPath,
ConnectionFlags.ReadWrite,
null))
{
// Back up the database before deleting any entries
for (int i = 1; ; i++)
{
var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i);
if (!File.Exists(bakPath))
{
try
{
File.Copy(dbPath, bakPath);
_logger.LogInformation("Library database backed up to {BackupPath}", bakPath);
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath);
throw;
}
}
}
// Migrate parental rating levels to new schema
_logger.LogInformation("Migrating parental rating levels.");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating = 'NR'");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE InheritedParentalRatingValue = ''");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE InheritedParentalRatingValue = 0");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 1000 WHERE InheritedParentalRatingValue = 100");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 1000 WHERE InheritedParentalRatingValue = 15");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 18 WHERE InheritedParentalRatingValue = 10");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 18 WHERE InheritedParentalRatingValue = 9");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 16 WHERE InheritedParentalRatingValue = 8");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 12 WHERE InheritedParentalRatingValue = 7");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 12 WHERE InheritedParentalRatingValue = 6");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 12 WHERE InheritedParentalRatingValue = 5");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 7 WHERE InheritedParentalRatingValue = 4");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 6 WHERE InheritedParentalRatingValue = 3");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 6 WHERE InheritedParentalRatingValue = 2");
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = 0 WHERE InheritedParentalRatingValue = 1");
}
}
}
}

@ -0,0 +1,103 @@
using System;
using System.Globalization;
using System.IO;
using Emby.Server.Implementations.Data;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Globalization;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
namespace Jellyfin.Server.Migrations.Routines
{
/// <summary>
/// Migrate rating levels to new rating level system.
/// </summary>
internal class MigrateRatingLevels : IMigrationRoutine
{
private const string DbFilename = "library.db";
private readonly ILogger<MigrateRatingLevels> _logger;
private readonly IServerApplicationPaths _applicationPaths;
private readonly ILocalizationManager _localizationManager;
private readonly IItemRepository _repository;
public MigrateRatingLevels(
IServerApplicationPaths applicationPaths,
ILoggerFactory loggerFactory,
ILocalizationManager localizationManager,
IItemRepository repository)
{
_applicationPaths = applicationPaths;
_localizationManager = localizationManager;
_repository = repository;
_logger = loggerFactory.CreateLogger<MigrateRatingLevels>();
}
/// <inheritdoc/>
public Guid Id => Guid.Parse("{67445D54-B895-4B24-9F4C-35CE0690EA07}");
/// <inheritdoc/>
public string Name => "MigrateRatingLevels";
/// <inheritdoc/>
public bool PerformOnNewInstall => false;
/// <inheritdoc/>
public void Perform()
{
var dbPath = Path.Combine(_applicationPaths.DataPath, DbFilename);
// Back up the database before modifying any entries
for (int i = 1; ; i++)
{
var bakPath = string.Format(CultureInfo.InvariantCulture, "{0}.bak{1}", dbPath, i);
if (!File.Exists(bakPath))
{
try
{
File.Copy(dbPath, bakPath);
_logger.LogInformation("Library database backed up to {BackupPath}", bakPath);
break;
}
catch (Exception ex)
{
_logger.LogError(ex, "Cannot make a backup of {Library} at path {BackupPath}", DbFilename, bakPath);
throw;
}
}
}
// Migrate parental rating strings to new levels
_logger.LogInformation("Recalculating parental rating levels based on rating string.");
using (var connection = SQLite3.Open(
dbPath,
ConnectionFlags.ReadWrite,
null))
{
var queryResult = connection.Query("SELECT DISTINCT OfficialRating FROM TypedBaseItems");
foreach (var entry in queryResult)
{
var ratingString = entry[0].ToString();
if (string.IsNullOrEmpty(ratingString))
{
connection.Execute("UPDATE TypedBaseItems SET InheritedParentalRatingValue = NULL WHERE OfficialRating IS NULL OR OfficialRating='';");
}
else
{
var ratingValue = _localizationManager.GetRatingLevel(ratingString).ToString();
if (string.IsNullOrEmpty(ratingValue))
{
ratingValue = "NULL";
}
var statement = connection.PrepareStatement("UPDATE TypedBaseItems SET InheritedParentalRatingValue = @Value WHERE OfficialRating = @Rating;");
statement.TryBind("@Value", ratingValue);
statement.TryBind("@Rating", ratingString);
statement.ExecuteQuery();
}
}
}
}
}
}

@ -8,6 +8,7 @@ using System.Text;
using Jellyfin.Api.Middleware;
using Jellyfin.MediaEncoding.Hls.Extensions;
using Jellyfin.Networking.Configuration;
using Jellyfin.Networking.HappyEyeballs;
using Jellyfin.Server.Extensions;
using Jellyfin.Server.HealthChecks;
using Jellyfin.Server.Implementations;
@ -26,6 +27,7 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.VisualBasic;
using Prometheus;
namespace Jellyfin.Server
@ -78,6 +80,13 @@ namespace Jellyfin.Server
var acceptJsonHeader = new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json, 1.0);
var acceptXmlHeader = new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Xml, 0.9);
var acceptAnyHeader = new MediaTypeWithQualityHeaderValue("*/*", 0.8);
Func<IServiceProvider, HttpMessageHandler> eyeballsHttpClientHandlerDelegate = (_) => new SocketsHttpHandler()
{
AutomaticDecompression = DecompressionMethods.All,
RequestHeaderEncodingSelector = (_, _) => Encoding.UTF8,
ConnectCallback = HttpClientExtension.OnConnect
};
Func<IServiceProvider, HttpMessageHandler> defaultHttpClientHandlerDelegate = (_) => new SocketsHttpHandler()
{
AutomaticDecompression = DecompressionMethods.All,
@ -91,7 +100,7 @@ namespace Jellyfin.Server
c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader);
c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader);
})
.ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate);
.ConfigurePrimaryHttpMessageHandler(eyeballsHttpClientHandlerDelegate);
services.AddHttpClient(NamedClient.MusicBrainz, c =>
{
@ -100,6 +109,15 @@ namespace Jellyfin.Server
c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader);
c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader);
})
.ConfigurePrimaryHttpMessageHandler(eyeballsHttpClientHandlerDelegate);
services.AddHttpClient(NamedClient.DirectIp, c =>
{
c.DefaultRequestHeaders.UserAgent.Add(productHeader);
c.DefaultRequestHeaders.Accept.Add(acceptJsonHeader);
c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader);
c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader);
})
.ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate);
services.AddHttpClient(NamedClient.Dlna, c =>

@ -1,4 +1,4 @@
namespace MediaBrowser.Common.Net
namespace MediaBrowser.Common.Net
{
/// <summary>
/// Registered http client names.
@ -6,7 +6,7 @@
public static class NamedClient
{
/// <summary>
/// Gets the value for the default named http client.
/// Gets the value for the default named http client which implements happy eyeballs.
/// </summary>
public const string Default = nameof(Default);
@ -19,5 +19,10 @@
/// Gets the value for the DLNA named http client.
/// </summary>
public const string Dlna = nameof(Dlna);
/// <summary>
/// Non happy eyeballs implementation.
/// </summary>
public const string DirectIp = nameof(DirectIp);
}
}

@ -32,6 +32,7 @@ namespace MediaBrowser.Providers.Manager
private readonly ILogger _logger;
private readonly IProviderManager _providerManager;
private readonly IFileSystem _fileSystem;
private static readonly ImageType[] AllImageTypes = Enum.GetValues<ImageType>();
/// <summary>
/// Image types that are only one per item.
@ -90,11 +91,12 @@ namespace MediaBrowser.Providers.Manager
/// </summary>
/// <param name="item">The <see cref="BaseItem"/> to validate images for.</param>
/// <param name="providers">The providers to use, must include <see cref="ILocalImageProvider"/>(s) for local scanning.</param>
/// <param name="directoryService">The directory service for <see cref="ILocalImageProvider"/>s to use.</param>
/// <param name="refreshOptions">The refresh options.</param>
/// <returns><c>true</c> if changes were made to the item; otherwise <c>false</c>.</returns>
public bool ValidateImages(BaseItem item, IEnumerable<IImageProvider> providers, IDirectoryService directoryService)
public bool ValidateImages(BaseItem item, IEnumerable<IImageProvider> providers, ImageRefreshOptions refreshOptions)
{
var hasChanges = false;
IDirectoryService directoryService = refreshOptions?.DirectoryService;
if (item is not Photo)
{
@ -102,7 +104,7 @@ namespace MediaBrowser.Providers.Manager
.SelectMany(i => i.GetImages(item, directoryService))
.ToList();
if (MergeImages(item, images))
if (MergeImages(item, images, refreshOptions))
{
hasChanges = true;
}
@ -381,15 +383,36 @@ namespace MediaBrowser.Providers.Manager
item.RemoveImages(images);
}
/// <summary>
/// Merges a list of images into the provided item, validating existing images and replacing them or adding new images as necessary.
/// </summary>
/// <param name="refreshOptions">The refresh options.</param>
/// <param name="dontReplaceImages">List of imageTypes to remove from ReplaceImages.</param>
public void UpdateReplaceImages(ImageRefreshOptions refreshOptions, ICollection<ImageType> dontReplaceImages)
{
if (refreshOptions is not null)
{
if (refreshOptions.ReplaceAllImages)
{
refreshOptions.ReplaceAllImages = false;
refreshOptions.ReplaceImages = AllImageTypes.ToList();
}
refreshOptions.ReplaceImages = refreshOptions.ReplaceImages.Except(dontReplaceImages).ToList();
}
}
/// <summary>
/// Merges a list of images into the provided item, validating existing images and replacing them or adding new images as necessary.
/// </summary>
/// <param name="item">The <see cref="BaseItem"/> to modify.</param>
/// <param name="images">The new images to place in <c>item</c>.</param>
/// <param name="refreshOptions">The refresh options.</param>
/// <returns><c>true</c> if changes were made to the item; otherwise <c>false</c>.</returns>
public bool MergeImages(BaseItem item, IReadOnlyList<LocalImageInfo> images)
public bool MergeImages(BaseItem item, IReadOnlyList<LocalImageInfo> images, ImageRefreshOptions refreshOptions)
{
var changed = item.ValidateImages();
var foundImageTypes = new List<ImageType>();
for (var i = 0; i < _singularImages.Length; i++)
{
@ -399,6 +422,11 @@ namespace MediaBrowser.Providers.Manager
if (image is not null)
{
var currentImage = item.GetImageInfo(type, 0);
// if image file is stored with media, don't replace that later
if (item.ContainingFolderPath is not null && item.ContainingFolderPath.Contains(Path.GetDirectoryName(image.FileInfo.FullName), StringComparison.OrdinalIgnoreCase))
{
foundImageTypes.Add(type);
}
if (currentImage is null || !string.Equals(currentImage.Path, image.FileInfo.FullName, StringComparison.OrdinalIgnoreCase))
{
@ -425,6 +453,12 @@ namespace MediaBrowser.Providers.Manager
if (UpdateMultiImages(item, images, ImageType.Backdrop))
{
changed = true;
foundImageTypes.Add(ImageType.Backdrop);
}
if (foundImageTypes.Count > 0)
{
UpdateReplaceImages(refreshOptions, foundImageTypes);
}
return changed;

@ -26,8 +26,6 @@ namespace MediaBrowser.Providers.Manager
where TItemType : BaseItem, IHasLookupInfo<TIdType>, new()
where TIdType : ItemLookupInfo, new()
{
private static readonly ImageType[] AllImageTypes = Enum.GetValues<ImageType>();
protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger<MetadataService<TItemType, TIdType>> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager)
{
ServerConfigurationManager = serverConfigurationManager;
@ -110,7 +108,7 @@ namespace MediaBrowser.Providers.Manager
try
{
// Always validate images and check for new locally stored ones.
if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions.DirectoryService))
if (ImageProvider.ValidateImages(item, allImageProviders.OfType<ILocalImageProvider>(), refreshOptions))
{
updateType |= ItemUpdateType.ImageUpdate;
}
@ -674,8 +672,7 @@ namespace MediaBrowser.Providers.Manager
}
var hasLocalMetadata = false;
var replaceImages = AllImageTypes.ToList();
var localImagesFound = false;
var foundImageTypes = new List<ImageType>();
foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
{
@ -703,9 +700,8 @@ namespace MediaBrowser.Providers.Manager
await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false);
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
// remove imagetype that has just been downloaded
replaceImages.Remove(remoteImage.Type);
localImagesFound = true;
// remember imagetype that has just been downloaded
foundImageTypes.Add(remoteImage.Type);
}
catch (HttpRequestException ex)
{
@ -713,13 +709,12 @@ namespace MediaBrowser.Providers.Manager
}
}
if (localImagesFound)
if (foundImageTypes.Count > 0)
{
options.ReplaceAllImages = false;
options.ReplaceImages = replaceImages;
imageService.UpdateReplaceImages(options, foundImageTypes);
}
if (imageService.MergeImages(item, localItem.Images))
if (imageService.MergeImages(item, localItem.Images, options))
{
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
}

@ -1,4 +1,4 @@
FROM fedora:36
FROM fedora:39
# Docker build arguments
ARG SOURCE_DIR=/jellyfin
ARG ARTIFACT_DIR=/dist

@ -94,7 +94,7 @@ namespace Jellyfin.Providers.Tests.Manager
public void MergeImages_EmptyItemNewImagesEmpty_NoChange()
{
var itemImageProvider = GetItemImageProvider(null, null);
var changed = itemImageProvider.MergeImages(new Video(), Array.Empty<LocalImageInfo>());
var changed = itemImageProvider.MergeImages(new Video(), Array.Empty<LocalImageInfo>(), new ImageRefreshOptions(Mock.Of<IDirectoryService>()));
Assert.False(changed);
}
@ -108,7 +108,7 @@ namespace Jellyfin.Providers.Tests.Manager
var images = GetImages(imageType, imageCount, false);
var itemImageProvider = GetItemImageProvider(null, null);
var changed = itemImageProvider.MergeImages(item, images);
var changed = itemImageProvider.MergeImages(item, images, new ImageRefreshOptions(Mock.Of<IDirectoryService>()));
Assert.True(changed);
// adds for types that allow multiple, replaces singular type images
@ -151,7 +151,7 @@ namespace Jellyfin.Providers.Tests.Manager
var images = GetImages(imageType, imageCount, true);
var itemImageProvider = GetItemImageProvider(null, fileSystem);
var changed = itemImageProvider.MergeImages(item, images);
var changed = itemImageProvider.MergeImages(item, images, new ImageRefreshOptions(Mock.Of<IDirectoryService>()));
if (updateTime)
{

Loading…
Cancel
Save