Merge pull request #4652 from crobibero/display-preferences

Add support for custom item display preferences
This commit is contained in:
Joshua M. Boniface 2020-12-04 20:05:04 -05:00 committed by GitHub
commit 4e6584c345
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 964 additions and 58 deletions

View File

@ -6,6 +6,7 @@ using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Authorization;
@ -47,13 +48,19 @@ namespace Jellyfin.Api.Controllers
[FromQuery, Required] Guid userId,
[FromQuery, Required] string client)
{
var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, client);
var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, Guid.Empty, displayPreferences.Client);
if (!Guid.TryParse(displayPreferencesId, out var itemId))
{
itemId = displayPreferencesId.GetMD5();
}
var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
itemPreferences.ItemId = itemId;
var dto = new DisplayPreferencesDto
{
Client = displayPreferences.Client,
Id = displayPreferences.UserId.ToString(),
Id = displayPreferences.ItemId.ToString(),
ViewType = itemPreferences.ViewType.ToString(),
SortBy = itemPreferences.SortBy,
SortOrder = itemPreferences.SortOrder,
@ -81,6 +88,16 @@ namespace Jellyfin.Api.Controllers
dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString(CultureInfo.InvariantCulture);
dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
// Load all custom display preferences
var customDisplayPreferences = _displayPreferencesManager.ListCustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client);
if (customDisplayPreferences != null)
{
foreach (var (key, value) in customDisplayPreferences)
{
dto.CustomPrefs.TryAdd(key, value);
}
}
// This will essentially be a noop if no changes have been made, but new prefs must be saved at least.
_displayPreferencesManager.SaveChanges();
@ -115,7 +132,12 @@ namespace Jellyfin.Api.Controllers
HomeSectionType.LatestMedia, HomeSectionType.None,
};
var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, client);
if (!Guid.TryParse(displayPreferencesId, out var itemId))
{
itemId = displayPreferencesId.GetMD5();
}
var existingDisplayPreferences = _displayPreferencesManager.GetDisplayPreferences(userId, itemId, client);
existingDisplayPreferences.IndexBy = Enum.TryParse<IndexingKind>(displayPreferences.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null;
existingDisplayPreferences.ShowBackdrop = displayPreferences.ShowBackdrop;
existingDisplayPreferences.ShowSidebar = displayPreferences.ShowSidebar;
@ -124,21 +146,33 @@ namespace Jellyfin.Api.Controllers
existingDisplayPreferences.ChromecastVersion = displayPreferences.CustomPrefs.TryGetValue("chromecastVersion", out var chromecastVersion)
? Enum.Parse<ChromecastVersion>(chromecastVersion, true)
: ChromecastVersion.Stable;
displayPreferences.CustomPrefs.Remove("chromecastVersion");
existingDisplayPreferences.EnableNextVideoInfoOverlay = displayPreferences.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enableNextVideoInfoOverlay)
? bool.Parse(enableNextVideoInfoOverlay)
: true;
displayPreferences.CustomPrefs.Remove("enableNextVideoInfoOverlay");
existingDisplayPreferences.SkipBackwardLength = displayPreferences.CustomPrefs.TryGetValue("skipBackLength", out var skipBackLength)
? int.Parse(skipBackLength, CultureInfo.InvariantCulture)
: 10000;
displayPreferences.CustomPrefs.Remove("skipBackLength");
existingDisplayPreferences.SkipForwardLength = displayPreferences.CustomPrefs.TryGetValue("skipForwardLength", out var skipForwardLength)
? int.Parse(skipForwardLength, CultureInfo.InvariantCulture)
: 30000;
displayPreferences.CustomPrefs.Remove("skipForwardLength");
existingDisplayPreferences.DashboardTheme = displayPreferences.CustomPrefs.TryGetValue("dashboardTheme", out var theme)
? theme
: string.Empty;
displayPreferences.CustomPrefs.Remove("dashboardTheme");
existingDisplayPreferences.TvHome = displayPreferences.CustomPrefs.TryGetValue("tvhome", out var home)
? home
: string.Empty;
displayPreferences.CustomPrefs.Remove("tvhome");
existingDisplayPreferences.HomeSections.Clear();
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("homesection", StringComparison.OrdinalIgnoreCase)))
@ -149,26 +183,34 @@ namespace Jellyfin.Api.Controllers
type = order < 7 ? defaults[order] : HomeSectionType.None;
}
displayPreferences.CustomPrefs.Remove(key);
existingDisplayPreferences.HomeSections.Add(new HomeSection { Order = order, Type = type });
}
foreach (var key in displayPreferences.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.OrdinalIgnoreCase)))
{
var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, Guid.Parse(key.Substring("landing-".Length)), existingDisplayPreferences.Client);
itemPreferences.ViewType = Enum.Parse<ViewType>(displayPreferences.ViewType);
if (Guid.TryParse(key.AsSpan().Slice("landing-".Length), out var preferenceId))
{
var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, preferenceId, existingDisplayPreferences.Client);
itemPreferences.ViewType = Enum.Parse<ViewType>(displayPreferences.ViewType);
displayPreferences.CustomPrefs.Remove(key);
}
}
var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, Guid.Empty, existingDisplayPreferences.Client);
var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(existingDisplayPreferences.UserId, itemId, existingDisplayPreferences.Client);
itemPrefs.SortBy = displayPreferences.SortBy;
itemPrefs.SortOrder = displayPreferences.SortOrder;
itemPrefs.RememberIndexing = displayPreferences.RememberIndexing;
itemPrefs.RememberSorting = displayPreferences.RememberSorting;
itemPrefs.ItemId = itemId;
if (Enum.TryParse<ViewType>(displayPreferences.ViewType, true, out var viewType))
{
itemPrefs.ViewType = viewType;
}
// Set all remaining custom preferences.
_displayPreferencesManager.SetCustomItemDisplayPreferences(userId, itemId, existingDisplayPreferences.Client, displayPreferences.CustomPrefs);
_displayPreferencesManager.SaveChanges();
return NoContent();

