aboutsummaryrefslogtreecommitdiff
path: root/Jellyfin.Server
diff options
context:
space:
mode:
authorJPVenson <github@jpb.email>2025-03-25 15:12:48 +0000
committerJPVenson <github@jpb.email>2025-03-25 15:12:48 +0000
commit850f1c79f1319de56a300c1d62565c9b2793b7d8 (patch)
treef307a380c63d80809fed23c7ce2c8a0d0aba5de8 /Jellyfin.Server
parentef7f6fc8a97118df7f410a7afa2f501f3f4ca3e2 (diff)
parent671d801d9f734665d0acbd441246712ad2e3d91f (diff)
Merge branch 'master' into feature/DatabaseRefactor
Diffstat (limited to 'Jellyfin.Server')
-rw-r--r--Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs22
-rw-r--r--Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs24
-rw-r--r--Jellyfin.Server/Program.cs35
-rw-r--r--Jellyfin.Server/ServerSetupApp/SetupServer.cs172
4 files changed, 237 insertions, 16 deletions
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
index 30b453d60..f183bce10 100644
--- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
+++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs
@@ -94,7 +94,7 @@ public class MigrateLibraryDb : IMigrationRoutine
Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId,
DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, SeasonName, SeasonId, SeriesId,
PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate,
- ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType FROM TypedBaseItems
+ ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName, CleanName FROM TypedBaseItems
""";
dbContext.BaseItems.ExecuteDelete();
@@ -168,7 +168,6 @@ public class MigrateLibraryDb : IMigrationRoutine
dbContext.UserData.ExecuteDelete();
var users = dbContext.Users.AsNoTracking().ToImmutableArray();
- var oldUserdata = new Dictionary<string, UserData>();
foreach (var entity in queryResult)
{
@@ -189,6 +188,8 @@ public class MigrateLibraryDb : IMigrationRoutine
dbContext.UserData.Add(userData);
}
+ users.Clear();
+ legacyBaseItemWithUserKeys.Clear();
_logger.LogInformation("Try saving {0} UserData entries.", dbContext.UserData.Local.Count);
dbContext.SaveChanges();
@@ -225,11 +226,12 @@ public class MigrateLibraryDb : IMigrationRoutine
dbContext.PeopleBaseItemMap.ExecuteDelete();
var peopleCache = new Dictionary<string, (People Person, List<PeopleBaseItemMap> Items)>();
+ var baseItemIds = dbContext.BaseItems.Select(b => b.Id).ToHashSet();
foreach (SqliteDataReader reader in connection.Query(personsQuery))
{
var itemId = reader.GetGuid(0);
- if (!dbContext.BaseItems.Any(f => f.Id == itemId))
+ if (!baseItemIds.Contains(itemId))
{
_logger.LogError("Dont save person {0} because its not in use by any BaseItem", reader.GetString(1));
continue;
@@ -261,12 +263,16 @@ public class MigrateLibraryDb : IMigrationRoutine
});
}
+ baseItemIds.Clear();
+
foreach (var item in peopleCache)
{
dbContext.Peoples.Add(item.Value.Person);
dbContext.PeopleBaseItemMap.AddRange(item.Value.Items.DistinctBy(e => (e.ItemId, e.PeopleId)));
}
+ peopleCache.Clear();
+
_logger.LogInformation("Try saving {0} People entries.", dbContext.MediaStreamInfos.Local.Count);
dbContext.SaveChanges();
migrationTotalTime += stopwatch.Elapsed;
@@ -1029,6 +1035,16 @@ public class MigrateLibraryDb : IMigrationRoutine
entity.MediaType = mediaType;
}
+ if (reader.TryGetString(index++, out var sortName))
+ {
+ entity.SortName = sortName;
+ }
+
+ if (reader.TryGetString(index++, out var cleanName))
+ {
+ entity.CleanName = cleanName;
+ }
+
var baseItem = BaseItemRepository.DeserialiseBaseItem(entity, _logger, null, false);
var dataKeys = baseItem.GetUserDataKeys();
userDataKeys.AddRange(dataKeys);
diff --git a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
index c1a9e8894..f4ebac377 100644
--- a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
+++ b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs
@@ -39,7 +39,7 @@ public class MoveTrickplayFiles : IMigrationRoutine
}
/// <inheritdoc />
- public Guid Id => new("4EF123D5-8EFF-4B0B-869D-3AED07A60E1B");
+ public Guid Id => new("9540D44A-D8DC-11EF-9CBB-B77274F77C52");
/// <inheritdoc />
public string Name => "MoveTrickplayFiles";
@@ -89,6 +89,12 @@ public class MoveTrickplayFiles : IMigrationRoutine
{
_fileSystem.MoveDirectory(oldPath, newPath);
}
+
+ oldPath = GetNewOldTrickplayDirectory(item, trickplayInfo.TileWidth, trickplayInfo.TileHeight, trickplayInfo.Width, false);
+ if (_fileSystem.DirectoryExists(oldPath))
+ {
+ _fileSystem.MoveDirectory(oldPath, newPath);
+ }
}
} while (previousCount == Limit);
@@ -101,4 +107,20 @@ public class MoveTrickplayFiles : IMigrationRoutine
return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path;
}
+
+ private string GetNewOldTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false)
+ {
+ var path = saveWithMedia
+ ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay"))
+ : Path.Combine(item.GetInternalMetadataPath(), "trickplay");
+
+ var subdirectory = string.Format(
+ CultureInfo.InvariantCulture,
+ "{0} - {1}x{2}",
+ width.ToString(CultureInfo.InvariantCulture),
+ tileWidth.ToString(CultureInfo.InvariantCulture),
+ tileHeight.ToString(CultureInfo.InvariantCulture));
+
+ return Path.Combine(path, subdirectory);
+ }
}
diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs
index d0f2bafb4..d8684f6da 100644
--- a/Jellyfin.Server/Program.cs
+++ b/Jellyfin.Server/Program.cs
@@ -11,7 +11,9 @@ using Emby.Server.Implementations;
using Jellyfin.Server.Extensions;
using Jellyfin.Server.Helpers;
using Jellyfin.Server.Implementations;
+using Jellyfin.Server.ServerSetupApp;
using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Data.Sqlite;
@@ -44,6 +46,9 @@ namespace Jellyfin.Server
public const string LoggingConfigFileSystem = "logging.json";
private static readonly SerilogLoggerFactory _loggerFactory = new SerilogLoggerFactory();
+ private static SetupServer _setupServer = new();
+ private static CoreAppHost? _appHost;
+ private static IHost? _jellyfinHost = null;
private static long _startTimestamp;
private static ILogger _logger = NullLogger.Instance;
private static bool _restartOnShutdown;
@@ -70,6 +75,7 @@ namespace Jellyfin.Server
{
_startTimestamp = Stopwatch.GetTimestamp();
ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options);
+ await _setupServer.RunAsync(static () => _jellyfinHost?.Services?.GetService<INetworkManager>(), appPaths, static () => _appHost).ConfigureAwait(false);
// $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager
Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath);
@@ -124,22 +130,23 @@ namespace Jellyfin.Server
if (_restartOnShutdown)
{
_startTimestamp = Stopwatch.GetTimestamp();
+ _setupServer = new SetupServer();
+ await _setupServer.RunAsync(static () => _jellyfinHost?.Services?.GetService<INetworkManager>(), appPaths, static () => _appHost).ConfigureAwait(false);
}
} while (_restartOnShutdown);
}
private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig)
{
- using var appHost = new CoreAppHost(
- appPaths,
- _loggerFactory,
- options,
- startupConfig);
-
- IHost? host = null;
+ using CoreAppHost appHost = new CoreAppHost(
+ appPaths,
+ _loggerFactory,
+ options,
+ startupConfig);
+ _appHost = appHost;
try
{
- host = Host.CreateDefaultBuilder()
+ _jellyfinHost = Host.CreateDefaultBuilder()
.UseConsoleLifetime()
.ConfigureServices(services => appHost.Init(services))
.ConfigureWebHostDefaults(webHostBuilder =>
@@ -156,14 +163,17 @@ namespace Jellyfin.Server
.Build();
// Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection.
- appHost.ServiceProvider = host.Services;
+ appHost.ServiceProvider = _jellyfinHost.Services;
await appHost.InitializeServices(startupConfig).ConfigureAwait(false);
Migrations.MigrationRunner.Run(appHost, _loggerFactory);
try
{
- await host.StartAsync().ConfigureAwait(false);
+ await _setupServer.StopAsync().ConfigureAwait(false);
+ _setupServer.Dispose();
+ _setupServer = null!;
+ await _jellyfinHost.StartAsync().ConfigureAwait(false);
if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket())
{
@@ -182,7 +192,7 @@ namespace Jellyfin.Server
_logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp));
- await host.WaitForShutdownAsync().ConfigureAwait(false);
+ await _jellyfinHost.WaitForShutdownAsync().ConfigureAwait(false);
_restartOnShutdown = appHost.ShouldRestart;
}
catch (Exception ex)
@@ -203,7 +213,8 @@ namespace Jellyfin.Server
await databaseProvider.RunShutdownTask(shutdownSource.Token).ConfigureAwait(false);
}
- host?.Dispose();
+ _appHost = null;
+ _jellyfinHost?.Dispose();
}
}
diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs
new file mode 100644
index 000000000..9e2cf5bc8
--- /dev/null
+++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs
@@ -0,0 +1,172 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using MediaBrowser.Common.Configuration;
+using MediaBrowser.Common.Net;
+using MediaBrowser.Controller;
+using MediaBrowser.Model.System;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Hosting;
+
+namespace Jellyfin.Server.ServerSetupApp;
+
+/// <summary>
+/// Creates a fake application pipeline that will only exist for as long as the main app is not started.
+/// </summary>
+public sealed class SetupServer : IDisposable
+{
+ private IHost? _startupServer;
+ private bool _disposed;
+
+ /// <summary>
+ /// Starts the Bind-All Setup aspcore server to provide a reflection on the current core setup.
+ /// </summary>
+ /// <param name="networkManagerFactory">The networkmanager.</param>
+ /// <param name="applicationPaths">The application paths.</param>
+ /// <param name="serverApplicationHost">The servers application host.</param>
+ /// <returns>A Task.</returns>
+ public async Task RunAsync(
+ Func<INetworkManager?> networkManagerFactory,
+ IApplicationPaths applicationPaths,
+ Func<IServerApplicationHost?> serverApplicationHost)
+ {
+ ThrowIfDisposed();
+ _startupServer = Host.CreateDefaultBuilder()
+ .UseConsoleLifetime()
+ .ConfigureServices(serv =>
+ {
+ serv.AddHealthChecks()
+ .AddCheck<SetupHealthcheck>("StartupCheck");
+ })
+ .ConfigureWebHostDefaults(webHostBuilder =>
+ {
+ webHostBuilder
+ .UseKestrel()
+ .Configure(app =>
+ {
+ app.UseHealthChecks("/health");
+
+ app.Map("/startup/logger", loggerRoute =>
+ {
+ loggerRoute.Run(async context =>
+ {
+ var networkManager = networkManagerFactory();
+ if (context.Connection.RemoteIpAddress is null || networkManager is null || !networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress))
+ {
+ context.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
+ return;
+ }
+
+ var logFilePath = new DirectoryInfo(applicationPaths.LogDirectoryPath)
+ .EnumerateFiles()
+ .OrderBy(f => f.CreationTimeUtc)
+ .FirstOrDefault()
+ ?.FullName;
+ if (logFilePath is not null)
+ {
+ await context.Response.SendFileAsync(logFilePath, CancellationToken.None).ConfigureAwait(false);
+ }
+ });
+ });
+
+ app.Map("/System/Info/Public", systemRoute =>
+ {
+ systemRoute.Run(async context =>
+ {
+ var jfApplicationHost = serverApplicationHost();
+
+ var retryCounter = 0;
+ while (jfApplicationHost is null && retryCounter < 5)
+ {
+ await Task.Delay(500).ConfigureAwait(false);
+ jfApplicationHost = serverApplicationHost();
+ retryCounter++;
+ }
+
+ if (jfApplicationHost is null)
+ {
+ context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
+ context.Response.Headers.RetryAfter = new Microsoft.Extensions.Primitives.StringValues("60");
+ return;
+ }
+
+ var sysInfo = new PublicSystemInfo
+ {
+ Version = jfApplicationHost.ApplicationVersionString,
+ ProductName = jfApplicationHost.Name,
+ Id = jfApplicationHost.SystemId,
+ ServerName = jfApplicationHost.FriendlyName,
+ LocalAddress = jfApplicationHost.GetSmartApiUrl(context.Request),
+ StartupWizardCompleted = false
+ };
+
+ await context.Response.WriteAsJsonAsync(sysInfo).ConfigureAwait(false);
+ });
+ });
+
+ app.Run((context) =>
+ {
+ context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable;
+ context.Response.Headers.RetryAfter = new Microsoft.Extensions.Primitives.StringValues("60");
+ context.Response.WriteAsync("<p>Jellyfin Server still starting. Please wait.</p>");
+ var networkManager = networkManagerFactory();
+ if (networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress))
+ {
+ context.Response.WriteAsync("<p>You can download the current logfiles <a href='/startup/logger'>here</a>.</p>");
+ }
+
+ return Task.CompletedTask;
+ });
+ });
+ })
+ .Build();
+ await _startupServer.StartAsync().ConfigureAwait(false);
+ }
+
+ /// <summary>
+ /// Stops the Setup server.
+ /// </summary>
+ /// <returns>A task. Duh.</returns>
+ public async Task StopAsync()
+ {
+ ThrowIfDisposed();
+ if (_startupServer is null)
+ {
+ throw new InvalidOperationException("Tried to stop a non existing startup server");
+ }
+
+ await _startupServer.StopAsync().ConfigureAwait(false);
+ }
+
+ /// <inheritdoc/>
+ public void Dispose()
+ {
+ if (_disposed)
+ {
+ return;
+ }
+
+ _disposed = true;
+ _startupServer?.Dispose();
+ }
+
+ private void ThrowIfDisposed()
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+ }
+
+ private class SetupHealthcheck : IHealthCheck
+ {
+ public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default)
+ {
+ return Task.FromResult(HealthCheckResult.Degraded("Server is still starting up."));
+ }
+ }
+}