Merge branch 'master' into network-rewrite

pull/8147/head
Shadowghost 2 years ago
commit 006b04dc0b

@ -22,16 +22,16 @@ jobs:
- name: Checkout repository
uses: actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab # v3.5.2
- name: Setup .NET
uses: actions/setup-dotnet@aa983c550dfda0d1722b6ac6aed55724ffacc6d3 # v3.1.0
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
with:
dotnet-version: '7.0.x'
- name: Initialize CodeQL
uses: github/codeql-action/init@f0e3dfb30302f8a0881bb509b044e0de4f6ef589 # v2.3.4
uses: github/codeql-action/init@83f0fe6c4988d98a455712a27f0255212bba9bd4 # v2.3.6
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
uses: github/codeql-action/autobuild@f0e3dfb30302f8a0881bb509b044e0de4f6ef589 # v2.3.4
uses: github/codeql-action/autobuild@83f0fe6c4988d98a455712a27f0255212bba9bd4 # v2.3.6
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@f0e3dfb30302f8a0881bb509b044e0de4f6ef589 # v2.3.4
uses: github/codeql-action/analyze@83f0fe6c4988d98a455712a27f0255212bba9bd4 # v2.3.6

@ -19,7 +19,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
- name: Setup .NET
uses: actions/setup-dotnet@aa983c550dfda0d1722b6ac6aed55724ffacc6d3 # v3.1.0
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
with:
dotnet-version: '7.0.x'
- name: Generate openapi.json
@ -51,7 +51,7 @@ jobs:
ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/${{ github.head_ref }})
git checkout --progress --force $ANCESTOR_REF
- name: Setup .NET
uses: actions/setup-dotnet@aa983c550dfda0d1722b6ac6aed55724ffacc6d3 # v3.1.0
uses: actions/setup-dotnet@3447fd6a9f9e57506b15f895c5b76d3b197dc7c2 # v3.2.0
with:
dotnet-version: '7.0.x'
- name: Generate openapi.json

@ -17,7 +17,7 @@
<PackageVersion Include="Diacritics" Version="3.3.18" />
<PackageVersion Include="DiscUtils.Udf" Version="0.16.13" />
<PackageVersion Include="DotNet.Glob" Version="3.1.3" />
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.1" />
<PackageVersion Include="EFCoreSecondLevelCacheInterceptor" Version="3.9.2" />
<PackageVersion Include="FsCheck.Xunit" Version="2.16.5" />
<PackageVersion Include="Jellyfin.XmlTv" Version="10.8.0" />
<PackageVersion Include="libse" Version="3.6.13" />
@ -46,14 +46,14 @@
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="7.0.1" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="1.1.1" />
<PackageVersion Include="MimeTypes" Version="2.4.0" />
<PackageVersion Include="Mono.Nat" Version="3.0.4" />
<PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="NEbml" Version="0.11.0" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="PlaylistsNET" Version="1.3.2" />
<PackageVersion Include="PlaylistsNET" Version="1.4.0" />
<PackageVersion Include="prometheus-net.AspNetCore" Version="8.0.0" />
<PackageVersion Include="prometheus-net.DotNetRuntime" Version="4.4.0" />
<PackageVersion Include="prometheus-net" Version="8.0.0" />

@ -11,14 +11,15 @@ namespace Emby.Server.Implementations
/// <summary>
/// Gets a new copy of the default configuration options.
/// </summary>
public static Dictionary<string, string?> DefaultConfiguration => new Dictionary<string, string?>
public static Dictionary<string, string?> DefaultConfiguration => new()
{
{ HostWebClientKey, bool.TrueString },
{ DefaultRedirectKey, "web/" },
{ FfmpegProbeSizeKey, "1G" },
{ FfmpegAnalyzeDurationKey, "200M" },
{ PlaylistsAllowDuplicatesKey, bool.FalseString },
{ BindToUnixSocketKey, bool.FalseString }
{ BindToUnixSocketKey, bool.FalseString },
{ SqliteCacheSizeKey, "20000" }
};
}
}

@ -82,9 +82,10 @@ namespace Emby.Server.Implementations.Data
/// <summary>
/// Gets the journal size limit. <see href="https://www.sqlite.org/pragma.html#pragma_journal_size_limit" />.
/// The default (-1) is overriden to prevent unconstrained WAL size, as reported by users.
/// </summary>
/// <value>The journal size limit.</value>
protected virtual int? JournalSizeLimit => 0;
protected virtual int? JournalSizeLimit => 134_217_728; // 128MiB
/// <summary>
/// Gets the page size.

@ -25,6 +25,7 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
@ -34,6 +35,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.LiveTv;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
@ -319,13 +321,15 @@ namespace Emby.Server.Implementations.Data
/// <param name="logger">Instance of the <see cref="ILogger{SqliteItemRepository}"/> interface.</param>
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
/// <param name="imageProcessor">Instance of the <see cref="IImageProcessor"/> interface.</param>
/// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param>
/// <exception cref="ArgumentNullException">config is null.</exception>
public SqliteItemRepository(
IServerConfigurationManager config,
IServerApplicationHost appHost,
ILogger<SqliteItemRepository> logger,
ILocalizationManager localization,
IImageProcessor imageProcessor)
IImageProcessor imageProcessor,
IConfiguration configuration)
: base(logger)
{
_config = config;
@ -337,11 +341,13 @@ namespace Emby.Server.Implementations.Data
_jsonOptions = JsonDefaults.Options;
DbFilePath = Path.Combine(_config.ApplicationPaths.DataPath, "library.db");
CacheSize = configuration.GetSqliteCacheSize();
ReadConnectionsCount = Environment.ProcessorCount * 2;
}
/// <inheritdoc />
protected override int? CacheSize => 20000;
protected override int? CacheSize { get; }
/// <inheritdoc />
protected override TempStoreMode TempStore => TempStoreMode.Memory;

@ -1264,7 +1264,14 @@ namespace Emby.Server.Implementations.Library
AddUserToQuery(query, query.User, allowExternalContent);
}
return _itemRepository.GetItemList(query);
var itemList = _itemRepository.GetItemList(query);
var user = query.User;
if (user is not null)
{
return itemList.Where(i => i.IsVisible(user)).ToList();
}
return itemList;
}
public List<BaseItem> GetItemList(InternalItemsQuery query)

@ -104,5 +104,24 @@
"TaskCleanActivityLog": "क्रियाकलाप लॉग साफ करें",
"TasksChannelsCategory": "इंटरनेट प्रणाली",
"TasksApplicationCategory": "अनुप्रयोग",
"TaskRefreshPeople": "लोगोकी जानकारी ताज़ी करें"
"TaskRefreshPeople": "लोगोकी जानकारी ताज़ी करें",
"TaskKeyframeExtractor": "कीफ़्रेम एक्सट्रैक्टर",
"TaskCleanActivityLogDescription": "कॉन्फ़िगर की गई आयु से पुरानी गतिविधि लॉग प्रविष्टियां हटाता है।",
"TaskRefreshChapterImagesDescription": "अध्याय वाले वीडियो के लिए थंबनेल बनाता है।",
"TaskRefreshLibraryDescription": "नई फ़ाइलों के लिए आपकी मीडिया लाइब्रेरी को स्कैन करता है और मेटाडेटा को ताज़ा करता है।",
"TaskCleanLogs": "स्वच्छ लॉग निर्देशिका",
"TaskUpdatePluginsDescription": "प्लगइन्स के लिए अपडेट डाउनलोड और इंस्टॉल करें जो स्वचालित रूप से अपडेट करने के लिए कॉन्फ़िगर किए गए हैं।",
"TaskCleanTranscode": "स्वच्छ ट्रांसकोड निर्देशिका",
"TaskCleanTranscodeDescription": "एक दिन से अधिक पुरानी ट्रांसकोड फ़ाइलें हटाता है.",
"TaskRefreshChannelsDescription": "इंटरनेट चैनल की जानकारी को ताज़ा करता है।",
"TaskOptimizeDatabaseDescription": "डेटाबेस को कॉम्पैक्ट करता है और मुक्त स्थान को छोटा करता है। लाइब्रेरी को स्कैन करने के बाद इस कार्य को चलाने या अन्य परिवर्तन करने से जो डेटाबेस संशोधनों को लागू करते हैं, प्रदर्शन में सुधार कर सकते हैं।",
"TaskRefreshChannels": "इंटरनेट चैनल की जानकारी को ताज़ा करता है",
"TaskRefreshChapterImages": "अध्याय छवियाँ निकालें",
"TaskCleanLogsDescription": "{0} दिन से अधिक पुरानी लॉग फ़ाइलें हटाता है।",
"TaskCleanCacheDescription": "उन कैश फ़ाइलों को हटाता है जिनकी अब सिस्टम को आवश्यकता नहीं है।",
"TaskUpdatePlugins": "अद्यतन प्लगइन्स",
"TaskRefreshPeopleDescription": "आपकी मीडिया लाइब्रेरी में अभिनेताओं और निर्देशकों के लिए मेटाडेटा अपडेट करता है।",
"TaskCleanCache": "स्वच्छ कैश निर्देशिका",
"TaskDownloadMissingSubtitlesDescription": "मेटाडेटा कॉन्फ़िगरेशन के आधार पर लापता उपशीर्षक के लिए इंटरनेट खोजता है।",
"TaskKeyframeExtractorDescription": "अधिक सटीक एचएलएस प्लेलिस्ट बनाने के लिए वीडियो फ़ाइलों से मुख्य-फ़्रेम निकालता है। यह कार्य लंबे समय तक चल सकता है।"
}

@ -20,9 +20,9 @@
"HeaderFavoriteAlbums": "Mėgstami Albumai",
"HeaderFavoriteArtists": "Mėgstami Atlikėjai",
"HeaderFavoriteEpisodes": "Mėgstamiausios serijos",
"HeaderFavoriteShows": "Mėgstamiausi serialai",
"HeaderFavoriteSongs": "Mėgstamos dainos",
"HeaderLiveTV": "TV gyvai",
"HeaderFavoriteShows": "Mėgstamiausios TV Laidos",
"HeaderFavoriteSongs": "Mėgstamos Dainos",
"HeaderLiveTV": "Tiesioginė TV",
"HeaderNextUp": "Toliau eilėje",
"HeaderRecordingGroups": "Įrašų grupės",
"HomeVideos": "Namų vaizdo įrašai",

@ -42,7 +42,7 @@ namespace Emby.Server.Implementations.Playlists
query.Recursive = true;
query.IncludeItemTypes = new[] { BaseItemKind.Playlist };
return LibraryManager.GetItemsResult(query);
return QueryWithPostFiltering2(query);
}
public override string GetClientTypeName()

@ -512,12 +512,10 @@ public class ItemsController : BaseJellyfinApiController
result = new QueryResult<BaseItem>(itemsArray);
}
// result might include items not accessible by the user, DtoService will remove them
var accessibleItems = _dtoService.GetBaseItemDtos(result.Items, dtoOptions, user);
return new QueryResult<BaseItemDto>(
startIndex,
accessibleItems.Count,
accessibleItems);
result.TotalRecordCount,
_dtoService.GetBaseItemDtos(result.Items, dtoOptions, user));
}
/// <summary>

@ -131,6 +131,10 @@ public class StartupController : BaseJellyfinApiController
public async Task<ActionResult> UpdateStartupUser([FromBody] StartupUserDto startupUserDto)
{
var user = _userManager.Users.First();
if (string.IsNullOrWhiteSpace(startupUserDto.Password))
{
return BadRequest("Password must not be empty");
}
if (startupUserDto.Name is not null)
{

@ -323,36 +323,16 @@ public class UserController : BaseJellyfinApiController
/// <response code="404">User not found.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success or a <see cref="ForbidResult"/> or a <see cref="NotFoundResult"/> on failure.</returns>
[HttpPost("{userId}/EasyPassword")]
[Obsolete("Use Quick Connect instead")]
[Authorize]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status403Forbidden)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> UpdateUserEasyPassword(
public ActionResult UpdateUserEasyPassword(
[FromRoute, Required] Guid userId,
[FromBody, Required] UpdateUserEasyPassword request)
{
if (!RequestHelpers.AssertCanUpdateUser(_userManager, User, userId, true))
{
return StatusCode(StatusCodes.Status403Forbidden, "User is not allowed to update the easy password.");
}
var user = _userManager.GetUserById(userId);
if (user is null)
{
return NotFound("User not found");
}
if (request.ResetPassword)
{
await _userManager.ResetEasyPassword(user).ConfigureAwait(false);
}
else
{
await _userManager.ChangeEasyPassword(user, request.NewPw ?? string.Empty, request.NewPassword ?? string.Empty).ConfigureAwait(false);
}
return NoContent();
return Forbid();
}
/// <summary>

@ -91,16 +91,6 @@ namespace Jellyfin.Data.Entities
[StringLength(65535)]
public string? Password { get; set; }
/// <summary>
/// Gets or sets the user's easy password, or <c>null</c> if none is set.
/// </summary>
/// <remarks>
/// Max length = 65535.
/// </remarks>
[MaxLength(65535)]
[StringLength(65535)]
public string? EasyPassword { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the user must update their password.
/// </summary>

@ -0,0 +1,650 @@
// <auto-generated />
using System;
using Jellyfin.Server.Implementations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
[DbContext(typeof(JellyfinDbContext))]
[Migration("20230526173516_RemoveEasyPassword")]
partial class RemoveEasyPassword
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.5");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("DayOfWeek")
.HasColumnType("INTEGER");
b.Property<double>("EndHour")
.HasColumnType("REAL");
b.Property<double>("StartHour")
.HasColumnType("REAL");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("AccessSchedules");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<string>("ItemId")
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<int>("LogSeverity")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<string>("Overview")
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("ShortOverview")
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DateCreated");
b.ToTable("ActivityLogs");
});
modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<string>("Value")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId", "ItemId", "Client", "Key")
.IsUnique();
b.ToTable("CustomItemDisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("ChromecastVersion")
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<string>("DashboardTheme")
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<bool>("EnableNextVideoInfoOverlay")
.HasColumnType("INTEGER");
b.Property<int?>("IndexBy")
.HasColumnType("INTEGER");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<int>("ScrollDirection")
.HasColumnType("INTEGER");
b.Property<bool>("ShowBackdrop")
.HasColumnType("INTEGER");
b.Property<bool>("ShowSidebar")
.HasColumnType("INTEGER");
b.Property<int>("SkipBackwardLength")
.HasColumnType("INTEGER");
b.Property<int>("SkipForwardLength")
.HasColumnType("INTEGER");
b.Property<string>("TvHome")
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId", "ItemId", "Client")
.IsUnique();
b.ToTable("DisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("DisplayPreferencesId")
.HasColumnType("INTEGER");
b.Property<int>("Order")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("DisplayPreferencesId");
b.ToTable("HomeSection");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<DateTime>("LastModified")
.HasColumnType("TEXT");
b.Property<string>("Path")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<Guid?>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("ImageInfos");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<int?>("IndexBy")
.HasColumnType("INTEGER");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<bool>("RememberIndexing")
.HasColumnType("INTEGER");
b.Property<bool>("RememberSorting")
.HasColumnType("INTEGER");
b.Property<string>("SortBy")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.Property<int>("ViewType")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("ItemDisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<Guid?>("Permission_Permissions_Guid")
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<Guid?>("UserId")
.HasColumnType("TEXT");
b.Property<bool>("Value")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId", "Kind")
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("Permissions");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<int>("Kind")
.HasColumnType("INTEGER");
b.Property<Guid?>("Preference_Preferences_Guid")
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<Guid?>("UserId")
.HasColumnType("TEXT");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(65535)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId", "Kind")
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("Preferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AccessToken")
.IsRequired()
.HasColumnType("TEXT");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateLastActivity")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("AccessToken")
.IsUnique();
b.ToTable("ApiKeys");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("AccessToken")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("AppName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<string>("AppVersion")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<DateTime>("DateCreated")
.HasColumnType("TEXT");
b.Property<DateTime>("DateLastActivity")
.HasColumnType("TEXT");
b.Property<DateTime>("DateModified")
.HasColumnType("TEXT");
b.Property<string>("DeviceId")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<string>("DeviceName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<bool>("IsActive")
.HasColumnType("INTEGER");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DeviceId");
b.HasIndex("AccessToken", "DateLastActivity");
b.HasIndex("DeviceId", "DateLastActivity");
b.HasIndex("UserId", "DeviceId");
b.ToTable("Devices");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("CustomName")
.HasColumnType("TEXT");
b.Property<string>("DeviceId")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DeviceId")
.IsUnique();
b.ToTable("DeviceOptions");
});
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<string>("AudioLanguagePreference")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("AuthenticationProviderId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<bool>("DisplayCollectionsView")
.HasColumnType("INTEGER");
b.Property<bool>("DisplayMissingEpisodes")
.HasColumnType("INTEGER");
b.Property<bool>("EnableAutoLogin")
.HasColumnType("INTEGER");
b.Property<bool>("EnableLocalPassword")
.HasColumnType("INTEGER");
b.Property<bool>("EnableNextEpisodeAutoPlay")
.HasColumnType("INTEGER");
b.Property<bool>("EnableUserPreferenceAccess")
.HasColumnType("INTEGER");
b.Property<bool>("HidePlayedInLatest")
.HasColumnType("INTEGER");
b.Property<long>("InternalId")
.HasColumnType("INTEGER");
b.Property<int>("InvalidLoginAttemptCount")
.HasColumnType("INTEGER");
b.Property<DateTime?>("LastActivityDate")
.HasColumnType("TEXT");
b.Property<DateTime?>("LastLoginDate")
.HasColumnType("TEXT");
b.Property<int?>("LoginAttemptsBeforeLockout")
.HasColumnType("INTEGER");
b.Property<int>("MaxActiveSessions")
.HasColumnType("INTEGER");
b.Property<int?>("MaxParentalAgeRating")
.HasColumnType("INTEGER");
b.Property<bool>("MustUpdatePassword")
.HasColumnType("INTEGER");
b.Property<string>("Password")
.HasMaxLength(65535)
.HasColumnType("TEXT");
b.Property<string>("PasswordResetProviderId")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<bool>("PlayDefaultAudioTrack")
.HasColumnType("INTEGER");
b.Property<bool>("RememberAudioSelections")
.HasColumnType("INTEGER");
b.Property<bool>("RememberSubtitleSelections")
.HasColumnType("INTEGER");
b.Property<int?>("RemoteClientBitrateLimit")
.HasColumnType("INTEGER");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("SubtitleLanguagePreference")
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<int>("SubtitleMode")
.HasColumnType("INTEGER");
b.Property<int>("SyncPlayAccess")
.HasColumnType("INTEGER");
b.Property<string>("Username")
.IsRequired()
.HasMaxLength(255)
.HasColumnType("TEXT")
.UseCollation("NOCASE");
b.HasKey("Id");
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("AccessSchedules")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("DisplayPreferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
{
b.HasOne("Jellyfin.Data.Entities.DisplayPreferences", null)
.WithMany("HomeSections")
.HasForeignKey("DisplayPreferencesId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithOne("ProfileImage")
.HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("ItemDisplayPreferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Permissions")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Preferences")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.Navigation("HomeSections");
});
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Navigation("AccessSchedules");
b.Navigation("DisplayPreferences");
b.Navigation("ItemDisplayPreferences");
b.Navigation("Permissions");
b.Navigation("Preferences");
b.Navigation("ProfileImage");
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,164 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Jellyfin.Server.Implementations.Migrations
{
/// <inheritdoc />
public partial class RemoveEasyPassword : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "EasyPassword",
schema: "jellyfin",
table: "Users");
migrationBuilder.RenameTable(
name: "Users",
schema: "jellyfin",
newName: "Users");
migrationBuilder.RenameTable(
name: "Preferences",
schema: "jellyfin",
newName: "Preferences");
migrationBuilder.RenameTable(
name: "Permissions",
schema: "jellyfin",
newName: "Permissions");
migrationBuilder.RenameTable(
name: "ItemDisplayPreferences",
schema: "jellyfin",
newName: "ItemDisplayPreferences");
migrationBuilder.RenameTable(
name: "ImageInfos",
schema: "jellyfin",
newName: "ImageInfos");
migrationBuilder.RenameTable(
name: "HomeSection",
schema: "jellyfin",
newName: "HomeSection");
migrationBuilder.RenameTable(
name: "DisplayPreferences",
schema: "jellyfin",
newName: "DisplayPreferences");
migrationBuilder.RenameTable(
name: "Devices",
schema: "jellyfin",
newName: "Devices");
migrationBuilder.RenameTable(
name: "DeviceOptions",
schema: "jellyfin",
newName: "DeviceOptions");
migrationBuilder.RenameTable(
name: "CustomItemDisplayPreferences",
schema: "jellyfin",
newName: "CustomItemDisplayPreferences");
migrationBuilder.RenameTable(
name: "ApiKeys",
schema: "jellyfin",
newName: "ApiKeys");
migrationBuilder.RenameTable(
name: "ActivityLogs",
schema: "jellyfin",
newName: "ActivityLogs");
migrationBuilder.RenameTable(
name: "AccessSchedules",
schema: "jellyfin",
newName: "AccessSchedules");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "jellyfin");
migrationBuilder.RenameTable(
name: "Users",
newName: "Users",
newSchema: "jellyfin");
migrationBuilder.RenameTable(
name: "Preferences",
newName: "Preferences",
newSchema: "jellyfin");
migrationBuilder.RenameTable(
name: "Permissions",
newName: "Permissions",
newSchema: "jellyfin");
migrationBuilder.RenameTable(
name: "ItemDisplayPreferences",
newName: "ItemDisplayPreferences",
newSchema: "jellyfin");
migrationBuilder.RenameTable(
name: "ImageInfos",
newName: "ImageInfos",
newSchema: "jellyfin");
migrationBuilder.RenameTable(
name: "HomeSection",
newName: "HomeSection",
newSchema: "jellyfin");
migrationBuilder.RenameTable(
name: "DisplayPreferences",
newName: "DisplayPreferences",
newSchema: "jellyfin");
migrationBuilder.RenameTable(
name: "Devices",
newName: "Devices",
newSchema: "jellyfin");
migrationBuilder.RenameTable(
name: "DeviceOptions",
newName: "DeviceOptions",
newSchema: "jellyfin");
migrationBuilder.RenameTable(
name: "CustomItemDisplayPreferences",
newName: "CustomItemDisplayPreferences",
newSchema: "jellyfin");
migrationBuilder.RenameTable(
name: "ApiKeys",
newName: "ApiKeys",
newSchema: "jellyfin");
migrationBuilder.RenameTable(
name: "ActivityLogs",
newName: "ActivityLogs",
newSchema: "jellyfin");
migrationBuilder.RenameTable(
name: "AccessSchedules",
newName: "AccessSchedules",
newSchema: "jellyfin");
migrationBuilder.AddColumn<string>(
name: "EasyPassword",
schema: "jellyfin",
table: "Users",
type: "TEXT",
maxLength: 65535,
nullable: true);
}
}
}

@ -15,9 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("jellyfin")
.HasAnnotation("ProductVersion", "6.0.9");
modelBuilder.HasAnnotation("ProductVersion", "7.0.5");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@ -41,7 +39,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("UserId");
b.ToTable("AccessSchedules", "jellyfin");
b.ToTable("AccessSchedules");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ActivityLog", b =>
@ -89,7 +87,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("DateCreated");
b.ToTable("ActivityLogs", "jellyfin");
b.ToTable("ActivityLogs");
});
modelBuilder.Entity("Jellyfin.Data.Entities.CustomItemDisplayPreferences", b =>
@ -121,7 +119,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("UserId", "ItemId", "Client", "Key")
.IsUnique();
b.ToTable("CustomItemDisplayPreferences", "jellyfin");
b.ToTable("CustomItemDisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
@ -178,7 +176,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("UserId", "ItemId", "Client")
.IsUnique();
b.ToTable("DisplayPreferences", "jellyfin");
b.ToTable("DisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.HomeSection", b =>
@ -200,7 +198,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("DisplayPreferencesId");
b.ToTable("HomeSection", "jellyfin");
b.ToTable("HomeSection");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ImageInfo", b =>
@ -225,7 +223,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("UserId")
.IsUnique();
b.ToTable("ImageInfos", "jellyfin");
b.ToTable("ImageInfos");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
@ -269,7 +267,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("UserId");
b.ToTable("ItemDisplayPreferences", "jellyfin");
b.ToTable("ItemDisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Permission", b =>
@ -300,7 +298,7 @@ namespace Jellyfin.Server.Implementations.Migrations
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("Permissions", "jellyfin");
b.ToTable("Permissions");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
@ -333,7 +331,7 @@ namespace Jellyfin.Server.Implementations.Migrations
.IsUnique()
.HasFilter("[UserId] IS NOT NULL");
b.ToTable("Preferences", "jellyfin");
b.ToTable("Preferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.ApiKey", b =>
@ -362,7 +360,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("AccessToken")
.IsUnique();
b.ToTable("ApiKeys", "jellyfin");
b.ToTable("ApiKeys");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.Device", b =>
@ -420,7 +418,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("UserId", "DeviceId");
b.ToTable("Devices", "jellyfin");
b.ToTable("Devices");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Security.DeviceOptions", b =>
@ -441,7 +439,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("DeviceId")
.IsUnique();
b.ToTable("DeviceOptions", "jellyfin");
b.ToTable("DeviceOptions");
});
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
@ -465,10 +463,6 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<bool>("DisplayMissingEpisodes")
.HasColumnType("INTEGER");
b.Property<string>("EasyPassword")
.HasMaxLength(65535)
.HasColumnType("TEXT");
b.Property<bool>("EnableAutoLogin")
.HasColumnType("INTEGER");
@ -554,7 +548,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("Username")
.IsUnique();
b.ToTable("Users", "jellyfin");
b.ToTable("Users");
});
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>

@ -114,8 +114,6 @@ namespace Jellyfin.Server.Implementations.Users
await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false);
}
user.EasyPassword = pin;
return new ForgotPasswordResult
{
Action = ForgotPasswordAction.PinCode,

@ -268,37 +268,16 @@ namespace Jellyfin.Server.Implementations.Users
return ChangePassword(user, string.Empty);
}
/// <inheritdoc/>
public Task ResetEasyPassword(User user)
{
return ChangeEasyPassword(user, string.Empty, null);
}
/// <inheritdoc/>
public async Task ChangePassword(User user, string newPassword)
{
ArgumentNullException.ThrowIfNull(user);
await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false);
await UpdateUserAsync(user).ConfigureAwait(false);
await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(user)).ConfigureAwait(false);
}
/// <inheritdoc/>
public async Task ChangeEasyPassword(User user, string newPassword, string? newPasswordSha1)
{
if (newPassword is not null)
{
newPasswordSha1 = _cryptoProvider.CreatePasswordHash(newPassword).ToString();
}
if (string.IsNullOrWhiteSpace(newPasswordSha1))
if (user.HasPermission(PermissionKind.IsAdministrator) && string.IsNullOrWhiteSpace(newPassword))
{
throw new ArgumentNullException(nameof(newPasswordSha1));
throw new ArgumentException("Admin user passwords must not be empty", nameof(newPassword));
}
user.EasyPassword = newPasswordSha1;
await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false);
await UpdateUserAsync(user).ConfigureAwait(false);
await _eventManager.PublishAsync(new UserPasswordChangedEventArgs(user)).ConfigureAwait(false);
@ -315,7 +294,6 @@ namespace Jellyfin.Server.Implementations.Users
ServerId = _appHost.SystemId,
HasPassword = hasPassword,
HasConfiguredPassword = hasPassword,
HasConfiguredEasyPassword = !string.IsNullOrEmpty(user.EasyPassword),
EnableAutoLogin = user.EnableAutoLogin,
LastLoginDate = user.LastLoginDate,
LastActivityDate = user.LastActivityDate,
@ -832,16 +810,6 @@ namespace Jellyfin.Server.Implementations.Users
}
}
if (!success
&& _networkManager.IsInLocalNetwork(remoteEndPoint)
&& user?.EnableLocalPassword == true
&& !string.IsNullOrEmpty(user.EasyPassword))
{
// Check easy password
var passwordHash = PasswordHash.Parse(user.EasyPassword);
success = _cryptoProvider.Verify(passwordHash, password);
}
return (authenticationProvider, username, success);
}

@ -127,7 +127,6 @@ namespace Jellyfin.Server.Migrations.Routines
RememberSubtitleSelections = config.RememberSubtitleSelections,
SubtitleLanguagePreference = config.SubtitleLanguagePreference,
Password = mockup.Password,
EasyPassword = mockup.EasyPassword,
LastLoginDate = mockup.LastLoginDate,
LastActivityDate = mockup.LastActivityDate
};

@ -730,7 +730,7 @@ namespace MediaBrowser.Controller.Entities
return LibraryManager.GetItemsResult(query);
}
private QueryResult<BaseItem> QueryWithPostFiltering2(InternalItemsQuery query)
protected QueryResult<BaseItem> QueryWithPostFiltering2(InternalItemsQuery query)
{
var startIndex = query.StartIndex;
var limit = query.Limit;
@ -1272,7 +1272,7 @@ namespace MediaBrowser.Controller.Entities
{
ArgumentNullException.ThrowIfNull(user);
return GetChildren(user, includeLinkedChildren, null);
return GetChildren(user, includeLinkedChildren, new InternalItemsQuery(user));
}
public virtual List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)

@ -59,6 +59,11 @@ namespace MediaBrowser.Controller.Extensions
/// </summary>
public const string UnixSocketPermissionsKey = "kestrel:socketPermissions";
/// <summary>
/// The cache size of the SQL database, see cache_size.
/// </summary>
public const string SqliteCacheSizeKey = "sqlite:cacheSize";
/// <summary>
/// Gets a value indicating whether the application should host static web content from the <see cref="IConfiguration"/>.
/// </summary>
@ -115,5 +120,13 @@ namespace MediaBrowser.Controller.Extensions
/// <returns>The unix socket permissions.</returns>
public static string? GetUnixSocketPermissions(this IConfiguration configuration)
=> configuration[UnixSocketPermissionsKey];
/// <summary>
/// Gets the cache_size from the <see cref="IConfiguration" />.
/// </summary>
/// <param name="configuration">The configuration to read the setting from.</param>
/// <returns>The sqlite cache size.</returns>
public static int? GetSqliteCacheSize(this IConfiguration configuration)
=> configuration.GetValue<int?>(SqliteCacheSizeKey);
}
}

@ -96,13 +96,6 @@ namespace MediaBrowser.Controller.Library
/// <returns>Task.</returns>
Task ResetPassword(User user);
/// <summary>
/// Resets the easy password.
/// </summary>
/// <param name="user">The user.</param>
/// <returns>Task.</returns>
Task ResetEasyPassword(User user);
/// <summary>
/// Changes the password.
/// </summary>
@ -111,15 +104,6 @@ namespace MediaBrowser.Controller.Library
/// <returns>Awaitable task.</returns>
Task ChangePassword(User user, string newPassword);
/// <summary>
/// Changes the easy password.
/// </summary>
/// <param name="user">The user.</param>
/// <param name="newPassword">New password to use.</param>
/// <param name="newPasswordSha1">Hash of new password.</param>
/// <returns>Task.</returns>
Task ChangeEasyPassword(User user, string newPassword, string newPasswordSha1);
/// <summary>
/// Gets the user dto.
/// </summary>

@ -783,7 +783,7 @@ namespace MediaBrowser.Model.Dto
/// Gets or sets the LUFS value.
/// </summary>
/// <value>The LUFS Value.</value>
public float LUFS { get; set; }
public float? LUFS { get; set; }
/// <summary>
/// Gets or sets the current program.

@ -66,6 +66,7 @@ namespace MediaBrowser.Model.Dto
/// Gets or sets a value indicating whether this instance has configured easy password.
/// </summary>
/// <value><c>true</c> if this instance has configured easy password; otherwise, <c>false</c>.</value>
[Obsolete("Easy Password has been replaced with Quick Connect")]
public bool HasConfiguredEasyPassword { get; set; }
/// <summary>

@ -6,6 +6,7 @@ using Emby.Server.Implementations.Data;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Configuration;
using Moq;
using Xunit;
@ -27,8 +28,18 @@ namespace Jellyfin.Server.Implementations.Tests.Data
appHost.Setup(x => x.ReverseVirtualPath(It.IsAny<string>()))
.Returns((string x) => x.Replace(MetaDataPath, VirtualMetaDataPath, StringComparison.Ordinal));
var configSection = new Mock<IConfigurationSection>();
configSection
.SetupGet(x => x[It.Is<string>(s => s == MediaBrowser.Controller.Extensions.ConfigurationExtensions.SqliteCacheSizeKey)])
.Returns("0");
var config = new Mock<IConfiguration>();
config
.Setup(x => x.GetSection(It.Is<string>(s => s == MediaBrowser.Controller.Extensions.ConfigurationExtensions.SqliteCacheSizeKey)))
.Returns(configSection.Object);
_fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true });
_fixture.Inject(appHost);
_fixture.Inject(config);
_sqliteItemRepository = _fixture.Create<SqliteItemRepository>();
}

Loading…
Cancel
Save