View File

@ -0,0 +1,90 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Jellyfin.Data.Entities
{
/// <summary>
/// An entity that represents a user's custom display preferences for a specific item.
/// </summary>
public class CustomItemDisplayPreferences
{
/// <summary>
/// Initializes a new instance of the <see cref="CustomItemDisplayPreferences"/> class.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="itemId">The item id.</param>
/// <param name="client">The client.</param>
/// <param name="preferenceKey">The preference key.</param>
/// <param name="preferenceValue">The preference value.</param>
public CustomItemDisplayPreferences(Guid userId, Guid itemId, string client, string preferenceKey, string preferenceValue)
{
UserId = userId;
ItemId = itemId;
Client = client;
Key = preferenceKey;
Value = preferenceValue;
}
/// <summary>
/// Initializes a new instance of the <see cref="CustomItemDisplayPreferences"/> class.
/// </summary>
protected CustomItemDisplayPreferences()
{
}
/// <summary>
/// Gets or sets the Id.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; protected set; }
/// <summary>
/// Gets or sets the user Id.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public Guid UserId { get; set; }
/// <summary>
/// Gets or sets the id of the associated item.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public Guid ItemId { get; set; }
/// <summary>
/// Gets or sets the client string.
/// </summary>
/// <remarks>
/// Required. Max Length = 32.
/// </remarks>
[Required]
[MaxLength(32)]
[StringLength(32)]
public string Client { get; set; }
/// <summary>
/// Gets or sets the preference key.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[Required]
public string Key { get; set; }
/// <summary>
/// Gets or sets the preference value.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[Required]
public string Value { get; set; }
}
}

View File

