mirror of https://github.com/jellyfin/jellyfin.git
Merge branch 'master' into network-rewrite
This commit is contained in:
commit
c042f20224
|
@ -164,7 +164,7 @@ namespace Emby.Dlna.Eventing
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var response = await _httpClientFactory.CreateClient(NamedClient.Default)
|
using var response = await _httpClientFactory.CreateClient(NamedClient.DirectIp)
|
||||||
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
|
.SendAsync(options, HttpCompletionOption.ResponseHeadersRead).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
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;
|
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>
|
/// <summary>
|
||||||
/// Takes a filename and removes invalid characters.
|
/// Takes a filename and removes invalid characters.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -403,19 +384,18 @@ namespace Emby.Server.Implementations.IO
|
||||||
return;
|
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)
|
if (isHidden)
|
||||||
{
|
{
|
||||||
File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.Hidden);
|
File.SetAttributes(path, info.Attributes | FileAttributes.Hidden);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var attributes = File.GetAttributes(path);
|
File.SetAttributes(path, info.Attributes & ~FileAttributes.Hidden);
|
||||||
attributes = RemoveAttribute(attributes, FileAttributes.Hidden);
|
|
||||||
File.SetAttributes(path, attributes);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -428,19 +408,20 @@ namespace Emby.Server.Implementations.IO
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var info = GetExtendedFileSystemInfo(path);
|
var info = new FileInfo(path);
|
||||||
|
|
||||||
if (!info.Exists)
|
if (!info.Exists)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.IsReadOnly == readOnly && info.IsHidden == isHidden)
|
if (((info.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly) == readOnly
|
||||||
|
&& ((info.Attributes & FileAttributes.Hidden) == FileAttributes.Hidden) == isHidden)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var attributes = File.GetAttributes(path);
|
var attributes = info.Attributes;
|
||||||
|
|
||||||
if (readOnly)
|
if (readOnly)
|
||||||
{
|
{
|
||||||
|
@ -448,7 +429,7 @@ namespace Emby.Server.Implementations.IO
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly);
|
attributes &= ~FileAttributes.ReadOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isHidden)
|
if (isHidden)
|
||||||
|
@ -457,17 +438,12 @@ namespace Emby.Server.Implementations.IO
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
attributes = RemoveAttribute(attributes, FileAttributes.Hidden);
|
attributes &= ~FileAttributes.Hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
File.SetAttributes(path, attributes);
|
File.SetAttributes(path, attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static FileAttributes RemoveAttribute(FileAttributes attributes, FileAttributes attributesToRemove)
|
|
||||||
{
|
|
||||||
return attributes & ~attributesToRemove;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Swaps the files.
|
/// Swaps the files.
|
||||||
/// </summary>
|
/// </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": "我的最愛",
|
"Favorites": "我的最愛",
|
||||||
"Folders": "資料夾",
|
"Folders": "資料夾",
|
||||||
"Genres": "風格",
|
"Genres": "風格",
|
||||||
"HeaderAlbumArtists": "專輯藝人",
|
"HeaderAlbumArtists": "專輯歌手",
|
||||||
"HeaderContinueWatching": "繼續觀看",
|
"HeaderContinueWatching": "繼續觀看",
|
||||||
"HeaderFavoriteAlbums": "最愛的專輯",
|
"HeaderFavoriteAlbums": "最愛的專輯",
|
||||||
"HeaderFavoriteArtists": "最愛的藝人",
|
"HeaderFavoriteArtists": "最愛的藝人",
|
||||||
|
|
|
@ -38,7 +38,15 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
|
||||||
return Task.CompletedTask;
|
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();
|
context.Fail();
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
@ -50,7 +58,7 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupPolicy
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = _userManager.GetUserById(context.User.GetUserId());
|
var user = _userManager.GetUserById(userId);
|
||||||
if (user is null)
|
if (user is null)
|
||||||
{
|
{
|
||||||
throw new ResourceNotFoundException();
|
throw new ResourceNotFoundException();
|
||||||
|
|
|
@ -59,10 +59,12 @@ public class SystemController : BaseJellyfinApiController
|
||||||
/// Gets information about the server.
|
/// Gets information about the server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200">Information retrieved.</response>
|
/// <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>
|
/// <returns>A <see cref="SystemInfo"/> with info about the system.</returns>
|
||||||
[HttpGet("Info")]
|
[HttpGet("Info")]
|
||||||
[Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)]
|
[Authorize(Policy = Policies.FirstTimeSetupOrIgnoreParentalControl)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public ActionResult<SystemInfo> GetSystemInfo()
|
public ActionResult<SystemInfo> GetSystemInfo()
|
||||||
{
|
{
|
||||||
return _appHost.GetSystemInfo(Request);
|
return _appHost.GetSystemInfo(Request);
|
||||||
|
@ -97,10 +99,12 @@ public class SystemController : BaseJellyfinApiController
|
||||||
/// Restarts the application.
|
/// Restarts the application.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="204">Server restarted.</response>
|
/// <response code="204">Server restarted.</response>
|
||||||
|
/// <response code="403">User does not have permission to restart server.</response>
|
||||||
/// <returns>No content. Server restarted.</returns>
|
/// <returns>No content. Server restarted.</returns>
|
||||||
[HttpPost("Restart")]
|
[HttpPost("Restart")]
|
||||||
[Authorize(Policy = Policies.LocalAccessOrRequiresElevation)]
|
[Authorize(Policy = Policies.LocalAccessOrRequiresElevation)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public ActionResult RestartApplication()
|
public ActionResult RestartApplication()
|
||||||
{
|
{
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
|
@ -115,10 +119,12 @@ public class SystemController : BaseJellyfinApiController
|
||||||
/// Shuts down the application.
|
/// Shuts down the application.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="204">Server shut down.</response>
|
/// <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>
|
/// <returns>No content. Server shut down.</returns>
|
||||||
[HttpPost("Shutdown")]
|
[HttpPost("Shutdown")]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public ActionResult ShutdownApplication()
|
public ActionResult ShutdownApplication()
|
||||||
{
|
{
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
|
@ -133,10 +139,12 @@ public class SystemController : BaseJellyfinApiController
|
||||||
/// Gets a list of available server log files.
|
/// Gets a list of available server log files.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200">Information retrieved.</response>
|
/// <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>
|
/// <returns>An array of <see cref="LogFile"/> with the available log files.</returns>
|
||||||
[HttpGet("Logs")]
|
[HttpGet("Logs")]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public ActionResult<LogFile[]> GetServerLogs()
|
public ActionResult<LogFile[]> GetServerLogs()
|
||||||
{
|
{
|
||||||
IEnumerable<FileSystemMetadata> files;
|
IEnumerable<FileSystemMetadata> files;
|
||||||
|
@ -170,10 +178,12 @@ public class SystemController : BaseJellyfinApiController
|
||||||
/// Gets information about the request endpoint.
|
/// Gets information about the request endpoint.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <response code="200">Information retrieved.</response>
|
/// <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>
|
/// <returns><see cref="EndPointInfo"/> with information about the endpoint.</returns>
|
||||||
[HttpGet("Endpoint")]
|
[HttpGet("Endpoint")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
public ActionResult<EndPointInfo> GetEndpointInfo()
|
public ActionResult<EndPointInfo> GetEndpointInfo()
|
||||||
{
|
{
|
||||||
return new EndPointInfo
|
return new EndPointInfo
|
||||||
|
@ -188,10 +198,12 @@ public class SystemController : BaseJellyfinApiController
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="name">The name of the log file to get.</param>
|
/// <param name="name">The name of the log file to get.</param>
|
||||||
/// <response code="200">Log file retrieved.</response>
|
/// <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>
|
/// <returns>The log file.</returns>
|
||||||
[HttpGet("Logs/Log")]
|
[HttpGet("Logs/Log")]
|
||||||
[Authorize(Policy = Policies.RequiresElevation)]
|
[Authorize(Policy = Policies.RequiresElevation)]
|
||||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||||
[ProducesFile(MediaTypeNames.Text.Plain)]
|
[ProducesFile(MediaTypeNames.Text.Plain)]
|
||||||
public ActionResult GetLogFile([FromQuery, Required] string name)
|
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);
|
Thread.Sleep(2000);
|
||||||
var networkConfig = _configurationManager.GetNetworkConfiguration();
|
var networkConfig = _configurationManager.GetNetworkConfiguration();
|
||||||
InitialiseLan(networkConfig);
|
if (IsIPv6Enabled && !Socket.OSSupportsIPv6)
|
||||||
InitialiseInterfaces();
|
{
|
||||||
EnforceBindSettings(networkConfig);
|
UpdateSettings(networkConfig);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
InitialiseInterfaces();
|
||||||
|
InitialiseLan(networkConfig);
|
||||||
|
EnforceBindSettings(networkConfig);
|
||||||
|
}
|
||||||
|
|
||||||
NetworkChanged?.Invoke(this, EventArgs.Empty);
|
NetworkChanged?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
@ -519,6 +526,7 @@ namespace Jellyfin.Networking.Manager
|
||||||
ArgumentNullException.ThrowIfNull(configuration);
|
ArgumentNullException.ThrowIfNull(configuration);
|
||||||
|
|
||||||
var config = (NetworkConfiguration)configuration;
|
var config = (NetworkConfiguration)configuration;
|
||||||
|
HappyEyeballs.HttpClientExtension.UseIPv6 = config.EnableIPv6;
|
||||||
|
|
||||||
InitialiseLan(config);
|
InitialiseLan(config);
|
||||||
InitialiseRemote(config);
|
InitialiseRemote(config);
|
||||||
|
|
|
@ -22,8 +22,7 @@ namespace Jellyfin.Server.Migrations
|
||||||
private static readonly Type[] _preStartupMigrationTypes =
|
private static readonly Type[] _preStartupMigrationTypes =
|
||||||
{
|
{
|
||||||
typeof(PreStartupRoutines.CreateNetworkConfiguration),
|
typeof(PreStartupRoutines.CreateNetworkConfiguration),
|
||||||
typeof(PreStartupRoutines.MigrateMusicBrainzTimeout),
|
typeof(PreStartupRoutines.MigrateMusicBrainzTimeout)
|
||||||
typeof(PreStartupRoutines.MigrateRatingLevels)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -41,7 +40,8 @@ namespace Jellyfin.Server.Migrations
|
||||||
typeof(Routines.MigrateDisplayPreferencesDb),
|
typeof(Routines.MigrateDisplayPreferencesDb),
|
||||||
typeof(Routines.RemoveDownloadImagesInAdvance),
|
typeof(Routines.RemoveDownloadImagesInAdvance),
|
||||||
typeof(Routines.MigrateAuthenticationDb),
|
typeof(Routines.MigrateAuthenticationDb),
|
||||||
typeof(Routines.FixPlaylistOwner)
|
typeof(Routines.FixPlaylistOwner),
|
||||||
|
typeof(Routines.MigrateRatingLevels)
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <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.Api.Middleware;
|
||||||
using Jellyfin.MediaEncoding.Hls.Extensions;
|
using Jellyfin.MediaEncoding.Hls.Extensions;
|
||||||
using Jellyfin.Networking.Configuration;
|
using Jellyfin.Networking.Configuration;
|
||||||
|
using Jellyfin.Networking.HappyEyeballs;
|
||||||
using Jellyfin.Server.Extensions;
|
using Jellyfin.Server.Extensions;
|
||||||
using Jellyfin.Server.HealthChecks;
|
using Jellyfin.Server.HealthChecks;
|
||||||
using Jellyfin.Server.Implementations;
|
using Jellyfin.Server.Implementations;
|
||||||
|
@ -26,6 +27,7 @@ using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.FileProviders;
|
using Microsoft.Extensions.FileProviders;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.VisualBasic;
|
||||||
using Prometheus;
|
using Prometheus;
|
||||||
|
|
||||||
namespace Jellyfin.Server
|
namespace Jellyfin.Server
|
||||||
|
@ -78,6 +80,13 @@ namespace Jellyfin.Server
|
||||||
var acceptJsonHeader = new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json, 1.0);
|
var acceptJsonHeader = new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json, 1.0);
|
||||||
var acceptXmlHeader = new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Xml, 0.9);
|
var acceptXmlHeader = new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Xml, 0.9);
|
||||||
var acceptAnyHeader = new MediaTypeWithQualityHeaderValue("*/*", 0.8);
|
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()
|
Func<IServiceProvider, HttpMessageHandler> defaultHttpClientHandlerDelegate = (_) => new SocketsHttpHandler()
|
||||||
{
|
{
|
||||||
AutomaticDecompression = DecompressionMethods.All,
|
AutomaticDecompression = DecompressionMethods.All,
|
||||||
|
@ -91,7 +100,7 @@ namespace Jellyfin.Server
|
||||||
c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader);
|
c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader);
|
||||||
c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader);
|
c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader);
|
||||||
})
|
})
|
||||||
.ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate);
|
.ConfigurePrimaryHttpMessageHandler(eyeballsHttpClientHandlerDelegate);
|
||||||
|
|
||||||
services.AddHttpClient(NamedClient.MusicBrainz, c =>
|
services.AddHttpClient(NamedClient.MusicBrainz, c =>
|
||||||
{
|
{
|
||||||
|
@ -100,6 +109,15 @@ namespace Jellyfin.Server
|
||||||
c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader);
|
c.DefaultRequestHeaders.Accept.Add(acceptXmlHeader);
|
||||||
c.DefaultRequestHeaders.Accept.Add(acceptAnyHeader);
|
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);
|
.ConfigurePrimaryHttpMessageHandler(defaultHttpClientHandlerDelegate);
|
||||||
|
|
||||||
services.AddHttpClient(NamedClient.Dlna, c =>
|
services.AddHttpClient(NamedClient.Dlna, c =>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
namespace MediaBrowser.Common.Net
|
namespace MediaBrowser.Common.Net
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registered http client names.
|
/// Registered http client names.
|
||||||
|
@ -6,7 +6,7 @@
|
||||||
public static class NamedClient
|
public static class NamedClient
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the value for the default named http client.
|
/// Gets the value for the default named http client which implements happy eyeballs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Default = nameof(Default);
|
public const string Default = nameof(Default);
|
||||||
|
|
||||||
|
@ -19,5 +19,10 @@
|
||||||
/// Gets the value for the DLNA named http client.
|
/// Gets the value for the DLNA named http client.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string Dlna = nameof(Dlna);
|
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 ILogger _logger;
|
||||||
private readonly IProviderManager _providerManager;
|
private readonly IProviderManager _providerManager;
|
||||||
private readonly IFileSystem _fileSystem;
|
private readonly IFileSystem _fileSystem;
|
||||||
|
private static readonly ImageType[] AllImageTypes = Enum.GetValues<ImageType>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Image types that are only one per item.
|
/// Image types that are only one per item.
|
||||||
|
@ -90,11 +91,12 @@ namespace MediaBrowser.Providers.Manager
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="item">The <see cref="BaseItem"/> to validate images for.</param>
|
/// <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="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>
|
/// <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;
|
var hasChanges = false;
|
||||||
|
IDirectoryService directoryService = refreshOptions?.DirectoryService;
|
||||||
|
|
||||||
if (item is not Photo)
|
if (item is not Photo)
|
||||||
{
|
{
|
||||||
|
@ -102,7 +104,7 @@ namespace MediaBrowser.Providers.Manager
|
||||||
.SelectMany(i => i.GetImages(item, directoryService))
|
.SelectMany(i => i.GetImages(item, directoryService))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
if (MergeImages(item, images))
|
if (MergeImages(item, images, refreshOptions))
|
||||||
{
|
{
|
||||||
hasChanges = true;
|
hasChanges = true;
|
||||||
}
|
}
|
||||||
|
@ -381,15 +383,36 @@ namespace MediaBrowser.Providers.Manager
|
||||||
item.RemoveImages(images);
|
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>
|
/// <summary>
|
||||||
/// Merges a list of images into the provided item, validating existing images and replacing them or adding new images as necessary.
|
/// Merges a list of images into the provided item, validating existing images and replacing them or adding new images as necessary.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="item">The <see cref="BaseItem"/> to modify.</param>
|
/// <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="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>
|
/// <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 changed = item.ValidateImages();
|
||||||
|
var foundImageTypes = new List<ImageType>();
|
||||||
|
|
||||||
for (var i = 0; i < _singularImages.Length; i++)
|
for (var i = 0; i < _singularImages.Length; i++)
|
||||||
{
|
{
|
||||||
|
@ -399,6 +422,11 @@ namespace MediaBrowser.Providers.Manager
|
||||||
if (image is not null)
|
if (image is not null)
|
||||||
{
|
{
|
||||||
var currentImage = item.GetImageInfo(type, 0);
|
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))
|
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))
|
if (UpdateMultiImages(item, images, ImageType.Backdrop))
|
||||||
{
|
{
|
||||||
changed = true;
|
changed = true;
|
||||||
|
foundImageTypes.Add(ImageType.Backdrop);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (foundImageTypes.Count > 0)
|
||||||
|
{
|
||||||
|
UpdateReplaceImages(refreshOptions, foundImageTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
return changed;
|
return changed;
|
||||||
|
|
|
@ -26,8 +26,6 @@ namespace MediaBrowser.Providers.Manager
|
||||||
where TItemType : BaseItem, IHasLookupInfo<TIdType>, new()
|
where TItemType : BaseItem, IHasLookupInfo<TIdType>, new()
|
||||||
where TIdType : ItemLookupInfo, 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)
|
protected MetadataService(IServerConfigurationManager serverConfigurationManager, ILogger<MetadataService<TItemType, TIdType>> logger, IProviderManager providerManager, IFileSystem fileSystem, ILibraryManager libraryManager)
|
||||||
{
|
{
|
||||||
ServerConfigurationManager = serverConfigurationManager;
|
ServerConfigurationManager = serverConfigurationManager;
|
||||||
|
@ -110,7 +108,7 @@ namespace MediaBrowser.Providers.Manager
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Always validate images and check for new locally stored ones.
|
// 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;
|
updateType |= ItemUpdateType.ImageUpdate;
|
||||||
}
|
}
|
||||||
|
@ -674,8 +672,7 @@ namespace MediaBrowser.Providers.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
var hasLocalMetadata = false;
|
var hasLocalMetadata = false;
|
||||||
var replaceImages = AllImageTypes.ToList();
|
var foundImageTypes = new List<ImageType>();
|
||||||
var localImagesFound = false;
|
|
||||||
|
|
||||||
foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>())
|
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);
|
await ProviderManager.SaveImage(item, remoteImage.Url, remoteImage.Type, null, cancellationToken).ConfigureAwait(false);
|
||||||
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
||||||
|
|
||||||
// remove imagetype that has just been downloaded
|
// remember imagetype that has just been downloaded
|
||||||
replaceImages.Remove(remoteImage.Type);
|
foundImageTypes.Add(remoteImage.Type);
|
||||||
localImagesFound = true;
|
|
||||||
}
|
}
|
||||||
catch (HttpRequestException ex)
|
catch (HttpRequestException ex)
|
||||||
{
|
{
|
||||||
|
@ -713,13 +709,12 @@ namespace MediaBrowser.Providers.Manager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (localImagesFound)
|
if (foundImageTypes.Count > 0)
|
||||||
{
|
{
|
||||||
options.ReplaceAllImages = false;
|
imageService.UpdateReplaceImages(options, foundImageTypes);
|
||||||
options.ReplaceImages = replaceImages;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (imageService.MergeImages(item, localItem.Images))
|
if (imageService.MergeImages(item, localItem.Images, options))
|
||||||
{
|
{
|
||||||
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
refreshResult.UpdateType |= ItemUpdateType.ImageUpdate;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM fedora:36
|
FROM fedora:39
|
||||||
# Docker build arguments
|
# Docker build arguments
|
||||||
ARG SOURCE_DIR=/jellyfin
|
ARG SOURCE_DIR=/jellyfin
|
||||||
ARG ARTIFACT_DIR=/dist
|
ARG ARTIFACT_DIR=/dist
|
||||||
|
|
|
@ -94,7 +94,7 @@ namespace Jellyfin.Providers.Tests.Manager
|
||||||
public void MergeImages_EmptyItemNewImagesEmpty_NoChange()
|
public void MergeImages_EmptyItemNewImagesEmpty_NoChange()
|
||||||
{
|
{
|
||||||
var itemImageProvider = GetItemImageProvider(null, null);
|
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);
|
Assert.False(changed);
|
||||||
}
|
}
|
||||||
|
@ -108,7 +108,7 @@ namespace Jellyfin.Providers.Tests.Manager
|
||||||
var images = GetImages(imageType, imageCount, false);
|
var images = GetImages(imageType, imageCount, false);
|
||||||
|
|
||||||
var itemImageProvider = GetItemImageProvider(null, null);
|
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);
|
Assert.True(changed);
|
||||||
// adds for types that allow multiple, replaces singular type images
|
// 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 images = GetImages(imageType, imageCount, true);
|
||||||
|
|
||||||
var itemImageProvider = GetItemImageProvider(null, fileSystem);
|
var itemImageProvider = GetItemImageProvider(null, fileSystem);
|
||||||
var changed = itemImageProvider.MergeImages(item, images);
|
var changed = itemImageProvider.MergeImages(item, images, new ImageRefreshOptions(Mock.Of<IDirectoryService>()));
|
||||||
|
|
||||||
if (updateTime)
|
if (updateTime)
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue