mirror of
https://github.com/jellyfin/jellyfin.git
synced 2025-05-24 02:02:29 -04:00
Merge branch 'master' into network-rewrite
This commit is contained in:
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>
|
||||
|
28
Emby.Server.Implementations/Localization/Core/sn.json
Normal file
28
Emby.Server.Implementations/Localization/Core/sn.json
Normal file
@ -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)
|
||||
{
|
||||
|
120
Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs
Normal file
120
Jellyfin.Networking/HappyEyeballs/HttpClientExtension.cs
Normal file
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
103
Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
Normal file
103
Jellyfin.Server/Migrations/Routines/MigrateRatingLevels.cs
Normal file
@ -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…
x
Reference in New Issue
Block a user