@ -17,10 +17,12 @@ namespace Jellyfin.Data.Entities
/// Initializes a new instance of the <see cref="DisplayPreferences"/> class.
/// </summary>
/// <param name="userId">The user's id.</param>
/// <param name="itemId">The item id.</param>
/// <param name="client">The client string.</param>
public DisplayPreferences(Guid userId, string client)
public DisplayPreferences(Guid userId, Guid itemId, string client)
{
UserId = userId;
ItemId = itemId;
Client = client;
ShowSidebar = false;
ShowBackdrop = true;
@ -58,6 +60,14 @@ namespace Jellyfin.Data.Entities
/// </remarks>
public Guid UserId { get; set; }
/// <summary>
/// Gets or sets the id of the associated item.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public Guid ItemId { get; set; }
/// <summary>
/// Gets or sets the client string.
/// </summary>

View File

@ -29,7 +29,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All"/>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
<!-- Code analysers-->

View File

@ -34,6 +34,8 @@ namespace Jellyfin.Server.Implementations
public virtual DbSet<ItemDisplayPreferences> ItemDisplayPreferences { get; set; }
public virtual DbSet<CustomItemDisplayPreferences> CustomItemDisplayPreferences { get; set; }
public virtual DbSet<Permission> Permissions { get; set; }
public virtual DbSet<Preference> Preferences { get; set; }
@ -151,7 +153,15 @@ namespace Jellyfin.Server.Implementations
.IsUnique(false);
modelBuilder.Entity<DisplayPreferences>()
.HasIndex(entity => new { entity.UserId, entity.Client })
.HasIndex(entity => new { entity.UserId, entity.ItemId, entity.Client })
.IsUnique();
modelBuilder.Entity<CustomItemDisplayPreferences>()
.HasIndex(entity => entity.UserId)
.IsUnique(false);
modelBuilder.Entity<CustomItemDisplayPreferences>()
.HasIndex(entity => new { entity.UserId, entity.ItemId, entity.Client, entity.Key })
.IsUnique();
}
}

View File

