diff --git a/Emby.Server.Implementations/ConfigurationOptions.cs b/Emby.Server.Implementations/ConfigurationOptions.cs
index bddbbd67..bd682e6c 100644
--- a/Emby.Server.Implementations/ConfigurationOptions.cs
+++ b/Emby.Server.Implementations/ConfigurationOptions.cs
@@ -18,7 +18,6 @@ namespace Emby.Server.Implementations
{ FfmpegProbeSizeKey, "1G" },
{ FfmpegAnalyzeDurationKey, "200M" },
{ BindToUnixSocketKey, bool.FalseString },
- { SqliteCacheSizeKey, "20000" },
{ FfmpegSkipValidationKey, bool.FalseString },
{ FfmpegImgExtractPerfTradeoffKey, bool.FalseString },
{ DetectNetworkChangeKey, bool.TrueString }
diff --git a/Jellyfin.Api/Controllers/DatabaseViewsController.cs b/Jellyfin.Api/Controllers/DatabaseViewsController.cs
new file mode 100644
index 00000000..e69ae6b2
--- /dev/null
+++ b/Jellyfin.Api/Controllers/DatabaseViewsController.cs
@@ -0,0 +1,302 @@
+//
+// Copyright (c) PlaceholderCompany. All rights reserved.
+//
+
+namespace Jellyfin.Api.Controllers;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Jellyfin.Database.Implementations;
+using Jellyfin.Database.Implementations.Entities.Views;
+using MediaBrowser.Common.Api;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.EntityFrameworkCore;
+
+///
+/// Exposes the PostgreSQL reporting views as read-only API endpoints.
+/// All endpoints require administrator privileges.
+///
+[Route("DatabaseViews")]
+[Authorize(Policy = Policies.RequiresElevation)]
+public class DatabaseViewsController : BaseJellyfinApiController
+{
+ private readonly IDbContextFactory _dbFactory;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The EF Core context factory.
+ public DatabaseViewsController(IDbContextFactory dbFactory)
+ {
+ _dbFactory = dbFactory;
+ }
+
+ ///
+ /// Gets a flat list of all media streams (audio + video columns merged).
+ /// Maps to library."MediaStreamInfos_v".
+ ///
+ /// Optional item ID filter.
+ /// Optional stream type filter (0=Audio, 1=Video, 2=Subtitle).
+ /// Record index to start at.
+ /// Maximum records to return (1-1000, default 100).
+ /// List of media stream rows.
+ [HttpGet("MediaStreamInfos")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetMediaStreamInfos(
+ [FromQuery] Guid? itemId = null,
+ [FromQuery] int? streamType = null,
+ [FromQuery] int startIndex = 0,
+ [FromQuery] int limit = 100)
+ {
+ limit = Math.Clamp(limit, 1, 1000);
+ await using var db = await _dbFactory.CreateDbContextAsync().ConfigureAwait(false);
+
+ var query = db.MediaStreamInfoViews.AsNoTracking();
+ if (itemId.HasValue)
+ {
+ var id = itemId.Value;
+ query = query.Where(x => x.ItemId.HasValue && x.ItemId.Value.Equals(id));
+ }
+
+ if (streamType.HasValue)
+ {
+ query = query.Where(x => x.StreamType == streamType.Value);
+ }
+
+ IEnumerable results = await query
+ .OrderBy(x => x.ItemId)
+ .ThenBy(x => x.StreamIndex)
+ .Skip(startIndex)
+ .Take(limit)
+ .ToListAsync()
+ .ConfigureAwait(false);
+
+ return Ok(results);
+ }
+
+ ///
+ /// Gets video items joined with primary stream technical details.
+ /// Maps to library."VideoItems_v".
+ ///
+ /// Optional partial type filter (e.g. "Movie", "Episode").
+ /// Optional minimum video height (e.g. 2160 for 4K).
+ /// Optional HDR format ("Dolby Vision", "HDR10+", "HDR10", "HLG", "SDR").
+ /// Record index to start at.
+ /// Maximum records to return (1-1000, default 100).
+ /// List of video item rows.
+ [HttpGet("VideoItems")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetVideoItems(
+ [FromQuery] string? type = null,
+ [FromQuery] int? minHeight = null,
+ [FromQuery] string? hdrFormat = null,
+ [FromQuery] int startIndex = 0,
+ [FromQuery] int limit = 100)
+ {
+ limit = Math.Clamp(limit, 1, 1000);
+ await using var db = await _dbFactory.CreateDbContextAsync().ConfigureAwait(false);
+
+ var query = db.VideoItemViews.AsNoTracking();
+ if (!string.IsNullOrEmpty(type))
+ {
+ query = query.Where(x => x.Type != null && x.Type.Contains(type));
+ }
+
+ if (minHeight.HasValue)
+ {
+ query = query.Where(x => x.Height >= minHeight.Value);
+ }
+
+ if (!string.IsNullOrEmpty(hdrFormat))
+ {
+ query = query.Where(x => x.HDRFormat == hdrFormat);
+ }
+
+ IEnumerable results = await query
+ .OrderBy(x => x.Name)
+ .Skip(startIndex)
+ .Take(limit)
+ .ToListAsync()
+ .ConfigureAwait(false);
+
+ return Ok(results);
+ }
+
+ ///
+ /// Gets per-user playback history with calculated progress percentages.
+ /// Maps to library."UserPlaybackHistory_v".
+ ///
+ /// Optional exact username filter.
+ /// Optional item ID filter.
+ /// When true, returns only items started but not fully played.
+ /// Record index to start at.
+ /// Maximum records to return (1-1000, default 100).
+ /// List of playback history rows.
+ [HttpGet("PlaybackHistory")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetPlaybackHistory(
+ [FromQuery] string? username = null,
+ [FromQuery] Guid? itemId = null,
+ [FromQuery] bool inProgressOnly = false,
+ [FromQuery] int startIndex = 0,
+ [FromQuery] int limit = 100)
+ {
+ limit = Math.Clamp(limit, 1, 1000);
+ await using var db = await _dbFactory.CreateDbContextAsync().ConfigureAwait(false);
+
+ var query = db.UserPlaybackHistoryViews.AsNoTracking();
+ if (!string.IsNullOrEmpty(username))
+ {
+ query = query.Where(x => x.Username == username);
+ }
+
+ if (itemId.HasValue)
+ {
+ var id = itemId.Value;
+ query = query.Where(x => x.ItemId.HasValue && x.ItemId.Value.Equals(id));
+ }
+
+ if (inProgressOnly)
+ {
+ query = query.Where(x => x.Played != true && x.ProgressPct > 0);
+ }
+
+ IEnumerable results = await query
+ .OrderByDescending(x => x.LastPlayedDate)
+ .Skip(startIndex)
+ .Take(limit)
+ .ToListAsync()
+ .ConfigureAwait(false);
+
+ return Ok(results);
+ }
+
+ ///
+ /// Gets items with their external provider IDs (IMDb, TMDB, TVDB, etc.) in named columns.
+ /// Maps to library."ItemProviders_v".
+ ///
+ /// Optional partial type filter.
+ /// Optional exact IMDb ID filter.
+ /// Optional exact TMDB ID filter.
+ /// Optional exact TVDB ID filter.
+ /// Record index to start at.
+ /// Maximum records to return (1-1000, default 100).
+ /// List of item provider rows.
+ [HttpGet("ItemProviders")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetItemProviders(
+ [FromQuery] string? type = null,
+ [FromQuery] string? imdbId = null,
+ [FromQuery] string? tmdbId = null,
+ [FromQuery] string? tvdbId = null,
+ [FromQuery] int startIndex = 0,
+ [FromQuery] int limit = 100)
+ {
+ limit = Math.Clamp(limit, 1, 1000);
+ await using var db = await _dbFactory.CreateDbContextAsync().ConfigureAwait(false);
+
+ var query = db.ItemProviderViews.AsNoTracking();
+ if (!string.IsNullOrEmpty(type))
+ {
+ query = query.Where(x => x.Type != null && x.Type.Contains(type));
+ }
+
+ if (!string.IsNullOrEmpty(imdbId))
+ {
+ query = query.Where(x => x.ImdbId == imdbId);
+ }
+
+ if (!string.IsNullOrEmpty(tmdbId))
+ {
+ query = query.Where(x => x.TmdbId == tmdbId);
+ }
+
+ if (!string.IsNullOrEmpty(tvdbId))
+ {
+ query = query.Where(x => x.TvdbId == tvdbId);
+ }
+
+ IEnumerable results = await query
+ .OrderBy(x => x.Name)
+ .Skip(startIndex)
+ .Take(limit)
+ .ToListAsync()
+ .ConfigureAwait(false);
+
+ return Ok(results);
+ }
+
+ ///
+ /// Gets cast and crew entries linked to the items they appear in.
+ /// Maps to library."ItemPeople_v".
+ ///
+ /// Optional exact person name filter.
+ /// Optional person type filter (e.g. "Actor", "Director").
+ /// Optional item ID filter.
+ /// Record index to start at.
+ /// Maximum records to return (1-1000, default 100).
+ /// List of item person rows.
+ [HttpGet("ItemPeople")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetItemPeople(
+ [FromQuery] string? personName = null,
+ [FromQuery] string? personType = null,
+ [FromQuery] Guid? itemId = null,
+ [FromQuery] int startIndex = 0,
+ [FromQuery] int limit = 100)
+ {
+ limit = Math.Clamp(limit, 1, 1000);
+ await using var db = await _dbFactory.CreateDbContextAsync().ConfigureAwait(false);
+
+ var query = db.ItemPersonViews.AsNoTracking();
+ if (!string.IsNullOrEmpty(personName))
+ {
+ query = query.Where(x => x.PersonName == personName);
+ }
+
+ if (!string.IsNullOrEmpty(personType))
+ {
+ query = query.Where(x => x.PersonType == personType);
+ }
+
+ if (itemId.HasValue)
+ {
+ var id = itemId.Value;
+ query = query.Where(x => x.ItemId.HasValue && x.ItemId.Value.Equals(id));
+ }
+
+ IEnumerable results = await query
+ .OrderBy(x => x.PersonName)
+ .ThenBy(x => x.SortOrder)
+ .Skip(startIndex)
+ .Take(limit)
+ .ToListAsync()
+ .ConfigureAwait(false);
+
+ return Ok(results);
+ }
+
+ ///
+ /// Gets aggregate library statistics - one row per item type.
+ /// Maps to library."LibrarySummary_v".
+ ///
+ /// List of library summary rows, one per item type.
+ [HttpGet("LibrarySummary")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task>> GetLibrarySummary()
+ {
+ await using var db = await _dbFactory.CreateDbContextAsync().ConfigureAwait(false);
+
+ IEnumerable results = await db.LibrarySummaryViews
+ .AsNoTracking()
+ .OrderBy(x => x.Type)
+ .ToListAsync()
+ .ConfigureAwait(false);
+
+ return Ok(results);
+ }
+}
diff --git a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
index 1f06f742..00f4a8bf 100644
--- a/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
+++ b/Jellyfin.Server.Implementations/Extensions/ServiceCollectionExtensions.cs
@@ -99,23 +99,13 @@ public static class ServiceCollectionExtensions
}
else
{
- // when nothing is setup via new Database configuration, fallback to SQLite with default settings.
+ // PostgreSQL-only build: default to PostgreSQL when no database.xml exists.
efCoreConfiguration = new DatabaseConfigurationOptions()
{
- DatabaseType = "Jellyfin-SQLite",
+ DatabaseType = "Jellyfin-PostgreSQL",
LockingBehavior = DatabaseLockingBehaviorTypes.NoLock,
PresetConfigurations =
[
- new PresetConfigurationItem
- {
- Key = "SQLite",
- Value = new PresetDatabaseConfiguration
- {
- DatabaseType = "Jellyfin-SQLite",
- LockingBehavior = DatabaseLockingBehaviorTypes.NoLock,
- Description = "Default SQLite database configuration. Database stored in data directory."
- }
- },
new PresetConfigurationItem
{
Key = "PostgreSQL-Local",
@@ -135,7 +125,7 @@ public static class ServiceCollectionExtensions
// Ensure DatabaseType is set (handle corrupted or empty configuration files)
if (string.IsNullOrWhiteSpace(efCoreConfiguration.DatabaseType))
{
- efCoreConfiguration.DatabaseType = "Jellyfin-SQLite";
+ efCoreConfiguration.DatabaseType = "Jellyfin-PostgreSQL";
configurationManager.SaveConfiguration("database", efCoreConfiguration);
}
diff --git a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
deleted file mode 100644
index 9d263f31..00000000
--- a/Jellyfin.Server/Migrations/Routines/MigrateUserDb.cs
+++ /dev/null
@@ -1,237 +0,0 @@
-//
-// Copyright (c) PlaceholderCompany. All rights reserved.
-//
-
-namespace Jellyfin.Server.Migrations.Routines;
-
-using System;
-using System.IO;
-using Emby.Server.Implementations.Data;
-using Jellyfin.Data;
-using Jellyfin.Database.Implementations;
-using Jellyfin.Database.Implementations.Entities;
-using Jellyfin.Database.Implementations.Enums;
-using Jellyfin.Extensions.Json;
-using Jellyfin.Server.Implementations.Users;
-using MediaBrowser.Controller;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Model.Configuration;
-using MediaBrowser.Model.Serialization;
-using MediaBrowser.Model.Users;
-using Microsoft.Data.Sqlite;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Logging;
-using JsonSerializer = System.Text.Json.JsonSerializer;
-
-///
-/// The migration routine for migrating the user database to EF Core.
-///
-#pragma warning disable CS0618 // Type or member is obsolete
-[JellyfinMigration("2025-04-20T10:00:00", nameof(MigrateUserDb), "5C4B82A2-F053-4009-BD05-B6FCAD82F14C", RequiresSqlite = true)]
-public class MigrateUserDb : IMigrationRoutine
-#pragma warning restore CS0618 // Type or member is obsolete
-{
- private const string DbFilename = "users.db";
-
- private readonly ILogger _logger;
- private readonly IServerApplicationPaths _paths;
- private readonly IDbContextFactory _provider;
- private readonly IXmlSerializer _xmlSerializer;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The logger.
- /// The server application paths.
- /// The database provider.
- /// The xml serializer.
- public MigrateUserDb(
- ILogger logger,
- IServerApplicationPaths paths,
- IDbContextFactory provider,
- IXmlSerializer xmlSerializer)
- {
- _logger = logger;
- _paths = paths;
- _provider = provider;
- _xmlSerializer = xmlSerializer;
- }
-
- ///
- public void Perform()
- {
- var dataPath = _paths.DataPath;
- var userDbPath = Path.Combine(dataPath, DbFilename);
- if (!File.Exists(userDbPath))
- {
- _logger.LogWarning("{UserDbPath} doesn't exist, nothing to migrate", userDbPath);
- return;
- }
-
- _logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin.");
-
- using (var connection = new SqliteConnection($"Filename={userDbPath}"))
- {
- connection.Open();
- var tableQuery = connection.Query("SELECT count(*) FROM sqlite_master WHERE type='table' AND name='LocalUsersv2';");
- foreach (var row in tableQuery)
- {
- if (row.GetInt32(0) == 0)
- {
- _logger.LogWarning("Table 'LocalUsersv2' doesn't exist in {UserDbPath}, nothing to migrate", userDbPath);
- return;
- }
- }
-
- using var dbContext = _provider.CreateDbContext();
-
- var queryResult = connection.Query("SELECT * FROM LocalUsersv2");
-
- dbContext.RemoveRange(dbContext.Users);
- dbContext.SaveChanges();
-
- foreach (var entry in queryResult)
- {
- UserMockup? mockup = JsonSerializer.Deserialize(entry.GetStream(2), JsonDefaults.Options);
- if (mockup is null)
- {
- continue;
- }
-
- var userDataDir = Path.Combine(_paths.UserConfigurationDirectoryPath, mockup.Name);
-
- var configPath = Path.Combine(userDataDir, "config.xml");
- var config = File.Exists(configPath)
- ? (UserConfiguration?)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), configPath) ?? new UserConfiguration()
- : new UserConfiguration();
-
- var policyPath = Path.Combine(userDataDir, "policy.xml");
- var policy = File.Exists(policyPath)
- ? (UserPolicy?)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), policyPath) ?? new UserPolicy()
- : new UserPolicy();
- policy.AuthenticationProviderId = policy.AuthenticationProviderId?.Replace(
- "Emby.Server.Implementations.Library",
- "Jellyfin.Server.Implementations.Users",
- StringComparison.Ordinal)
- ?? typeof(DefaultAuthenticationProvider).FullName;
-
- policy.PasswordResetProviderId = typeof(DefaultPasswordResetProvider).FullName;
- int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
- {
- -1 => null,
- 0 => 3,
- _ => policy.LoginAttemptsBeforeLockout
- };
-
- var user = new User(mockup.Name, policy.AuthenticationProviderId!, policy.PasswordResetProviderId!)
- {
- Id = entry.GetGuid(1),
- InternalId = entry.GetInt64(0),
- MaxParentalRatingScore = policy.MaxParentalRating,
- MaxParentalRatingSubScore = null,
- EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess,
- RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit,
- InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount,
- LoginAttemptsBeforeLockout = maxLoginAttempts,
- SubtitleMode = config.SubtitleMode,
- HidePlayedInLatest = config.HidePlayedInLatest,
- EnableLocalPassword = config.EnableLocalPassword,
- PlayDefaultAudioTrack = config.PlayDefaultAudioTrack,
- DisplayCollectionsView = config.DisplayCollectionsView,
- DisplayMissingEpisodes = config.DisplayMissingEpisodes,
- AudioLanguagePreference = config.AudioLanguagePreference,
- RememberAudioSelections = config.RememberAudioSelections,
- EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay,
- RememberSubtitleSelections = config.RememberSubtitleSelections,
- SubtitleLanguagePreference = config.SubtitleLanguagePreference,
- Password = mockup.Password,
- LastLoginDate = mockup.LastLoginDate,
- LastActivityDate = mockup.LastActivityDate
- };
-
- if (mockup.ImageInfos.Length > 0)
- {
- ItemImageInfo info = mockup.ImageInfos[0];
-
- user.ProfileImage = new ImageInfo(info.Path)
- {
- LastModified = info.DateModified
- };
- }
-
- user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
- user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
- user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
- user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
- user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
- user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
- user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
- user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
- user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding);
- user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding);
- user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
- user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
- user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
- user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
- user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
- user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
- user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
- user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
- user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
- user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
- user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
- user.SetPermission(PermissionKind.EnableCollectionManagement, policy.EnableCollectionManagement);
-
- foreach (var policyAccessSchedule in policy.AccessSchedules)
- {
- user.AccessSchedules.Add(policyAccessSchedule);
- }
-
- user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
- user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
- user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
- user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
- user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
- user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
- user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
- user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
- user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
-
- dbContext.Users.Add(user);
- }
-
- dbContext.SaveChanges();
- }
-
- try
- {
- File.Move(Path.Combine(dataPath, DbFilename), Path.Combine(dataPath, DbFilename + ".old"));
-
- var journalPath = Path.Combine(dataPath, DbFilename + "-journal");
- if (File.Exists(journalPath))
- {
- File.Move(journalPath, Path.Combine(dataPath, DbFilename + ".old-journal"));
- }
- }
- catch (IOException e)
- {
- _logger.LogError(e, "Error renaming legacy user database to 'users.db.old'");
- }
- }
-
-#nullable disable
- internal class UserMockup
- {
- public string Password { get; set; }
-
- public string EasyPassword { get; set; }
-
- public DateTime? LastLoginDate { get; set; }
-
- public DateTime? LastActivityDate { get; set; }
-
- public string Name { get; set; }
-
- public ItemImageInfo[] ImageInfos { get; set; }
- }
-}
diff --git a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs b/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
deleted file mode 100644
index 1080bbd2..00000000
--- a/Jellyfin.Server/Migrations/Routines/RemoveDuplicateExtras.cs
+++ /dev/null
@@ -1,88 +0,0 @@
-//
-// Copyright (c) PlaceholderCompany. All rights reserved.
-//
-
-namespace Jellyfin.Server.Migrations.Routines;
-
-using System;
-using System.Globalization;
-using System.IO;
-using System.Linq;
-using Emby.Server.Implementations.Data;
-using MediaBrowser.Controller;
-using Microsoft.Data.Sqlite;
-using Microsoft.Extensions.Logging;
-
-///
-/// Remove duplicate entries which were caused by a bug where a file was considered to be an "Extra" to itself.
-///
-#pragma warning disable CS0618 // Type or member is obsolete
-[JellyfinMigration("2025-04-20T08:00:00", nameof(RemoveDuplicateExtras), "ACBE17B7-8435-4A83-8B64-6FCF162CB9BD", RequiresSqlite = true)]
-internal class RemoveDuplicateExtras : IMigrationRoutine
-#pragma warning restore CS0618 // Type or member is obsolete
-{
- private const string DbFilename = "library.db";
- private readonly ILogger _logger;
- private readonly IServerApplicationPaths _paths;
-
- public RemoveDuplicateExtras(ILogger logger, IServerApplicationPaths paths)
- {
- _logger = logger;
- _paths = paths;
- }
-
- ///
- public void Perform()
- {
- var dataPath = _paths.DataPath;
- var dbPath = Path.Combine(dataPath, DbFilename);
-
- // Skip this migration if using PostgreSQL or if library.db doesn't exist
- if (!File.Exists(dbPath))
- {
- _logger.LogInformation("SQLite library.db not found, skipping SQLite-specific migration.");
- return;
- }
-
- using var connection = new SqliteConnection($"Filename={dbPath}");
- connection.Open();
- using (var transaction = connection.BeginTransaction())
- {
- // Query the database for the ids of duplicate extras
- var queryResult = connection.Query("SELECT t1.Path FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video'");
- var bads = string.Join(", ", queryResult.Select(x => x.GetString(0)));
-
- // Do nothing if no duplicate extras were detected
- if (bads.Length == 0)
- {
- _logger.LogInformation("No duplicate extras detected, skipping migration.");
- return;
- }
-
- // 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;
- }
- }
- }
-
- // Delete all duplicate extras
- _logger.LogInformation("Removing found duplicated extras for the following items: {DuplicateExtras}", bads);
- connection.Execute("DELETE FROM TypedBaseItems WHERE rowid IN (SELECT t1.rowid FROM TypedBaseItems AS t1, TypedBaseItems AS t2 WHERE t1.Path=t2.Path AND t1.Type!=t2.Type AND t1.Type='MediaBrowser.Controller.Entities.Video')");
- transaction.Commit();
- }
- }
-}
diff --git a/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs b/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs
deleted file mode 100644
index 46a16ce0..00000000
--- a/Jellyfin.Server/Migrations/Routines/ReseedFolderFlag.cs
+++ /dev/null
@@ -1,78 +0,0 @@
-//
-// Copyright (c) PlaceholderCompany. All rights reserved.
-//
-
-#pragma warning disable RS0030 // Do not use banned APIs
-using System.IO;
-using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
-using Emby.Server.Implementations.Data;
-using Jellyfin.Database.Implementations;
-using Jellyfin.Server.ServerSetupApp;
-using MediaBrowser.Controller;
-using Microsoft.Data.Sqlite;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.Extensions.Logging;
-
-namespace Jellyfin.Server.Migrations.Routines;
-
-[JellyfinMigration("2025-07-30T21:50:00", nameof(ReseedFolderFlag), RequiresSqlite = true)]
-[JellyfinMigrationBackup(JellyfinDb = true)]
-internal class ReseedFolderFlag : IAsyncMigrationRoutine
-{
- private const string DbFilename = "library.db.old";
-
- private readonly IStartupLogger _logger;
- private readonly IServerApplicationPaths _paths;
- private readonly IDbContextFactory _provider;
-
- public ReseedFolderFlag(
- IStartupLogger startupLogger,
- IDbContextFactory provider,
- IServerApplicationPaths paths)
- {
- _logger = startupLogger;
- _provider = provider;
- _paths = paths;
- }
-
- internal static bool RerunGuardFlag { get; set; } = false;
-
- public async Task PerformAsync(CancellationToken cancellationToken)
- {
- if (RerunGuardFlag)
- {
- _logger.LogInformation("Migration is skipped because it does not apply.");
- return;
- }
-
- _logger.LogInformation("Migrating the IsFolder flag from library.db.old may take a while, do not stop Jellyfin.");
-
- var dataPath = _paths.DataPath;
- var libraryDbPath = Path.Combine(dataPath, DbFilename);
- if (!File.Exists(libraryDbPath))
- {
- _logger.LogError("Cannot migrate IsFolder flag from {LibraryDb} as it does not exist. This migration expects the MigrateLibraryDb to run first.", libraryDbPath);
- return;
- }
-
- var dbContext = await _provider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
- await using (dbContext.ConfigureAwait(false))
- {
- using var connection = new SqliteConnection($"Filename={libraryDbPath};Mode=ReadOnly");
- var queryResult = connection.Query(
- """
- SELECT guid FROM TypedBaseItems
- WHERE IsFolder = true
- """)
- .Select(entity => entity.GetGuid(0))
- .ToList();
- _logger.LogInformation("Migrating the IsFolder flag for {Count} items.", queryResult.Count);
- foreach (var id in queryResult)
- {
- await dbContext.BaseItems.Where(e => e.Id == id).ExecuteUpdateAsync(e => e.SetProperty(f => f.IsFolder, true), cancellationToken).ConfigureAwait(false);
- }
- }
- }
-}
diff --git a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
index 18f3a1cd..33420702 100644
--- a/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
+++ b/MediaBrowser.Controller/Extensions/ConfigurationExtensions.cs
@@ -68,11 +68,6 @@ namespace MediaBrowser.Controller.Extensions
///
public const string UnixSocketPermissionsKey = "kestrel:socketPermissions";
- ///
- /// The cache size of the SQL database, see cache_size.
- ///
- public const string SqliteCacheSizeKey = "sqlite:cacheSize";
-
///
/// The key for a setting that indicates whether the application should detect network status change.
///
@@ -143,12 +138,5 @@ namespace MediaBrowser.Controller.Extensions
public static string? GetUnixSocketPermissions(this IConfiguration configuration)
=> configuration[UnixSocketPermissionsKey];
- ///
- /// Gets the cache_size from the .
- ///
- /// The configuration to read the setting from.
- /// The sqlite cache size.
- public static int? GetSqliteCacheSize(this IConfiguration configuration)
- => configuration.GetValue(SqliteCacheSizeKey);
}
}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Views/ItemPersonView.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Views/ItemPersonView.cs
new file mode 100644
index 00000000..c15ff2d7
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Views/ItemPersonView.cs
@@ -0,0 +1,29 @@
+//
+// Copyright (c) PlaceholderCompany. All rights reserved.
+//
+
+namespace Jellyfin.Database.Implementations.Entities.Views;
+
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
+using System;
+
+///
+/// Read-only projection of the library."ItemPeople_v" view.
+/// All cast and crew entries linked to the items they appear in.
+///
+public class ItemPersonView
+{
+ public Guid? ItemId { get; set; }
+ public string? ItemType { get; set; }
+ public string? ItemName { get; set; }
+ public string? SeriesName { get; set; }
+ public int? ProductionYear { get; set; }
+ public Guid? PersonId { get; set; }
+ public string? PersonName { get; set; }
+ public string? PersonType { get; set; }
+ public string? Role { get; set; }
+ public int? SortOrder { get; set; }
+ public int? ListOrder { get; set; }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Views/ItemProviderView.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Views/ItemProviderView.cs
new file mode 100644
index 00000000..0c716598
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Views/ItemProviderView.cs
@@ -0,0 +1,34 @@
+//
+// Copyright (c) PlaceholderCompany. All rights reserved.
+//
+
+namespace Jellyfin.Database.Implementations.Entities.Views;
+
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
+using System;
+
+///
+/// Read-only projection of library."ItemProviders_v".
+/// Items with external provider IDs pivoted into named columns.
+///
+public class ItemProviderView
+{
+ public Guid? ItemId { get; set; }
+ public string? Type { get; set; }
+ public string? Name { get; set; }
+ public string? OriginalTitle { get; set; }
+ public int? ProductionYear { get; set; }
+ public string? SeriesName { get; set; }
+ public int? EpisodeNumber { get; set; }
+ public int? SeasonNumber { get; set; }
+ public string? ImdbId { get; set; }
+ public string? TmdbId { get; set; }
+ public string? TvdbId { get; set; }
+ public string? TvRageId { get; set; }
+ public string? MusicBrainzAlbumId { get; set; }
+ public string? MusicBrainzArtistId { get; set; }
+ /// Gets or sets remaining providers as a JSON object string.
+ public string? OtherProviders { get; set; }
+}
\ No newline at end of file
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Views/LibrarySummaryView.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Views/LibrarySummaryView.cs
new file mode 100644
index 00000000..e8c62e4e
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Views/LibrarySummaryView.cs
@@ -0,0 +1,33 @@
+//
+// Copyright (c) PlaceholderCompany. All rights reserved.
+//
+
+namespace Jellyfin.Database.Implementations.Entities.Views;
+
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
+///
+/// Read-only projection of the library."LibrarySummary_v" view.
+/// One row per item type with aggregate statistics across the whole library.
+///
+public class LibrarySummaryView
+{
+ public string? Type { get; set; }
+ public long? ItemCount { get; set; }
+ public long? WithYear { get; set; }
+ public int? OldestYear { get; set; }
+ public int? NewestYear { get; set; }
+ public decimal? AvgCommunityRating { get; set; }
+ public decimal? TotalRunTimeTicks { get; set; }
+ public decimal? TotalRuntimeHours { get; set; }
+ public decimal? TotalSizeBytes { get; set; }
+ public decimal? TotalSizeGB { get; set; }
+ public long? Count4K { get; set; }
+ public long? Count1080p { get; set; }
+ public long? Count720p { get; set; }
+ public long? CountSD { get; set; }
+ public long? CountDolbyVision { get; set; }
+ public long? CountHDR10Plus { get; set; }
+ public long? CountHDR10 { get; set; }
+}
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Views/MediaStreamInfoView.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Views/MediaStreamInfoView.cs
new file mode 100644
index 00000000..b1c51b3f
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Views/MediaStreamInfoView.cs
@@ -0,0 +1,67 @@
+//
+// Copyright (c) PlaceholderCompany. All rights reserved.
+//
+
+namespace Jellyfin.Database.Implementations.Entities.Views;
+
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
+using System;
+
+///
+/// Read-only projection of library."MediaStreamInfos_v".
+/// Flat view of all media streams with audio- and video-specific columns merged.
+///
+public class MediaStreamInfoView
+{
+ public Guid? ItemId { get; set; }
+ public int? StreamIndex { get; set; }
+ public int? StreamType { get; set; }
+ public string? Codec { get; set; }
+ public string? Language { get; set; }
+ public string? Profile { get; set; }
+ public string? Path { get; set; }
+ public int? BitRate { get; set; }
+ public bool? IsDefault { get; set; }
+ public bool? IsForced { get; set; }
+ public bool? IsExternal { get; set; }
+ public float? Level { get; set; }
+ public string? CodecTag { get; set; }
+ public string? Comment { get; set; }
+ public bool? IsAvc { get; set; }
+ public string? Title { get; set; }
+ public string? TimeBase { get; set; }
+ public string? CodecTimeBase { get; set; }
+ // audio-specific
+ public string? ChannelLayout { get; set; }
+ public int? Channels { get; set; }
+ public int? SampleRate { get; set; }
+ public bool? IsHearingImpaired { get; set; }
+ // video-specific
+ public string? AspectRatio { get; set; }
+ public bool? IsInterlaced { get; set; }
+ public int? Height { get; set; }
+ public int? Width { get; set; }
+ public float? AverageFrameRate { get; set; }
+ public float? RealFrameRate { get; set; }
+ public string? PixelFormat { get; set; }
+ public int? BitDepth { get; set; }
+ public bool? IsAnamorphic { get; set; }
+ public int? RefFrames { get; set; }
+ public string? NalLengthSize { get; set; }
+ public string? ColorPrimaries { get; set; }
+ public string? ColorSpace { get; set; }
+ public string? ColorTransfer { get; set; }
+ public int? DvVersionMajor { get; set; }
+ public int? DvVersionMinor { get; set; }
+ public int? DvProfile { get; set; }
+ public int? DvLevel { get; set; }
+ public int? RpuPresentFlag { get; set; }
+ public int? ElPresentFlag { get; set; }
+ public int? BlPresentFlag { get; set; }
+ public int? DvBlSignalCompatibilityId { get; set; }
+ public int? Rotation { get; set; }
+ public string? KeyFrames { get; set; }
+ public bool? Hdr10PlusPresentFlag { get; set; }
+}
\ No newline at end of file
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Views/UserPlaybackHistoryView.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Views/UserPlaybackHistoryView.cs
new file mode 100644
index 00000000..b5629d17
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Views/UserPlaybackHistoryView.cs
@@ -0,0 +1,39 @@
+//
+// Copyright (c) PlaceholderCompany. All rights reserved.
+//
+
+namespace Jellyfin.Database.Implementations.Entities.Views;
+
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
+using System;
+
+///
+/// Read-only projection of library."UserPlaybackHistory_v".
+/// Per-user watch history with calculated progress percentage.
+///
+public class UserPlaybackHistoryView
+{
+ public string? Username { get; set; }
+ public Guid? UserId { get; set; }
+ public string? ItemName { get; set; }
+ public string? ItemType { get; set; }
+ public string? SeriesName { get; set; }
+ public string? SeasonName { get; set; }
+ public int? EpisodeNumber { get; set; }
+ public int? SeasonNumber { get; set; }
+ public int? ProductionYear { get; set; }
+ public long? RunTimeTicks { get; set; }
+ public Guid? ItemId { get; set; }
+ public bool? Played { get; set; }
+ public int? PlayCount { get; set; }
+ public DateTimeOffset? LastPlayedDate { get; set; }
+ public long? PlaybackPositionTicks { get; set; }
+ /// Gets or sets playback progress 0–100. NULL when no runtime.
+ public decimal? ProgressPct { get; set; }
+ public bool? IsFavorite { get; set; }
+ public double? Rating { get; set; }
+ public int? AudioStreamIndex { get; set; }
+ public int? SubtitleStreamIndex { get; set; }
+}
\ No newline at end of file
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Views/VideoItemView.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Views/VideoItemView.cs
new file mode 100644
index 00000000..218dc683
--- /dev/null
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/Entities/Views/VideoItemView.cs
@@ -0,0 +1,54 @@
+//
+// Copyright (c) PlaceholderCompany. All rights reserved.
+//
+
+namespace Jellyfin.Database.Implementations.Entities.Views;
+
+#pragma warning disable CS1591
+#pragma warning disable SA1600
+
+using System;
+
+///
+/// Read-only projection of library."VideoItems_v".
+/// Every non-folder, non-virtual item with at least one video stream.
+///
+public class VideoItemView
+{
+ public Guid? ItemId { get; set; }
+ public string? Type { get; set; }
+ public string? Name { get; set; }
+ public string? OriginalTitle { get; set; }
+ public int? ProductionYear { get; set; }
+ public string? OfficialRating { get; set; }
+ public float? CommunityRating { get; set; }
+ public long? RunTimeTicks { get; set; }
+ public string? Path { get; set; }
+ public DateTimeOffset? DateCreated { get; set; }
+ public DateTimeOffset? DateModified { get; set; }
+ public string? SeriesName { get; set; }
+ public string? SeasonName { get; set; }
+ public int? EpisodeNumber { get; set; }
+ public int? SeasonNumber { get; set; }
+ public int? TotalBitrate { get; set; }
+ public long? Size { get; set; }
+ public string? VideoCodec { get; set; }
+ public string? VideoProfile { get; set; }
+ public int? Width { get; set; }
+ public int? Height { get; set; }
+ public float? AverageFrameRate { get; set; }
+ public int? BitDepth { get; set; }
+ public string? PixelFormat { get; set; }
+ public string? ColorSpace { get; set; }
+ public string? ColorTransfer { get; set; }
+ public string? ColorPrimaries { get; set; }
+ public bool? IsInterlaced { get; set; }
+ public bool? IsAnamorphic { get; set; }
+ public string? AspectRatio { get; set; }
+ public bool? IsDolbyVision { get; set; }
+ public bool? IsHDR10Plus { get; set; }
+ /// Gets or sets derived HDR format: "Dolby Vision", "HDR10+", "HDR10", "HLG", or "SDR".
+ public string? HDRFormat { get; set; }
+ public long? AudioTrackCount { get; set; }
+ public long? SubtitleTrackCount { get; set; }
+}
\ No newline at end of file
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs
index 9b01153b..df212074 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Implementations/JellyfinDbContext.cs
@@ -14,6 +14,7 @@ using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Entities.Security;
+using Jellyfin.Database.Implementations.Entities.Views;
using Jellyfin.Database.Implementations.Interfaces;
using Jellyfin.Database.Implementations.Locking;
using Microsoft.EntityFrameworkCore;
@@ -156,6 +157,44 @@ public class JellyfinDbContext(DbContextOptions options, ILog
///
public DbSet VideoStreamDetails => this.Set();
+ // ── Read-only view projections (PostgreSQL only) ──────────────────────
+
+ ///
+ /// Gets the flat view over MediaStreamInfos joined with audio and video detail tables.
+ /// Maps to library."MediaStreamInfos_v".
+ ///
+ public DbSet MediaStreamInfoViews => this.Set();
+
+ ///
+ /// Gets the view of video items joined with their primary stream details.
+ /// Maps to library."VideoItems_v".
+ ///
+ public DbSet VideoItemViews => this.Set();
+
+ ///
+ /// Gets the view of per-user watch history with calculated progress.
+ /// Maps to library."UserPlaybackHistory_v".
+ ///
+ public DbSet UserPlaybackHistoryViews => this.Set();
+
+ ///
+ /// Gets the view of items with pivoted external provider IDs.
+ /// Maps to library."ItemProviders_v".
+ ///
+ public DbSet ItemProviderViews => this.Set();
+
+ ///
+ /// Gets the view of cast and crew entries linked to their items.
+ /// Maps to library."ItemPeople_v".
+ ///
+ public DbSet ItemPersonViews => this.Set();
+
+ ///
+ /// Gets the aggregate library statistics view, one row per item type.
+ /// Maps to library."LibrarySummary_v".
+ ///
+ public DbSet LibrarySummaryViews => this.Set();
+
///
/// Gets the .
///
diff --git a/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresDatabaseProvider.cs b/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresDatabaseProvider.cs
index 251ac94f..05c32d58 100644
--- a/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresDatabaseProvider.cs
+++ b/src/Jellyfin.Database/Jellyfin.Database.Providers.Postgres/PostgresDatabaseProvider.cs
@@ -19,6 +19,7 @@ using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.DbConfiguration;
using Jellyfin.Database.Implementations.Entities;
using Jellyfin.Database.Implementations.Entities.Security;
+using Jellyfin.Database.Implementations.Entities.Views;
using Jellyfin.Database.Providers.Postgres.Exceptions;
using MediaBrowser.Common.Configuration;
using Microsoft.EntityFrameworkCore;
@@ -765,6 +766,9 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
// Assign entities to schemas based on their legacy database origin
AssignEntitiesToSchemas(modelBuilder);
+
+ // Register read-only view projections (PostgreSQL only)
+ RegisterViewMappings(modelBuilder);
}
///
@@ -818,6 +822,39 @@ public sealed class PostgresDatabaseProvider : IJellyfinDatabaseProvider
modelBuilder.Entity().HasIndex(e => e.DateModified);
}
+ ///
+ /// Registers read-only keyless view projections for PostgreSQL.
+ /// Each view lives in the library schema and is mapped with
+ /// HasNoKey().ToView() so EF Core never tries to create a table or migration for it.
+ ///
+ /// The model builder.
+ private static void RegisterViewMappings(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity()
+ .HasNoKey()
+ .ToView("MediaStreamInfos_v", Schemas.Library);
+
+ modelBuilder.Entity()
+ .HasNoKey()
+ .ToView("VideoItems_v", Schemas.Library);
+
+ modelBuilder.Entity()
+ .HasNoKey()
+ .ToView("UserPlaybackHistory_v", Schemas.Library);
+
+ modelBuilder.Entity()
+ .HasNoKey()
+ .ToView("ItemProviders_v", Schemas.Library);
+
+ modelBuilder.Entity()
+ .HasNoKey()
+ .ToView("ItemPeople_v", Schemas.Library);
+
+ modelBuilder.Entity()
+ .HasNoKey()
+ .ToView("LibrarySummary_v", Schemas.Library);
+ }
+
///
public Task RunShutdownTask(CancellationToken cancellationToken)
{