@ -0,0 +1,522 @@
#pragma warning disable CS1591
// <auto-generated />
using System;
using Jellyfin.Server.Implementations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
[DbContext(typeof(JellyfinDb))]
[Migration("20201204223655_AddCustomDisplayPreferences")]
partial class AddCustomDisplayPreferences
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("jellyfin")
.HasAnnotation("ProductVersion", "5.0.0");
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.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")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
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");
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<bool>("Value")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("Permission_Permissions_Guid");
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<string>("Value")
.IsRequired()
.HasMaxLength(65535)
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Preference_Preferences_Guid");
b.ToTable("Preferences");
});
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<string>("EasyPassword")
.HasMaxLength(65535)
.HasColumnType("TEXT");
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");
b.HasKey("Id");
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)
.WithOne("DisplayPreferences")
.HasForeignKey("Jellyfin.Data.Entities.DisplayPreferences", "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");
});
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("Permission_Permissions_Guid");
});
modelBuilder.Entity("Jellyfin.Data.Entities.Preference", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithMany("Preferences")
.HasForeignKey("Preference_Preferences_Guid");
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.Navigation("HomeSections");
});
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Navigation("AccessSchedules");
b.Navigation("DisplayPreferences")
.IsRequired();
b.Navigation("ItemDisplayPreferences");
b.Navigation("Permissions");
b.Navigation("Preferences");
b.Navigation("ProfileImage");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,108 @@
#pragma warning disable CS1591
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Jellyfin.Server.Implementations.Migrations
{
public partial class AddCustomDisplayPreferences : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_DisplayPreferences_UserId_Client",
schema: "jellyfin",
table: "DisplayPreferences");
migrationBuilder.AlterColumn<int>(
name: "MaxActiveSessions",
schema: "jellyfin",
table: "Users",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AddColumn<Guid>(
name: "ItemId",
schema: "jellyfin",
table: "DisplayPreferences",
type: "TEXT",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
migrationBuilder.CreateTable(
name: "CustomItemDisplayPreferences",
schema: "jellyfin",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<Guid>(type: "TEXT", nullable: false),
ItemId = table.Column<Guid>(type: "TEXT", nullable: false),
Client = table.Column<string>(type: "TEXT", maxLength: 32, nullable: false),
Key = table.Column<string>(type: "TEXT", nullable: false),
Value = table.Column<string>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_CustomItemDisplayPreferences", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_DisplayPreferences_UserId_ItemId_Client",
schema: "jellyfin",
table: "DisplayPreferences",
columns: new[] { "UserId", "ItemId", "Client" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_CustomItemDisplayPreferences_UserId",
schema: "jellyfin",
table: "CustomItemDisplayPreferences",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_CustomItemDisplayPreferences_UserId_ItemId_Client_Key",
schema: "jellyfin",
table: "CustomItemDisplayPreferences",
columns: new[] { "UserId", "ItemId", "Client", "Key" },
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "CustomItemDisplayPreferences",
schema: "jellyfin");
migrationBuilder.DropIndex(
name: "IX_DisplayPreferences_UserId_ItemId_Client",
schema: "jellyfin",
table: "DisplayPreferences");
migrationBuilder.DropColumn(
name: "ItemId",
schema: "jellyfin",
table: "DisplayPreferences");
migrationBuilder.AlterColumn<int>(
name: "MaxActiveSessions",
schema: "jellyfin",
table: "Users",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.CreateIndex(
name: "IX_DisplayPreferences_UserId_Client",
schema: "jellyfin",
table: "DisplayPreferences",
columns: new[] { "UserId", "Client" },
unique: true);
}
}
}

View File

@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("jellyfin")
.HasAnnotation("ProductVersion", "3.1.8");
.HasAnnotation("ProductVersion", "5.0.0");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@ -52,33 +52,33 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasColumnType("TEXT");
b.Property<string>("ItemId")
.HasColumnType("TEXT")
.HasMaxLength(256);
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<int>("LogSeverity")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(512);
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<string>("Overview")
.HasColumnType("TEXT")
.HasMaxLength(512);
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("ShortOverview")
.HasColumnType("TEXT")
.HasMaxLength(512);
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<string>("Type")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(256);
.HasMaxLength(256)
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
@ -88,6 +88,41 @@ namespace Jellyfin.Server.Implementations.Migrations
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")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId");
b.HasIndex("UserId", "ItemId", "Client", "Key")
.IsUnique();
b.ToTable("CustomItemDisplayPreferences");
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.Property<int>("Id")
@ -99,12 +134,12 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<string>("Client")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(32);
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<string>("DashboardTheme")
.HasColumnType("TEXT")
.HasMaxLength(32);
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<bool>("EnableNextVideoInfoOverlay")
.HasColumnType("INTEGER");
@ -112,6 +147,9 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<int?>("IndexBy")
.HasColumnType("INTEGER");
b.Property<Guid>("ItemId")
.HasColumnType("TEXT");
b.Property<int>("ScrollDirection")
.HasColumnType("INTEGER");
@ -128,8 +166,8 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasColumnType("INTEGER");
b.Property<string>("TvHome")
.HasColumnType("TEXT")
.HasMaxLength(32);
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
@ -138,7 +176,7 @@ namespace Jellyfin.Server.Implementations.Migrations
b.HasIndex("UserId");
b.HasIndex("UserId", "Client")
b.HasIndex("UserId", "ItemId", "Client")
.IsUnique();
b.ToTable("DisplayPreferences");
@ -177,8 +215,8 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(512);
.HasMaxLength(512)
.HasColumnType("TEXT");
b.Property<Guid?>("UserId")
.HasColumnType("TEXT");
@ -199,8 +237,8 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<string>("Client")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(32);
.HasMaxLength(32)
.HasColumnType("TEXT");
b.Property<int?>("IndexBy")
.HasColumnType("INTEGER");
@ -216,8 +254,8 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<string>("SortBy")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(64);
.HasMaxLength(64)
.HasColumnType("TEXT");
b.Property<int>("SortOrder")
.HasColumnType("INTEGER");
@ -279,8 +317,8 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<string>("Value")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(65535);
.HasMaxLength(65535)
.HasColumnType("TEXT");
b.HasKey("Id");
@ -296,13 +334,13 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasColumnType("TEXT");
b.Property<string>("AudioLanguagePreference")
.HasColumnType("TEXT")
.HasMaxLength(255);
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<string>("AuthenticationProviderId")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(255);
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<bool>("DisplayCollectionsView")
.HasColumnType("INTEGER");
@ -311,8 +349,8 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasColumnType("INTEGER");
b.Property<string>("EasyPassword")
.HasColumnType("TEXT")
.HasMaxLength(65535);
.HasMaxLength(65535)
.HasColumnType("TEXT");
b.Property<bool>("EnableAutoLogin")
.HasColumnType("INTEGER");
@ -354,13 +392,13 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasColumnType("INTEGER");
b.Property<string>("Password")
.HasColumnType("TEXT")
.HasMaxLength(65535);
.HasMaxLength(65535)
.HasColumnType("TEXT");
b.Property<string>("PasswordResetProviderId")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(255);
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<bool>("PlayDefaultAudioTrack")
.HasColumnType("INTEGER");
@ -379,8 +417,8 @@ namespace Jellyfin.Server.Implementations.Migrations
.HasColumnType("INTEGER");
b.Property<string>("SubtitleLanguagePreference")
.HasColumnType("TEXT")
.HasMaxLength(255);
.HasMaxLength(255)
.HasColumnType("TEXT");
b.Property<int>("SubtitleMode")
.HasColumnType("INTEGER");
@ -390,8 +428,8 @@ namespace Jellyfin.Server.Implementations.Migrations
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(255);
.HasMaxLength(255)
.HasColumnType("TEXT");
b.HasKey("Id");
@ -454,6 +492,27 @@ namespace Jellyfin.Server.Implementations.Migrations
.WithMany("Preferences")
.HasForeignKey("Preference_Preferences_Guid");
});
modelBuilder.Entity("Jellyfin.Data.Entities.DisplayPreferences", b =>
{
b.Navigation("HomeSections");
});
modelBuilder.Entity("Jellyfin.Data.Entities.User", b =>
{
b.Navigation("AccessSchedules");
b.Navigation("DisplayPreferences")
.IsRequired();
b.Navigation("ItemDisplayPreferences");
b.Navigation("Permissions");
b.Navigation("Preferences");
b.Navigation("ProfileImage");
});
#pragma warning restore 612, 618
}
}

View File

@ -26,16 +26,16 @@ namespace Jellyfin.Server.Implementations.Users
}
/// <inheritdoc />
public DisplayPreferences GetDisplayPreferences(Guid userId, string client)
public DisplayPreferences GetDisplayPreferences(Guid userId, Guid itemId, string client)
{
var prefs = _dbContext.DisplayPreferences
.Include(pref => pref.HomeSections)
.FirstOrDefault(pref =>
pref.UserId == userId && string.Equals(pref.Client, client));
pref.UserId == userId && string.Equals(pref.Client, client) && pref.ItemId == itemId);
if (prefs == null)
{
prefs = new DisplayPreferences(userId, client);
prefs = new DisplayPreferences(userId, itemId, client);
_dbContext.DisplayPreferences.Add(prefs);
}
@ -66,6 +66,34 @@ namespace Jellyfin.Server.Implementations.Users
.ToList();
}
/// <inheritdoc />
public IDictionary<string, string> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client)
{
return _dbContext.CustomItemDisplayPreferences
.AsQueryable()
.Where(prefs => prefs.UserId == userId
&& prefs.ItemId == itemId
&& string.Equals(prefs.Client, client))
.ToDictionary(prefs => prefs.Key, prefs => prefs.Value);
}
/// <inheritdoc />
public void SetCustomItemDisplayPreferences(Guid userId, Guid itemId, string client, Dictionary<string, string> customPreferences)
{
var existingPrefs = _dbContext.CustomItemDisplayPreferences
.AsQueryable()
.Where(prefs => prefs.UserId == userId
&& prefs.ItemId == itemId
&& string.Equals(prefs.Client, client));
_dbContext.CustomItemDisplayPreferences.RemoveRange(existingPrefs);
foreach (var (key, value) in customPreferences)
{
_dbContext.CustomItemDisplayPreferences
.Add(new CustomItemDisplayPreferences(userId, itemId, client, key, value));
}
}
/// <inheritdoc />
public void SaveChanges()
{

View File

@ -8,6 +8,7 @@ using System.Text.Json.Serialization;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Server.Implementations;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
@ -94,6 +95,7 @@ namespace Jellyfin.Server.Migrations.Routines
continue;
}
var itemId = new Guid(result[1].ToBlob());
var dtoUserId = new Guid(result[1].ToBlob());
var existingUser = _userManager.GetUserById(dtoUserId);
if (existingUser == null)
@ -105,8 +107,9 @@ namespace Jellyfin.Server.Migrations.Routines
var chromecastVersion = dto.CustomPrefs.TryGetValue("chromecastVersion", out var version)
? chromecastDict[version]
: ChromecastVersion.Stable;
dto.CustomPrefs.Remove("chromecastVersion");
var displayPreferences = new DisplayPreferences(dtoUserId, result[2].ToString())
var displayPreferences = new DisplayPreferences(dtoUserId, itemId, result[2].ToString())
{
IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null,
ShowBackdrop = dto.ShowBackdrop,
@ -126,15 +129,24 @@ namespace Jellyfin.Server.Migrations.Routines
TvHome = dto.CustomPrefs.TryGetValue("tvhome", out var home) ? home : string.Empty
};
dto.CustomPrefs.Remove("skipForwardLength");
dto.CustomPrefs.Remove("skipBackLength");
dto.CustomPrefs.Remove("enableNextVideoInfoOverlay");
dto.CustomPrefs.Remove("dashboardtheme");
dto.CustomPrefs.Remove("tvhome");
for (int i = 0; i < 7; i++)
{
dto.CustomPrefs.TryGetValue("homesection" + i, out var homeSection);
var key = "homesection" + i;
dto.CustomPrefs.TryGetValue(key, out var homeSection);
displayPreferences.HomeSections.Add(new HomeSection
{
Order = i,
Type = Enum.TryParse<HomeSectionType>(homeSection, true, out var type) ? type : defaults[i]
});
dto.CustomPrefs.Remove(key);
}
var defaultLibraryPrefs = new ItemDisplayPreferences(displayPreferences.UserId, Guid.Empty, displayPreferences.Client)
@ -149,12 +161,12 @@ namespace Jellyfin.Server.Migrations.Routines
foreach (var key in dto.CustomPrefs.Keys.Where(key => key.StartsWith("landing-", StringComparison.Ordinal)))
{
if (!Guid.TryParse(key.AsSpan().Slice("landing-".Length), out var itemId))
if (!Guid.TryParse(key.AsSpan().Slice("landing-".Length), out var landingItemId))
{
continue;
}
var libraryDisplayPreferences = new ItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client)
var libraryDisplayPreferences = new ItemDisplayPreferences(displayPreferences.UserId, landingItemId, displayPreferences.Client)
{
SortBy = dto.SortBy ?? "SortName",
SortOrder = dto.SortOrder,
@ -167,9 +179,15 @@ namespace Jellyfin.Server.Migrations.Routines
libraryDisplayPreferences.ViewType = viewType;
}
dto.CustomPrefs.Remove(key);
dbContext.ItemDisplayPreferences.Add(libraryDisplayPreferences);
}
foreach (var (key, value) in dto.CustomPrefs)
{
dbContext.Add(new CustomItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client, key, value));
}
dbContext.Add(displayPreferences);
}

View File

@ -16,9 +16,10 @@ namespace MediaBrowser.Controller
/// This will create the display preferences if it does not exist, but it will not save automatically.
/// </remarks>
/// <param name="userId">The user's id.</param>
/// <param name="itemId">The item id.</param>
/// <param name="client">The client string.</param>
/// <returns>The associated display preferences.</returns>
DisplayPreferences GetDisplayPreferences(Guid userId, string client);
DisplayPreferences GetDisplayPreferences(Guid userId, Guid itemId, string client);
/// <summary>
/// Gets the default item display preferences for the user and client.
@ -40,6 +41,24 @@ namespace MediaBrowser.Controller
/// <returns>A list of item display preferences.</returns>
IList<ItemDisplayPreferences> ListItemDisplayPreferences(Guid userId, string client);
/// <summary>
/// Gets all of the custom item display preferences for the user and client.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="itemId">The item id.</param>
/// <param name="client">The client string.</param>
/// <returns>The dictionary of custom item display preferences.</returns>
IDictionary<string, string> ListCustomItemDisplayPreferences(Guid userId, Guid itemId, string client);
/// <summary>
/// Sets the custom item display preference for the user and client.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="itemId">The item id.</param>
/// <param name="client">The client id.</param>
/// <param name="customPreferences">A dictionary of custom item display preferences.</param>
void SetCustomItemDisplayPreferences(Guid userId, Guid itemId, string client, Dictionary<string, string> customPreferences);
/// <summary>
/// Saves changes made to the database.
/// </summary>