Merge branch 'master' into tonemap

pull/3442/head
Nyanmisaka 4 years ago committed by GitHub
commit c23d991c95
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -80,7 +80,15 @@ jobs:
pool:
vmImage: 'ubuntu-latest'
variables:
- name: JellyfinVersion
value: 0.0.0
steps:
- script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )"
displayName: Set release version (stable)
condition: startsWith(variables['Build.SourceBranch'], 'refs/tags')
- task: Docker@2
displayName: 'Push Unstable Image'
condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master')
@ -105,7 +113,7 @@ jobs:
containerRegistry: Docker Hub
tags: |
stable-$(Build.BuildNumber)-$(BuildConfiguration)
stable-$(BuildConfiguration)
$(JellyfinVersion)-$(BuildConfiguration)
- job: CollectArtifacts
displayName: 'Collect Artifacts'

@ -11,6 +11,7 @@ using System.Xml;
using Emby.Dlna.Didl;
using Emby.Dlna.Service;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;

@ -122,15 +122,15 @@ namespace Emby.Dlna
var builder = new StringBuilder();
builder.AppendLine("No matching device profile found. The default will need to be used.");
builder.AppendLine(string.Format("DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty));
builder.AppendLine(string.Format("FriendlyName:{0}", profile.FriendlyName ?? string.Empty));
builder.AppendLine(string.Format("Manufacturer:{0}", profile.Manufacturer ?? string.Empty));
builder.AppendLine(string.Format("ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty));
builder.AppendLine(string.Format("ModelDescription:{0}", profile.ModelDescription ?? string.Empty));
builder.AppendLine(string.Format("ModelName:{0}", profile.ModelName ?? string.Empty));
builder.AppendLine(string.Format("ModelNumber:{0}", profile.ModelNumber ?? string.Empty));
builder.AppendLine(string.Format("ModelUrl:{0}", profile.ModelUrl ?? string.Empty));
builder.AppendLine(string.Format("SerialNumber:{0}", profile.SerialNumber ?? string.Empty));
builder.AppendFormat(CultureInfo.InvariantCulture, "DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty).AppendLine();
builder.AppendFormat(CultureInfo.InvariantCulture, "FriendlyName:{0}", profile.FriendlyName ?? string.Empty).AppendLine();
builder.AppendFormat(CultureInfo.InvariantCulture, "Manufacturer:{0}", profile.Manufacturer ?? string.Empty).AppendLine();
builder.AppendFormat(CultureInfo.InvariantCulture, "ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty).AppendLine();
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelDescription:{0}", profile.ModelDescription ?? string.Empty).AppendLine();
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelName:{0}", profile.ModelName ?? string.Empty).AppendLine();
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelNumber:{0}", profile.ModelNumber ?? string.Empty).AppendLine();
builder.AppendFormat(CultureInfo.InvariantCulture, "ModelUrl:{0}", profile.ModelUrl ?? string.Empty).AppendLine();
builder.AppendFormat(CultureInfo.InvariantCulture, "SerialNumber:{0}", profile.SerialNumber ?? string.Empty).AppendLine();
_logger.LogInformation(builder.ToString());
}

@ -554,8 +554,6 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IUserDataRepository, SqliteUserDataRepository>();
serviceCollection.AddSingleton<IUserDataManager, UserDataManager>();
serviceCollection.AddSingleton<IDisplayPreferencesRepository, SqliteDisplayPreferencesRepository>();
serviceCollection.AddSingleton<IItemRepository, SqliteItemRepository>();
serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
@ -650,7 +648,6 @@ namespace Emby.Server.Implementations
_httpServer = Resolve<IHttpServer>();
_httpClient = Resolve<IHttpClient>();
((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
SetStaticProperties();

@ -1,5 +1,4 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@ -7,6 +6,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
@ -22,6 +22,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
@ -45,10 +46,7 @@ namespace Emby.Server.Implementations.Channels
private readonly IFileSystem _fileSystem;
private readonly IJsonSerializer _jsonSerializer;
private readonly IProviderManager _providerManager;
private readonly ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>> _channelItemMediaInfo =
new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>();
private readonly IMemoryCache _memoryCache;
private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1);
/// <summary>
@ -63,6 +61,7 @@ namespace Emby.Server.Implementations.Channels
/// <param name="userDataManager">The user data manager.</param>
/// <param name="jsonSerializer">The JSON serializer.</param>
/// <param name="providerManager">The provider manager.</param>
/// <param name="memoryCache">The memory cache.</param>
public ChannelManager(
IUserManager userManager,
IDtoService dtoService,
@ -72,7 +71,8 @@ namespace Emby.Server.Implementations.Channels
IFileSystem fileSystem,
IUserDataManager userDataManager,
IJsonSerializer jsonSerializer,
IProviderManager providerManager)
IProviderManager providerManager,
IMemoryCache memoryCache)
{
_userManager = userManager;
_dtoService = dtoService;
@ -83,6 +83,7 @@ namespace Emby.Server.Implementations.Channels
_userDataManager = userDataManager;
_jsonSerializer = jsonSerializer;
_providerManager = providerManager;
_memoryCache = memoryCache;
}
internal IChannel[] Channels { get; private set; }
@ -417,20 +418,15 @@ namespace Emby.Server.Implementations.Channels
private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken)
{
if (_channelItemMediaInfo.TryGetValue(id, out Tuple<DateTime, List<MediaSourceInfo>> cachedInfo))
if (_memoryCache.TryGetValue(id, out List<MediaSourceInfo> cachedInfo))
{
if ((DateTime.UtcNow - cachedInfo.Item1).TotalMinutes < 5)
{
return cachedInfo.Item2;
}
return cachedInfo;
}
var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken)
.ConfigureAwait(false);
var list = mediaInfo.ToList();
var item2 = new Tuple<DateTime, List<MediaSourceInfo>>(DateTime.UtcNow, list);
_channelItemMediaInfo.AddOrUpdate(id, item2, (key, oldValue) => item2);
_memoryCache.CreateEntry(id).SetValue(list).SetAbsoluteExpiration(DateTimeOffset.UtcNow.AddMinutes(5));
return list;
}

@ -1,225 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text.Json;
using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
{
/// <summary>
/// Class SQLiteDisplayPreferencesRepository.
/// </summary>
public class SqliteDisplayPreferencesRepository : BaseSqliteRepository, IDisplayPreferencesRepository
{
private readonly IFileSystem _fileSystem;
private readonly JsonSerializerOptions _jsonOptions;
public SqliteDisplayPreferencesRepository(ILogger<SqliteDisplayPreferencesRepository> logger, IApplicationPaths appPaths, IFileSystem fileSystem)
: base(logger)
{
_fileSystem = fileSystem;
_jsonOptions = JsonDefaults.GetOptions();
DbFilePath = Path.Combine(appPaths.DataPath, "displaypreferences.db");
}
/// <summary>
/// Gets the name of the repository.
/// </summary>
/// <value>The name.</value>
public string Name => "SQLite";
public void Initialize()
{
try
{
InitializeInternal();
}
catch (Exception ex)
{
Logger.LogError(ex, "Error loading database file. Will reset and retry.");
_fileSystem.DeleteFile(DbFilePath);
InitializeInternal();
}
}
/// <summary>
/// Opens the connection to the database.
/// </summary>
/// <returns>Task.</returns>
private void InitializeInternal()
{
string[] queries =
{
"create table if not exists userdisplaypreferences (id GUID NOT NULL, userId GUID NOT NULL, client text NOT NULL, data BLOB NOT NULL)",
"create unique index if not exists userdisplaypreferencesindex on userdisplaypreferences (id, userId, client)"
};
using (var connection = GetConnection())
{
connection.RunQueries(queries);
}
}
/// <summary>
/// Save the display preferences associated with an item in the repo.
/// </summary>
/// <param name="displayPreferences">The display preferences.</param>
/// <param name="userId">The user id.</param>
/// <param name="client">The client.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="ArgumentNullException">item</exception>
public void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, CancellationToken cancellationToken)
{
if (displayPreferences == null)
{
throw new ArgumentNullException(nameof(displayPreferences));
}
if (string.IsNullOrEmpty(displayPreferences.Id))
{
throw new ArgumentException("Display preferences has an invalid Id", nameof(displayPreferences));
}
cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection())
{
connection.RunInTransaction(
db => SaveDisplayPreferences(displayPreferences, userId, client, db),
TransactionMode);
}
}
private void SaveDisplayPreferences(DisplayPreferences displayPreferences, Guid userId, string client, IDatabaseConnection connection)
{
var serialized = JsonSerializer.SerializeToUtf8Bytes(displayPreferences, _jsonOptions);
using (var statement = connection.PrepareStatement("replace into userdisplaypreferences (id, userid, client, data) values (@id, @userId, @client, @data)"))
{
statement.TryBind("@id", new Guid(displayPreferences.Id).ToByteArray());
statement.TryBind("@userId", userId.ToByteArray());
statement.TryBind("@client", client);
statement.TryBind("@data", serialized);
statement.MoveNext();
}
}
/// <summary>
/// Save all display preferences associated with a user in the repo.
/// </summary>
/// <param name="displayPreferences">The display preferences.</param>
/// <param name="userId">The user id.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <exception cref="ArgumentNullException">item</exception>
public void SaveAllDisplayPreferences(IEnumerable<DisplayPreferences> displayPreferences, Guid userId, CancellationToken cancellationToken)
{
if (displayPreferences == null)
{
throw new ArgumentNullException(nameof(displayPreferences));
}
cancellationToken.ThrowIfCancellationRequested();
using (var connection = GetConnection())
{
connection.RunInTransaction(
db =>
{
foreach (var displayPreference in displayPreferences)
{
SaveDisplayPreferences(displayPreference, userId, displayPreference.Client, db);
}
},
TransactionMode);
}
}
/// <summary>
/// Gets the display preferences.
/// </summary>
/// <param name="displayPreferencesId">The display preferences id.</param>
/// <param name="userId">The user id.</param>
/// <param name="client">The client.</param>
/// <returns>Task{DisplayPreferences}.</returns>
/// <exception cref="ArgumentNullException">item</exception>
public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, Guid userId, string client)
{
if (string.IsNullOrEmpty(displayPreferencesId))
{
throw new ArgumentNullException(nameof(displayPreferencesId));
}
var guidId = displayPreferencesId.GetMD5();
using (var connection = GetConnection(true))
{
using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where id = @id and userId=@userId and client=@client"))
{
statement.TryBind("@id", guidId.ToByteArray());
statement.TryBind("@userId", userId.ToByteArray());
statement.TryBind("@client", client);
foreach (var row in statement.ExecuteQuery())
{
return Get(row);
}
}
}
return new DisplayPreferences
{
Id = guidId.ToString("N", CultureInfo.InvariantCulture)
};
}
/// <summary>
/// Gets all display preferences for the given user.
/// </summary>
/// <param name="userId">The user id.</param>
/// <returns>Task{DisplayPreferences}.</returns>
/// <exception cref="ArgumentNullException">item</exception>
public IEnumerable<DisplayPreferences> GetAllDisplayPreferences(Guid userId)
{
var list = new List<DisplayPreferences>();
using (var connection = GetConnection(true))
using (var statement = connection.PrepareStatement("select data from userdisplaypreferences where userId=@userId"))
{
statement.TryBind("@userId", userId.ToByteArray());
foreach (var row in statement.ExecuteQuery())
{
list.Add(Get(row));
}
}
return list;
}
private DisplayPreferences Get(IReadOnlyList<IResultSetValue> row)
=> JsonSerializer.Deserialize<DisplayPreferences>(row[0].ToBlob(), _jsonOptions);
public void SaveDisplayPreferences(DisplayPreferences displayPreferences, string userId, string client, CancellationToken cancellationToken)
=> SaveDisplayPreferences(displayPreferences, new Guid(userId), client, cancellationToken);
public DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client)
=> GetDisplayPreferences(displayPreferencesId, new Guid(userId), client);
}
}

@ -9,6 +9,7 @@ using System.Text;
using System.Text.Json;
using System.Threading;
using Emby.Server.Implementations.Playlists;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller;
@ -400,6 +401,8 @@ namespace Emby.Server.Implementations.Data
"OwnerId"
};
private static readonly string _retriveItemColumnsSelectQuery = $"select {string.Join(',', _retriveItemColumns)} from TypedBaseItems where guid = @guid";
private static readonly string[] _mediaStreamSaveColumns =
{
"ItemId",
@ -439,6 +442,12 @@ namespace Emby.Server.Implementations.Data
"ColorTransfer"
};
private static readonly string _mediaStreamSaveColumnsInsertQuery =
$"insert into mediastreams ({string.Join(',', _mediaStreamSaveColumns)}) values ";
private static readonly string _mediaStreamSaveColumnsSelectQuery =
$"select {string.Join(',', _mediaStreamSaveColumns)} from mediastreams where ItemId=@ItemId";
private static readonly string[] _mediaAttachmentSaveColumns =
{
"ItemId",
@ -450,102 +459,15 @@ namespace Emby.Server.Implementations.Data
"MIMEType"
};
private static readonly string _mediaAttachmentInsertPrefix;
private static string GetSaveItemCommandText()
{
var saveColumns = new[]
{
"guid",
"type",
"data",
"Path",
"StartDate",
"EndDate",
"ChannelId",
"IsMovie",
"IsSeries",
"EpisodeTitle",
"IsRepeat",
"CommunityRating",
"CustomRating",
"IndexNumber",
"IsLocked",
"Name",
"OfficialRating",
"MediaType",
"Overview",
"ParentIndexNumber",
"PremiereDate",
"ProductionYear",
"ParentId",
"Genres",
"InheritedParentalRatingValue",
"SortName",
"ForcedSortName",
"RunTimeTicks",
"Size",
"DateCreated",
"DateModified",
"PreferredMetadataLanguage",
"PreferredMetadataCountryCode",
"Width",
"Height",
"DateLastRefreshed",
"DateLastSaved",
"IsInMixedFolder",
"LockedFields",
"Studios",
"Audio",
"ExternalServiceId",
"Tags",
"IsFolder",
"UnratedType",
"TopParentId",
"TrailerTypes",
"CriticRating",
"CleanName",
"PresentationUniqueKey",
"OriginalTitle",
"PrimaryVersionId",
"DateLastMediaAdded",
"Album",
"IsVirtualItem",
"SeriesName",
"UserDataKey",
"SeasonName",
"SeasonId",
"SeriesId",
"ExternalSeriesId",
"Tagline",
"ProviderIds",
"Images",
"ProductionLocations",
"ExtraIds",
"TotalBitrate",
"ExtraType",
"Artists",
"AlbumArtists",
"ExternalId",
"SeriesPresentationUniqueKey",
"ShowId",
"OwnerId"
};
var saveItemCommandCommandText = "replace into TypedBaseItems (" + string.Join(",", saveColumns) + ") values (";
for (var i = 0; i < saveColumns.Length; i++)
{
if (i != 0)
{
saveItemCommandCommandText += ",";
}
private static readonly string _mediaAttachmentSaveColumnsSelectQuery =
$"select {string.Join(',', _mediaAttachmentSaveColumns)} from mediaattachments where ItemId=@ItemId";
saveItemCommandCommandText += "@" + saveColumns[i];
}
private static readonly string _mediaAttachmentInsertPrefix;
return saveItemCommandCommandText + ")";
}
private const string SaveItemCommandText =
@"replace into TypedBaseItems
(guid,type,data,Path,StartDate,EndDate,ChannelId,IsMovie,IsSeries,EpisodeTitle,IsRepeat,CommunityRating,CustomRating,IndexNumber,IsLocked,Name,OfficialRating,MediaType,Overview,ParentIndexNumber,PremiereDate,ProductionYear,ParentId,Genres,InheritedParentalRatingValue,SortName,ForcedSortName,RunTimeTicks,Size,DateCreated,DateModified,PreferredMetadataLanguage,PreferredMetadataCountryCode,Width,Height,DateLastRefreshed,DateLastSaved,IsInMixedFolder,LockedFields,Studios,Audio,ExternalServiceId,Tags,IsFolder,UnratedType,TopParentId,TrailerTypes,CriticRating,CleanName,PresentationUniqueKey,OriginalTitle,PrimaryVersionId,DateLastMediaAdded,Album,IsVirtualItem,SeriesName,UserDataKey,SeasonName,SeasonId,SeriesId,ExternalSeriesId,Tagline,ProviderIds,Images,ProductionLocations,ExtraIds,TotalBitrate,ExtraType,Artists,AlbumArtists,ExternalId,SeriesPresentationUniqueKey,ShowId,OwnerId)
values (@guid,@type,@data,@Path,@StartDate,@EndDate,@ChannelId,@IsMovie,@IsSeries,@EpisodeTitle,@IsRepeat,@CommunityRating,@CustomRating,@IndexNumber,@IsLocked,@Name,@OfficialRating,@MediaType,@Overview,@ParentIndexNumber,@PremiereDate,@ProductionYear,@ParentId,@Genres,@InheritedParentalRatingValue,@SortName,@ForcedSortName,@RunTimeTicks,@Size,@DateCreated,@DateModified,@PreferredMetadataLanguage,@PreferredMetadataCountryCode,@Width,@Height,@DateLastRefreshed,@DateLastSaved,@IsInMixedFolder,@LockedFields,@Studios,@Audio,@ExternalServiceId,@Tags,@IsFolder,@UnratedType,@TopParentId,@TrailerTypes,@CriticRating,@CleanName,@PresentationUniqueKey,@OriginalTitle,@PrimaryVersionId,@DateLastMediaAdded,@Album,@IsVirtualItem,@SeriesName,@UserDataKey,@SeasonName,@SeasonId,@SeriesId,@ExternalSeriesId,@Tagline,@ProviderIds,@Images,@ProductionLocations,@ExtraIds,@TotalBitrate,@ExtraType,@Artists,@AlbumArtists,@ExternalId,@SeriesPresentationUniqueKey,@ShowId,@OwnerId)";
/// <summary>
/// Save a standard item in the repo.
@ -636,7 +558,7 @@ namespace Emby.Server.Implementations.Data
{
var statements = PrepareAll(db, new string[]
{
GetSaveItemCommandText(),
SaveItemCommandText,
"delete from AncestorIds where ItemId=@ItemId"
}).ToList();
@ -1226,7 +1148,7 @@ namespace Emby.Server.Implementations.Data
using (var connection = GetConnection(true))
{
using (var statement = PrepareStatement(connection, "select " + string.Join(",", _retriveItemColumns) + " from TypedBaseItems where guid = @guid"))
using (var statement = PrepareStatement(connection, _retriveItemColumnsSelectQuery))
{
statement.TryBind("@guid", id);
@ -5894,10 +5816,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
throw new ArgumentNullException(nameof(query));
}
var cmdText = "select "
+ string.Join(",", _mediaStreamSaveColumns)
+ " from mediastreams where"
+ " ItemId=@ItemId";
var cmdText = _mediaStreamSaveColumnsSelectQuery;
if (query.Type.HasValue)
{
@ -5976,15 +5895,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
while (startIndex < streams.Count)
{
var insertText = new StringBuilder("insert into mediastreams (");
foreach (var column in _mediaStreamSaveColumns)
{
insertText.Append(column).Append(',');
}
// Remove last comma
insertText.Length--;
insertText.Append(") values ");
var insertText = new StringBuilder(_mediaStreamSaveColumnsInsertQuery);
var endIndex = Math.Min(streams.Count, startIndex + Limit);
@ -6251,10 +6162,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type
throw new ArgumentNullException(nameof(query));
}
var cmdText = "select "
+ string.Join(",", _mediaAttachmentSaveColumns)
+ " from mediaattachments where"
+ " ItemId=@ItemId";
var cmdText = _mediaAttachmentSaveColumnsSelectQuery;
if (query.Index.HasValue)
{

@ -5,8 +5,8 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
@ -17,16 +17,17 @@ using MediaBrowser.Model.Events;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Session;
using Microsoft.Extensions.Caching.Memory;
namespace Emby.Server.Implementations.Devices
{
public class DeviceManager : IDeviceManager
{
private readonly IMemoryCache _memoryCache;
private readonly IJsonSerializer _json;
private readonly IUserManager _userManager;
private readonly IServerConfigurationManager _config;
private readonly IAuthenticationRepository _authRepo;
private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache;
private readonly object _capabilitiesSyncLock = new object();
public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
@ -35,13 +36,14 @@ namespace Emby.Server.Implementations.Devices
IAuthenticationRepository authRepo,
IJsonSerializer json,
IUserManager userManager,
IServerConfigurationManager config)
IServerConfigurationManager config,
IMemoryCache memoryCache)
{
_json = json;
_userManager = userManager;
_config = config;
_memoryCache = memoryCache;
_authRepo = authRepo;
_capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase);
}
public void SaveCapabilities(string deviceId, ClientCapabilities capabilities)
@ -51,8 +53,7 @@ namespace Emby.Server.Implementations.Devices
lock (_capabilitiesSyncLock)
{
_capabilitiesCache[deviceId] = capabilities;
_memoryCache.CreateEntry(deviceId).SetValue(capabilities);
_json.SerializeToFile(capabilities, path);
}
}
@ -71,13 +72,13 @@ namespace Emby.Server.Implementations.Devices
public ClientCapabilities GetCapabilities(string id)
{
lock (_capabilitiesSyncLock)
if (_memoryCache.TryGetValue(id, out ClientCapabilities result))
{
if (_capabilitiesCache.TryGetValue(id, out var result))
{
return result;
}
return result;
}
lock (_capabilitiesSyncLock)
{
var path = Path.Combine(GetDevicePath(id), "capabilities.json");
try
{

@ -25,7 +25,7 @@
<ItemGroup>
<PackageReference Include="IPNetwork2" Version="2.5.211" />
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.0-pre1" />
<PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" />
<PackageReference Include="Microsoft.AspNetCore.Hosting" Version="2.2.7" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Hosting.Server.Abstractions" Version="2.2.0" />
@ -54,7 +54,7 @@
<TargetFramework>netstandard2.1</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'" >true</TreatWarningsAsErrors>
<TreatWarningsAsErrors Condition=" '$(Configuration)' == 'Release'">true</TreatWarningsAsErrors>
</PropertyGroup>
<!-- Code Analyzers-->

@ -3,7 +3,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using Emby.Server.Implementations.Images;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;

@ -1,7 +1,7 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using Emby.Server.Implementations.Images;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;

@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;

@ -1,7 +1,6 @@
#pragma warning disable CS1591
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
@ -46,11 +45,11 @@ using MediaBrowser.Model.Net;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.MediaInfo;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Genre = MediaBrowser.Controller.Entities.Genre;
using Person = MediaBrowser.Controller.Entities.Person;
using SortOrder = MediaBrowser.Model.Entities.SortOrder;
using VideoResolver = Emby.Naming.Video.VideoResolver;
namespace Emby.Server.Implementations.Library
@ -63,6 +62,7 @@ namespace Emby.Server.Implementations.Library
private const string ShortcutFileExtension = ".mblink";
private readonly ILogger<LibraryManager> _logger;
private readonly IMemoryCache _memoryCache;
private readonly ITaskManager _taskManager;
private readonly IUserManager _userManager;
private readonly IUserDataManager _userDataRepository;
@ -74,7 +74,6 @@ namespace Emby.Server.Implementations.Library
private readonly IMediaEncoder _mediaEncoder;
private readonly IFileSystem _fileSystem;
private readonly IItemRepository _itemRepository;
private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache;
private readonly IImageProcessor _imageProcessor;
/// <summary>
@ -112,6 +111,7 @@ namespace Emby.Server.Implementations.Library
/// <param name="mediaEncoder">The media encoder.</param>
/// <param name="itemRepository">The item repository.</param>
/// <param name="imageProcessor">The image processor.</param>
/// <param name="memoryCache">The memory cache.</param>
public LibraryManager(
IServerApplicationHost appHost,
ILogger<LibraryManager> logger,
@ -125,7 +125,8 @@ namespace Emby.Server.Implementations.Library
Lazy<IUserViewManager> userviewManagerFactory,
IMediaEncoder mediaEncoder,
IItemRepository itemRepository,
IImageProcessor imageProcessor)
IImageProcessor imageProcessor,
IMemoryCache memoryCache)
{
_appHost = appHost;
_logger = logger;
@ -140,8 +141,7 @@ namespace Emby.Server.Implementations.Library
_mediaEncoder = mediaEncoder;
_itemRepository = itemRepository;
_imageProcessor = imageProcessor;
_libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>();
_memoryCache = memoryCache;
_configurationManager.ConfigurationUpdated += ConfigurationUpdated;
@ -299,7 +299,7 @@ namespace Emby.Server.Implementations.Library
}
}
_libraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; });
_memoryCache.CreateEntry(item.Id).SetValue(item);
}
public void DeleteItem(BaseItem item, DeleteOptions options)
@ -447,7 +447,7 @@ namespace Emby.Server.Implementations.Library
_itemRepository.DeleteItem(child.Id);
}
_libraryItemsCache.TryRemove(item.Id, out BaseItem removed);
_memoryCache.Remove(item.Id);
ReportItemRemoved(item, parent);
}
@ -1248,7 +1248,7 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentException("Guid can't be empty", nameof(id));
}
if (_libraryItemsCache.TryGetValue(id, out BaseItem item))
if (_memoryCache.TryGetValue(id, out BaseItem item))
{
return item;
}
@ -1591,7 +1591,6 @@ namespace Emby.Server.Implementations.Library
public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user)
{
var tasks = IntroProviders
.OrderBy(i => i.GetType().Name.Contains("Default", StringComparison.OrdinalIgnoreCase) ? 1 : 0)
.Take(1)
.Select(i => GetIntros(i, item, user));

@ -4,12 +4,12 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;

@ -4,12 +4,12 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Search;
using Microsoft.Extensions.Logging;

@ -12,6 +12,7 @@ using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Emby.Server.Implementations.Library;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;

@ -101,8 +101,8 @@
"TaskCleanTranscode": "Lösche Transkodier Pfad",
"TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.",
"TaskUpdatePlugins": "Update Plugins",
"TaskRefreshPeopleDescription": "Erneuert Metadaten für Schausteller und Regisseure in deinen Bibliotheken.",
"TaskRefreshPeople": "Erneuere Schausteller",
"TaskRefreshPeopleDescription": "Erneuert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.",
"TaskRefreshPeople": "Erneuere Schauspieler",
"TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.",
"TaskCleanLogs": "Lösche Log Pfad",
"TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.",

@ -165,7 +165,7 @@ namespace Emby.Server.Implementations.Networking
(octet[0] == 127) || // RFC1122
(octet[0] == 169 && octet[1] == 254)) // RFC3927
{
return false;
return true;
}
if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint))

@ -0,0 +1,150 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Jellyfin.Data.Enums;
namespace Jellyfin.Data.Entities
{
/// <summary>
/// An entity representing a user's display preferences.
/// </summary>
public class DisplayPreferences
{
/// <summary>
/// Initializes a new instance of the <see cref="DisplayPreferences"/> class.
/// </summary>
/// <param name="userId">The user's id.</param>
/// <param name="client">The client string.</param>
public DisplayPreferences(Guid userId, string client)
{
UserId = userId;
Client = client;
ShowSidebar = false;
ShowBackdrop = true;
SkipForwardLength = 30000;
SkipBackwardLength = 10000;
ScrollDirection = ScrollDirection.Horizontal;
ChromecastVersion = ChromecastVersion.Stable;
DashboardTheme = string.Empty;
TvHome = string.Empty;
HomeSections = new HashSet<HomeSection>();
}
/// <summary>
/// Initializes a new instance of the <see cref="DisplayPreferences"/> class.
/// </summary>
protected DisplayPreferences()
{
}
/// <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 client string.
/// </summary>
/// <remarks>
/// Required. Max Length = 32.
/// </remarks>
[Required]
[MaxLength(32)]
[StringLength(32)]
public string Client { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to show the sidebar.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public bool ShowSidebar { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to show the backdrop.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public bool ShowBackdrop { get; set; }
/// <summary>
/// Gets or sets the scroll direction.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public ScrollDirection ScrollDirection { get; set; }
/// <summary>
/// Gets or sets what the view should be indexed by.
/// </summary>
public IndexingKind? IndexBy { get; set; }
/// <summary>
/// Gets or sets the length of time to skip forwards, in milliseconds.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public int SkipForwardLength { get; set; }
/// <summary>
/// Gets or sets the length of time to skip backwards, in milliseconds.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public int SkipBackwardLength { get; set; }
/// <summary>
/// Gets or sets the Chromecast Version.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public ChromecastVersion ChromecastVersion { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the next video info overlay should be shown.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public bool EnableNextVideoInfoOverlay { get; set; }
/// <summary>
/// Gets or sets the dashboard theme.
/// </summary>
[MaxLength(32)]
[StringLength(32)]
public string DashboardTheme { get; set; }
/// <summary>
/// Gets or sets the tv home screen.
/// </summary>
[MaxLength(32)]
[StringLength(32)]
public string TvHome { get; set; }
/// <summary>
/// Gets or sets the home sections.
/// </summary>
public virtual ICollection<HomeSection> HomeSections { get; protected set; }
}
}

@ -0,0 +1,46 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Jellyfin.Data.Enums;
namespace Jellyfin.Data.Entities
{
/// <summary>
/// An entity representing a section on the user's home page.
/// </summary>
public class HomeSection
{
/// <summary>
/// Gets or sets the id.
/// </summary>
/// <remarks>
/// Identity. Required.
/// </remarks>
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; protected set; }
/// <summary>
/// Gets or sets the Id of the associated display preferences.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public int DisplayPreferencesId { get; set; }
/// <summary>
/// Gets or sets the order.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public int Order { get; set; }
/// <summary>
/// Gets or sets the type.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public HomeSectionType Type { get; set; }
}
}

@ -0,0 +1,120 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Jellyfin.Data.Enums;
namespace Jellyfin.Data.Entities
{
public class ItemDisplayPreferences
{
/// <summary>
/// Initializes a new instance of the <see cref="ItemDisplayPreferences"/> class.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="itemId">The item id.</param>
/// <param name="client">The client.</param>
public ItemDisplayPreferences(Guid userId, Guid itemId, string client)
{
UserId = userId;
ItemId = itemId;
Client = client;
SortBy = "SortName";
ViewType = ViewType.Poster;
SortOrder = SortOrder.Ascending;
RememberSorting = false;
RememberIndexing = false;
}
/// <summary>
/// Initializes a new instance of the <see cref="ItemDisplayPreferences"/> class.
/// </summary>
protected ItemDisplayPreferences()
{
}
/// <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 view type.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public ViewType ViewType { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the indexing should be remembered.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public bool RememberIndexing { get; set; }
/// <summary>
/// Gets or sets what the view should be indexed by.
/// </summary>
public IndexingKind? IndexBy { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the sorting type should be remembered.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public bool RememberSorting { get; set; }
/// <summary>
/// Gets or sets what the view should be sorted by.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[Required]
[MaxLength(64)]
[StringLength(64)]
public string SortBy { get; set; }
/// <summary>
/// Gets or sets the sort order.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
public SortOrder SortOrder { get; set; }
}
}

@ -48,6 +48,7 @@ namespace Jellyfin.Data.Entities
PasswordResetProviderId = passwordResetProviderId;
AccessSchedules = new HashSet<AccessSchedule>();
ItemDisplayPreferences = new HashSet<ItemDisplayPreferences>();
// Groups = new HashSet<Group>();
Permissions = new HashSet<Permission>();
Preferences = new HashSet<Preference>();
@ -327,6 +328,15 @@ namespace Jellyfin.Data.Entities
// [ForeignKey("UserId")]
public virtual ImageInfo ProfileImage { get; set; }
/// <summary>
/// Gets or sets the user's display preferences.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[Required]
public virtual DisplayPreferences DisplayPreferences { get; set; }
[Required]
public SyncPlayAccess SyncPlayAccess { get; set; }
@ -349,6 +359,11 @@ namespace Jellyfin.Data.Entities
/// </summary>
public virtual ICollection<AccessSchedule> AccessSchedules { get; protected set; }
/// <summary>
/// Gets or sets the list of item display preferences.
/// </summary>
public virtual ICollection<ItemDisplayPreferences> ItemDisplayPreferences { get; protected set; }
/*
/// <summary>
/// Gets or sets the list of groups this user is a member of.

@ -0,0 +1,18 @@
namespace Jellyfin.Data.Enums
{
/// <summary>
/// An enum representing the version of Chromecast to be used by clients.
/// </summary>
public enum ChromecastVersion
{
/// <summary>
/// Stable Chromecast version.
/// </summary>
Stable = 0,
/// <summary>
/// Unstable Chromecast version.
/// </summary>
Unstable = 1
}
}

@ -0,0 +1,53 @@
namespace Jellyfin.Data.Enums
{
/// <summary>
/// An enum representing the different options for the home screen sections.
/// </summary>
public enum HomeSectionType
{
/// <summary>
/// None.
/// </summary>
None = 0,
/// <summary>
/// My Media.
/// </summary>
SmallLibraryTiles = 1,
/// <summary>
/// My Media Small.
/// </summary>
LibraryButtons = 2,
/// <summary>
/// Active Recordings.
/// </summary>
ActiveRecordings = 3,
/// <summary>
/// Continue Watching.
/// </summary>
Resume = 4,
/// <summary>
/// Continue Listening.
/// </summary>
ResumeAudio = 5,
/// <summary>
/// Latest Media.
/// </summary>
LatestMedia = 6,
/// <summary>
/// Next Up.
/// </summary>
NextUp = 7,
/// <summary>
/// Live TV.
/// </summary>
LiveTv = 8
}
}

@ -0,0 +1,20 @@
namespace Jellyfin.Data.Enums
{
public enum IndexingKind
{
/// <summary>
/// Index by the premiere date.
/// </summary>
PremiereDate = 0,
/// <summary>
/// Index by the production year.
/// </summary>
ProductionYear = 1,
/// <summary>
/// Index by the community rating.
/// </summary>
CommunityRating = 2
}
}

@ -0,0 +1,18 @@
namespace Jellyfin.Data.Enums
{
/// <summary>
/// An enum representing the axis that should be scrolled.
/// </summary>
public enum ScrollDirection
{
/// <summary>
/// Horizontal scrolling direction.
/// </summary>
Horizontal = 0,
/// <summary>
/// Vertical scrolling direction.
/// </summary>
Vertical = 1
}
}

@ -0,0 +1,18 @@
namespace Jellyfin.Data.Enums
{
/// <summary>
/// An enum representing the sorting order.
/// </summary>
public enum SortOrder
{
/// <summary>
/// Sort in increasing order.
/// </summary>
Ascending = 0,
/// <summary>
/// Sort in decreasing order.
/// </summary>
Descending = 1
}
}

@ -0,0 +1,38 @@
namespace Jellyfin.Data.Enums
{
/// <summary>
/// An enum representing the type of view for a library or collection.
/// </summary>
public enum ViewType
{
/// <summary>
/// Shows banners.
/// </summary>
Banner = 0,
/// <summary>
/// Shows a list of content.
/// </summary>
List = 1,
/// <summary>
/// Shows poster artwork.
/// </summary>
Poster = 2,
/// <summary>
/// Shows poster artwork with a card containing the name and year.
/// </summary>
PosterCard = 3,
/// <summary>
/// Shows a thumbnail.
/// </summary>
Thumb = 4,
/// <summary>
/// Shows a thumbnail with a card containing the name and year.
/// </summary>
ThumbCard = 5
}
}

@ -18,11 +18,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BlurHashSharp" Version="1.0.1" />
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.0.0" />
<PackageReference Include="SkiaSharp" Version="1.68.3" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="1.68.3" />
<PackageReference Include="Jellyfin.SkiaSharp.NativeAssets.LinuxArm" Version="1.68.1" />
<PackageReference Include="BlurHashSharp" Version="1.1.0" />
<PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.1.0" />
<PackageReference Include="SkiaSharp" Version="2.80.1" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.80.1" />
</ItemGroup>
<ItemGroup>

@ -199,7 +199,8 @@ namespace Jellyfin.Drawing.Skia
throw new ArgumentNullException(nameof(path));
}
return BlurHashEncoder.Encode(xComp, yComp, path);
// Any larger than 128x128 is too slow and there's no visually discernible difference
return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128);
}
private static bool HasDiacritics(string text)
@ -394,6 +395,56 @@ namespace Jellyfin.Drawing.Skia
return rotated;
}
/// <summary>
/// Resizes an image on the CPU, by utilizing a surface and canvas.
///
/// The convolutional matrix kernel used in this resize function gives a (light) sharpening effect.
/// This technique is similar to effect that can be created using for example the [Convolution matrix filter in GIMP](https://docs.gimp.org/2.10/en/gimp-filter-convolution-matrix.html).
/// </summary>
/// <param name="source">The source bitmap.</param>
/// <param name="targetInfo">This specifies the target size and other information required to create the surface.</param>
/// <param name="isAntialias">This enables anti-aliasing on the SKPaint instance.</param>
/// <param name="isDither">This enables dithering on the SKPaint instance.</param>
/// <returns>The resized image.</returns>
internal static SKImage ResizeImage(SKBitmap source, SKImageInfo targetInfo, bool isAntialias = false, bool isDither = false)
{
using var surface = SKSurface.Create(targetInfo);
using var canvas = surface.Canvas;
using var paint = new SKPaint
{
FilterQuality = SKFilterQuality.High,
IsAntialias = isAntialias,
IsDither = isDither
};
var kernel = new float[9]
{
0, -.1f, 0,
-.1f, 1.4f, -.1f,
0, -.1f, 0,
};
var kernelSize = new SKSizeI(3, 3);
var kernelOffset = new SKPointI(1, 1);
paint.ImageFilter = SKImageFilter.CreateMatrixConvolution(
kernelSize,
kernel,
1f,
0f,
kernelOffset,
SKShaderTileMode.Clamp,
false);
canvas.DrawBitmap(
source,
SKRect.Create(0, 0, source.Width, source.Height),
SKRect.Create(0, 0, targetInfo.Width, targetInfo.Height),
paint);
return surface.Snapshot();
}
/// <inheritdoc/>
public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat)
{
@ -435,9 +486,9 @@ namespace Jellyfin.Drawing.Skia
var width = newImageSize.Width;
var height = newImageSize.Height;
using var resizedBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType);
// scale image
bitmap.ScalePixels(resizedBitmap, SKFilterQuality.High);
// scale image (the FromImage creates a copy)
var imageInfo = new SKImageInfo(width, height, bitmap.ColorType, bitmap.AlphaType, bitmap.ColorSpace);
using var resizedBitmap = SKBitmap.FromImage(ResizeImage(bitmap, imageInfo));
// If all we're doing is resizing then we can stop now
if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator)
@ -445,7 +496,7 @@ namespace Jellyfin.Drawing.Skia
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
using var outputStream = new SKFileWStream(outputPath);
using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels());
pixmap.Encode(outputStream, skiaOutputFormat, quality);
resizedBitmap.Encode(outputStream, skiaOutputFormat, quality);
return outputPath;
}

@ -115,15 +115,13 @@ namespace Jellyfin.Drawing.Skia
// resize to the same aspect as the original
int iWidth = Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height);
using var resizeBitmap = new SKBitmap(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType);
currentBitmap.ScalePixels(resizeBitmap, SKFilterQuality.High);
using var resizedImage = SkiaEncoder.ResizeImage(bitmap, new SKImageInfo(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace));
// crop image
int ix = Math.Abs((iWidth - iSlice) / 2);
using var image = SKImage.FromBitmap(resizeBitmap);
using var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight));
using var subset = resizedImage.Subset(SKRectI.Create(ix, 0, iSlice, iHeight));
// draw image onto canvas
canvas.DrawImage(subset ?? image, iSlice * i, 0);
canvas.DrawImage(subset ?? resizedImage, iSlice * i, 0);
}
return bitmap;
@ -177,9 +175,9 @@ namespace Jellyfin.Drawing.Skia
continue;
}
using var resizedBitmap = new SKBitmap(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType);
// scale image
currentBitmap.ScalePixels(resizedBitmap, SKFilterQuality.High);
// Scale image. The FromBitmap creates a copy
var imageInfo = new SKImageInfo(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType, currentBitmap.ColorSpace);
using var resizedBitmap = SKBitmap.FromImage(SkiaEncoder.ResizeImage(bitmap, imageInfo));
// draw this image into the strip at the next position
var xPos = x * cellWidth;

@ -28,8 +28,12 @@ namespace Jellyfin.Server.Implementations
public virtual DbSet<ActivityLog> ActivityLogs { get; set; }
public virtual DbSet<DisplayPreferences> DisplayPreferences { get; set; }
public virtual DbSet<ImageInfo> ImageInfos { get; set; }
public virtual DbSet<ItemDisplayPreferences> ItemDisplayPreferences { get; set; }
public virtual DbSet<Permission> Permissions { get; set; }
public virtual DbSet<Preference> Preferences { get; set; }

@ -1,4 +1,6 @@
using System;
using System.IO;
using MediaBrowser.Common.Configuration;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
@ -10,15 +12,20 @@ namespace Jellyfin.Server.Implementations
public class JellyfinDbProvider
{
private readonly IServiceProvider _serviceProvider;
private readonly IApplicationPaths _appPaths;
/// <summary>
/// Initializes a new instance of the <see cref="JellyfinDbProvider"/> class.
/// </summary>
/// <param name="serviceProvider">The application's service provider.</param>
public JellyfinDbProvider(IServiceProvider serviceProvider)
/// <param name="appPaths">The application paths.</param>
public JellyfinDbProvider(IServiceProvider serviceProvider, IApplicationPaths appPaths)
{
_serviceProvider = serviceProvider;
serviceProvider.GetRequiredService<JellyfinDb>().Database.Migrate();
_appPaths = appPaths;
using var jellyfinDb = CreateContext();
jellyfinDb.Database.Migrate();
}
/// <summary>
@ -27,7 +34,8 @@ namespace Jellyfin.Server.Implementations
/// <returns>The newly created context.</returns>
public JellyfinDb CreateContext()
{
return _serviceProvider.GetRequiredService<JellyfinDb>();
var contextOptions = new DbContextOptionsBuilder<JellyfinDb>().UseSqlite($"Filename={Path.Combine(_appPaths.DataPath, "jellyfin.db")}");
return ActivatorUtilities.CreateInstance<JellyfinDb>(_serviceProvider, contextOptions.Options);
}
}
}

@ -0,0 +1,459 @@
#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("20200728005145_AddDisplayPreferences")]
partial class AddDisplayPreferences
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("jellyfin")
.HasAnnotation("ProductVersion", "3.1.6");
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")
.HasColumnType("TEXT")
.HasMaxLength(256);
b.Property<int>("LogSeverity")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(512);
b.Property<string>("Overview")
.HasColumnType("TEXT")
.HasMaxLength(512);
b.Property<uint>("RowVersion")
.IsConcurrencyToken()
.HasColumnType("INTEGER");
b.Property<string>("ShortOverview")
.HasColumnType("TEXT")
.HasMaxLength(512);
b.Property<string>("Type")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(256);
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("ActivityLogs");
});
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()
.HasColumnType("TEXT")
.HasMaxLength(32);
b.Property<string>("DashboardTheme")
.HasColumnType("TEXT")
.HasMaxLength(32);
b.Property<bool>("EnableNextVideoInfoOverlay")
.HasColumnType("INTEGER");
b.Property<int?>("IndexBy")
.HasColumnType("INTEGER");
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")
.HasColumnType("TEXT")
.HasMaxLength(32);
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId")
.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()
.HasColumnType("TEXT")
.HasMaxLength(512);
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()
.HasColumnType("TEXT")
.HasMaxLength(32);
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()
.HasColumnType("TEXT")
.HasMaxLength(64);
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()
.HasColumnType("TEXT")
.HasMaxLength(65535);
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")
.HasColumnType("TEXT")
.HasMaxLength(255);
b.Property<string>("AuthenticationProviderId")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(255);
b.Property<bool>("DisplayCollectionsView")
.HasColumnType("INTEGER");
b.Property<bool>("DisplayMissingEpisodes")
.HasColumnType("INTEGER");
b.Property<string>("EasyPassword")
.HasColumnType("TEXT")
.HasMaxLength(65535);
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?>("MaxParentalAgeRating")
.HasColumnType("INTEGER");
b.Property<bool>("MustUpdatePassword")
.HasColumnType("INTEGER");
b.Property<string>("Password")
.HasColumnType("TEXT")
.HasMaxLength(65535);
b.Property<string>("PasswordResetProviderId")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(255);
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")
.HasColumnType("TEXT")
.HasMaxLength(255);
b.Property<int>("SubtitleMode")
.HasColumnType("INTEGER");
b.Property<int>("SyncPlayAccess")
.HasColumnType("INTEGER");
b.Property<string>("Username")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(255);
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");
});
#pragma warning restore 612, 618
}
}
}

@ -0,0 +1,132 @@
#pragma warning disable CS1591
#pragma warning disable SA1601
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Jellyfin.Server.Implementations.Migrations
{
public partial class AddDisplayPreferences : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "DisplayPreferences",
schema: "jellyfin",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<Guid>(nullable: false),
Client = table.Column<string>(maxLength: 32, nullable: false),
ShowSidebar = table.Column<bool>(nullable: false),
ShowBackdrop = table.Column<bool>(nullable: false),
ScrollDirection = table.Column<int>(nullable: false),
IndexBy = table.Column<int>(nullable: true),
SkipForwardLength = table.Column<int>(nullable: false),
SkipBackwardLength = table.Column<int>(nullable: false),
ChromecastVersion = table.Column<int>(nullable: false),
EnableNextVideoInfoOverlay = table.Column<bool>(nullable: false),
DashboardTheme = table.Column<string>(maxLength: 32, nullable: true),
TvHome = table.Column<string>(maxLength: 32, nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_DisplayPreferences", x => x.Id);
table.ForeignKey(
name: "FK_DisplayPreferences_Users_UserId",
column: x => x.UserId,
principalSchema: "jellyfin",
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ItemDisplayPreferences",
schema: "jellyfin",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<Guid>(nullable: false),
ItemId = table.Column<Guid>(nullable: false),
Client = table.Column<string>(maxLength: 32, nullable: false),
ViewType = table.Column<int>(nullable: false),
RememberIndexing = table.Column<bool>(nullable: false),
IndexBy = table.Column<int>(nullable: true),
RememberSorting = table.Column<bool>(nullable: false),
SortBy = table.Column<string>(maxLength: 64, nullable: false),
SortOrder = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ItemDisplayPreferences", x => x.Id);
table.ForeignKey(
name: "FK_ItemDisplayPreferences_Users_UserId",
column: x => x.UserId,
principalSchema: "jellyfin",
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "HomeSection",
schema: "jellyfin",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
DisplayPreferencesId = table.Column<int>(nullable: false),
Order = table.Column<int>(nullable: false),
Type = table.Column<int>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_HomeSection", x => x.Id);
table.ForeignKey(
name: "FK_HomeSection_DisplayPreferences_DisplayPreferencesId",
column: x => x.DisplayPreferencesId,
principalSchema: "jellyfin",
principalTable: "DisplayPreferences",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_DisplayPreferences_UserId",
schema: "jellyfin",
table: "DisplayPreferences",
column: "UserId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_HomeSection_DisplayPreferencesId",
schema: "jellyfin",
table: "HomeSection",
column: "DisplayPreferencesId");
migrationBuilder.CreateIndex(
name: "IX_ItemDisplayPreferences_UserId",
schema: "jellyfin",
table: "ItemDisplayPreferences",
column: "UserId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "HomeSection",
schema: "jellyfin");
migrationBuilder.DropTable(
name: "ItemDisplayPreferences",
schema: "jellyfin");
migrationBuilder.DropTable(
name: "DisplayPreferences",
schema: "jellyfin");
}
}
}

@ -15,7 +15,7 @@ namespace Jellyfin.Server.Implementations.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("jellyfin")
.HasAnnotation("ProductVersion", "3.1.4");
.HasAnnotation("ProductVersion", "3.1.6");
modelBuilder.Entity("Jellyfin.Data.Entities.AccessSchedule", b =>
{
@ -88,6 +88,82 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("ActivityLogs");
});
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()
.HasColumnType("TEXT")
.HasMaxLength(32);
b.Property<string>("DashboardTheme")
.HasColumnType("TEXT")
.HasMaxLength(32);
b.Property<bool>("EnableNextVideoInfoOverlay")
.HasColumnType("INTEGER");
b.Property<int?>("IndexBy")
.HasColumnType("INTEGER");
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")
.HasColumnType("TEXT")
.HasMaxLength(32);
b.Property<Guid>("UserId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("UserId")
.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")
@ -113,6 +189,50 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("ImageInfos");
});
modelBuilder.Entity("Jellyfin.Data.Entities.ItemDisplayPreferences", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Client")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(32);
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()
.HasColumnType("TEXT")
.HasMaxLength(64);
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")
@ -282,6 +402,24 @@ namespace Jellyfin.Server.Implementations.Migrations
.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)
@ -289,6 +427,15 @@ namespace Jellyfin.Server.Implementations.Migrations
.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)

@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common;
@ -11,7 +12,6 @@ using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Users;
namespace Jellyfin.Server.Implementations.Users
@ -23,7 +23,6 @@ namespace Jellyfin.Server.Implementations.Users
{
private const string BaseResetFileName = "passwordreset";
private readonly IJsonSerializer _jsonSerializer;
private readonly IApplicationHost _appHost;
private readonly string _passwordResetFileBase;
@ -33,16 +32,11 @@ namespace Jellyfin.Server.Implementations.Users
/// Initializes a new instance of the <see cref="DefaultPasswordResetProvider"/> class.
/// </summary>
/// <param name="configurationManager">The configuration manager.</param>
/// <param name="jsonSerializer">The JSON serializer.</param>
/// <param name="appHost">The application host.</param>
public DefaultPasswordResetProvider(
IServerConfigurationManager configurationManager,
IJsonSerializer jsonSerializer,
IApplicationHost appHost)
public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IApplicationHost appHost)
{
_passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath;
_passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName);
_jsonSerializer = jsonSerializer;
_appHost = appHost;
// TODO: Remove the circular dependency on UserManager
}
@ -63,7 +57,7 @@ namespace Jellyfin.Server.Implementations.Users
SerializablePasswordReset spr;
await using (var str = File.OpenRead(resetFile))
{
spr = await _jsonSerializer.DeserializeFromStreamAsync<SerializablePasswordReset>(str).ConfigureAwait(false);
spr = await JsonSerializer.DeserializeAsync<SerializablePasswordReset>(str).ConfigureAwait(false);
}
if (spr.ExpirationDate < DateTime.UtcNow)
@ -119,7 +113,7 @@ namespace Jellyfin.Server.Implementations.Users
await using (FileStream fileStream = File.OpenWrite(filePath))
{
_jsonSerializer.SerializeToStream(spr, fileStream);
await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false);
await fileStream.FlushAsync().ConfigureAwait(false);
}

@ -0,0 +1,88 @@
#pragma warning disable CA1307
using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller;
using Microsoft.EntityFrameworkCore;
namespace Jellyfin.Server.Implementations.Users
{
/// <summary>
/// Manages the storage and retrieval of display preferences through Entity Framework.
/// </summary>
public class DisplayPreferencesManager : IDisplayPreferencesManager
{
private readonly JellyfinDbProvider _dbProvider;
/// <summary>
/// Initializes a new instance of the <see cref="DisplayPreferencesManager"/> class.
/// </summary>
/// <param name="dbProvider">The Jellyfin db provider.</param>
public DisplayPreferencesManager(JellyfinDbProvider dbProvider)
{
_dbProvider = dbProvider;
}
/// <inheritdoc />
public DisplayPreferences GetDisplayPreferences(Guid userId, string client)
{
using var dbContext = _dbProvider.CreateContext();
var prefs = dbContext.DisplayPreferences
.Include(pref => pref.HomeSections)
.FirstOrDefault(pref =>
pref.UserId == userId && string.Equals(pref.Client, client));
if (prefs == null)
{
prefs = new DisplayPreferences(userId, client);
dbContext.DisplayPreferences.Add(prefs);
}
return prefs;
}
/// <inheritdoc />
public ItemDisplayPreferences GetItemDisplayPreferences(Guid userId, Guid itemId, string client)
{
using var dbContext = _dbProvider.CreateContext();
var prefs = dbContext.ItemDisplayPreferences
.FirstOrDefault(pref => pref.UserId == userId && pref.ItemId == itemId && string.Equals(pref.Client, client));
if (prefs == null)
{
prefs = new ItemDisplayPreferences(userId, Guid.Empty, client);
dbContext.ItemDisplayPreferences.Add(prefs);
}
return prefs;
}
/// <inheritdoc />
public IList<ItemDisplayPreferences> ListItemDisplayPreferences(Guid userId, string client)
{
using var dbContext = _dbProvider.CreateContext();
return dbContext.ItemDisplayPreferences
.Where(prefs => prefs.UserId == userId && prefs.ItemId != Guid.Empty && string.Equals(prefs.Client, client))
.ToList();
}
/// <inheritdoc />
public void SaveChanges(DisplayPreferences preferences)
{
using var dbContext = _dbProvider.CreateContext();
dbContext.Update(preferences);
dbContext.SaveChanges();
}
/// <inheritdoc />
public void SaveChanges(ItemDisplayPreferences preferences)
{
using var dbContext = _dbProvider.CreateContext();
dbContext.Update(preferences);
dbContext.SaveChanges();
}
}
}

@ -600,18 +600,13 @@ namespace Jellyfin.Server.Implementations.Users
}
var defaultName = Environment.UserName;
if (string.IsNullOrWhiteSpace(defaultName))
if (string.IsNullOrWhiteSpace(defaultName) || !IsValidUsername(defaultName))
{
defaultName = "MyJellyfinUser";
}
_logger.LogWarning("No users, creating one with username {UserName}", defaultName);
if (!IsValidUsername(defaultName))
{
throw new ArgumentException("Provided username is not valid!", defaultName);
}
var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false);
newUser.SetPermission(PermissionKind.IsAdministrator, true);
newUser.SetPermission(PermissionKind.EnableContentDeletion, true);

@ -64,16 +64,18 @@ namespace Jellyfin.Server
Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}.");
}
// TODO: Set up scoping and use AddDbContextPool
serviceCollection.AddDbContext<JellyfinDb>(
options => options
.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
ServiceLifetime.Transient);
// TODO: Set up scoping and use AddDbContextPool,
// can't register as Transient since tracking transient in GC is funky
// serviceCollection.AddDbContext<JellyfinDb>(
// options => options
// .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
// ServiceLifetime.Transient);
serviceCollection.AddSingleton<JellyfinDbProvider>();
serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
serviceCollection.AddSingleton<IUserManager, UserManager>();
serviceCollection.AddSingleton<IDisplayPreferencesManager, DisplayPreferencesManager>();
base.RegisterServices(serviceCollection);
}

@ -22,7 +22,8 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.RemoveDuplicateExtras),
typeof(Routines.AddDefaultPluginRepository),
typeof(Routines.MigrateUserDb),
typeof(Routines.ReaddDefaultPluginRepository)
typeof(Routines.ReaddDefaultPluginRepository),
typeof(Routines.MigrateDisplayPreferencesDb)
};
/// <summary>

@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Server.Implementations;
using MediaBrowser.Controller;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
namespace Jellyfin.Server.Migrations.Routines
{
/// <summary>
/// The migration routine for migrating the display preferences database to EF Core.
/// </summary>
public class MigrateDisplayPreferencesDb : IMigrationRoutine
{
private const string DbFilename = "displaypreferences.db";
private readonly ILogger<MigrateDisplayPreferencesDb> _logger;
private readonly IServerApplicationPaths _paths;
private readonly JellyfinDbProvider _provider;
private readonly JsonSerializerOptions _jsonOptions;
/// <summary>
/// Initializes a new instance of the <see cref="MigrateDisplayPreferencesDb"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="paths">The server application paths.</param>
/// <param name="provider">The database provider.</param>
public MigrateDisplayPreferencesDb(ILogger<MigrateDisplayPreferencesDb> logger, IServerApplicationPaths paths, JellyfinDbProvider provider)
{
_logger = logger;
_paths = paths;
_provider = provider;
_jsonOptions = new JsonSerializerOptions();
_jsonOptions.Converters.Add(new JsonStringEnumConverter());
}
/// <inheritdoc />
public Guid Id => Guid.Parse("06387815-C3CC-421F-A888-FB5F9992BEA8");
/// <inheritdoc />
public string Name => "MigrateDisplayPreferencesDatabase";
/// <inheritdoc />
public bool PerformOnNewInstall => false;
/// <inheritdoc />
public void Perform()
{
HomeSectionType[] defaults =
{
HomeSectionType.SmallLibraryTiles,
HomeSectionType.Resume,
HomeSectionType.ResumeAudio,
HomeSectionType.LiveTv,
HomeSectionType.NextUp,
HomeSectionType.LatestMedia,
HomeSectionType.None,
};
var chromecastDict = new Dictionary<string, ChromecastVersion>(StringComparer.OrdinalIgnoreCase)
{
{ "stable", ChromecastVersion.Stable },
{ "nightly", ChromecastVersion.Unstable },
{ "unstable", ChromecastVersion.Unstable }
};
var dbFilePath = Path.Combine(_paths.DataPath, DbFilename);
using (var connection = SQLite3.Open(dbFilePath, ConnectionFlags.ReadOnly, null))
{
using var dbContext = _provider.CreateContext();
var results = connection.Query("SELECT * FROM userdisplaypreferences");
foreach (var result in results)
{
var dto = JsonSerializer.Deserialize<DisplayPreferencesDto>(result[3].ToString(), _jsonOptions);
var chromecastVersion = dto.CustomPrefs.TryGetValue("chromecastVersion", out var version)
? chromecastDict[version]
: ChromecastVersion.Stable;
var displayPreferences = new DisplayPreferences(new Guid(result[1].ToBlob()), result[2].ToString())
{
IndexBy = Enum.TryParse<IndexingKind>(dto.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null,
ShowBackdrop = dto.ShowBackdrop,
ShowSidebar = dto.ShowSidebar,
ScrollDirection = dto.ScrollDirection,
ChromecastVersion = chromecastVersion,
SkipForwardLength = dto.CustomPrefs.TryGetValue("skipForwardLength", out var length)
? int.Parse(length, CultureInfo.InvariantCulture)
: 30000,
SkipBackwardLength = dto.CustomPrefs.TryGetValue("skipBackLength", out length)
? int.Parse(length, CultureInfo.InvariantCulture)
: 10000,
EnableNextVideoInfoOverlay = dto.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enabled)
? bool.Parse(enabled)
: true,
DashboardTheme = dto.CustomPrefs.TryGetValue("dashboardtheme", out var theme) ? theme : string.Empty,
TvHome = dto.CustomPrefs.TryGetValue("tvhome", out var home) ? home : string.Empty
};
for (int i = 0; i < 7; i++)
{
dto.CustomPrefs.TryGetValue("homesection" + i, out var homeSection);
displayPreferences.HomeSections.Add(new HomeSection
{
Order = i,
Type = Enum.TryParse<HomeSectionType>(homeSection, true, out var type) ? type : defaults[i]
});
}
var defaultLibraryPrefs = new ItemDisplayPreferences(displayPreferences.UserId, Guid.Empty, displayPreferences.Client)
{
SortBy = dto.SortBy ?? "SortName",
SortOrder = dto.SortOrder,
RememberIndexing = dto.RememberIndexing,
RememberSorting = dto.RememberSorting,
};
dbContext.Add(defaultLibraryPrefs);
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))
{
continue;
}
var libraryDisplayPreferences = new ItemDisplayPreferences(displayPreferences.UserId, itemId, displayPreferences.Client)
{
SortBy = dto.SortBy ?? "SortName",
SortOrder = dto.SortOrder,
RememberIndexing = dto.RememberIndexing,
RememberSorting = dto.RememberSorting,
};
if (Enum.TryParse<ViewType>(dto.ViewType, true, out var viewType))
{
libraryDisplayPreferences.ViewType = viewType;
}
dbContext.ItemDisplayPreferences.Add(libraryDisplayPreferences);
}
dbContext.Add(displayPreferences);
}
dbContext.SaveChanges();
}
try
{
File.Move(dbFilePath, dbFilePath + ".old");
var journalPath = dbFilePath + "-journal";
if (File.Exists(journalPath))
{
File.Move(journalPath, dbFilePath + ".old-journal");
}
}
catch (IOException e)
{
_logger.LogError(e, "Error renaming legacy display preferences database to 'displaypreferences.db.old'");
}
}
}
}

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Api.UserLibrary;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
@ -11,7 +12,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Channels;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging;

@ -1,9 +1,11 @@
using System.Threading;
using System;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging;
@ -13,7 +15,7 @@ namespace MediaBrowser.Api
/// Class UpdateDisplayPreferences.
/// </summary>
[Route("/DisplayPreferences/{DisplayPreferencesId}", "POST", Summary = "Updates a user's display preferences for an item")]
public class UpdateDisplayPreferences : DisplayPreferences, IReturnVoid
public class UpdateDisplayPreferences : DisplayPreferencesDto, IReturnVoid
{
/// <summary>
/// Gets or sets the id.
@ -27,7 +29,7 @@ namespace MediaBrowser.Api
}
[Route("/DisplayPreferences/{Id}", "GET", Summary = "Gets a user's display preferences for an item")]
public class GetDisplayPreferences : IReturn<DisplayPreferences>
public class GetDisplayPreferences : IReturn<DisplayPreferencesDto>
{
/// <summary>
/// Gets or sets the id.
@ -50,28 +52,21 @@ namespace MediaBrowser.Api
public class DisplayPreferencesService : BaseApiService
{
/// <summary>
/// The _display preferences manager.
/// The display preferences manager.
/// </summary>
private readonly IDisplayPreferencesRepository _displayPreferencesManager;
/// <summary>
/// The _json serializer.
/// </summary>
private readonly IJsonSerializer _jsonSerializer;
private readonly IDisplayPreferencesManager _displayPreferencesManager;
/// <summary>
/// Initializes a new instance of the <see cref="DisplayPreferencesService" /> class.
/// </summary>
/// <param name="jsonSerializer">The json serializer.</param>
/// <param name="displayPreferencesManager">The display preferences manager.</param>
public DisplayPreferencesService(
ILogger<DisplayPreferencesService> logger,
IServerConfigurationManager serverConfigurationManager,
IHttpResultFactory httpResultFactory,
IJsonSerializer jsonSerializer,
IDisplayPreferencesRepository displayPreferencesManager)
IDisplayPreferencesManager displayPreferencesManager)
: base(logger, serverConfigurationManager, httpResultFactory)
{
_jsonSerializer = jsonSerializer;
_displayPreferencesManager = displayPreferencesManager;
}
@ -81,9 +76,41 @@ namespace MediaBrowser.Api
/// <param name="request">The request.</param>
public object Get(GetDisplayPreferences request)
{
var result = _displayPreferencesManager.GetDisplayPreferences(request.Id, request.UserId, request.Client);
var displayPreferences = _displayPreferencesManager.GetDisplayPreferences(Guid.Parse(request.UserId), request.Client);
var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(displayPreferences.UserId, Guid.Empty, displayPreferences.Client);
var dto = new DisplayPreferencesDto
{
Client = displayPreferences.Client,
Id = displayPreferences.UserId.ToString(),
ViewType = itemPreferences.ViewType.ToString(),
SortBy = itemPreferences.SortBy,
SortOrder = itemPreferences.SortOrder,
IndexBy = displayPreferences.IndexBy?.ToString(),
RememberIndexing = itemPreferences.RememberIndexing,
RememberSorting = itemPreferences.RememberSorting,
ScrollDirection = displayPreferences.ScrollDirection,
ShowBackdrop = displayPreferences.ShowBackdrop,
ShowSidebar = displayPreferences.ShowSidebar
};
foreach (var homeSection in displayPreferences.HomeSections)
{
dto.CustomPrefs["homesection" + homeSection.Order] = homeSection.Type.ToString().ToLowerInvariant();
}
return ToOptimizedResult(result);
foreach (var itemDisplayPreferences in _displayPreferencesManager.ListItemDisplayPreferences(displayPreferences.UserId, displayPreferences.Client))
{
dto.CustomPrefs["landing-" + itemDisplayPreferences.ItemId] = itemDisplayPreferences.ViewType.ToString().ToLowerInvariant();
}
dto.CustomPrefs["chromecastVersion"] = displayPreferences.ChromecastVersion.ToString().ToLowerInvariant();
dto.CustomPrefs["skipForwardLength"] = displayPreferences.SkipForwardLength.ToString();
dto.CustomPrefs["skipBackLength"] = displayPreferences.SkipBackwardLength.ToString();
dto.CustomPrefs["enableNextVideoInfoOverlay"] = displayPreferences.EnableNextVideoInfoOverlay.ToString();
dto.CustomPrefs["tvhome"] = displayPreferences.TvHome;
return ToOptimizedResult(dto);
}
/// <summary>
@ -92,10 +119,71 @@ namespace MediaBrowser.Api
/// <param name="request">The request.</param>
public void Post(UpdateDisplayPreferences request)
{
// Serialize to json and then back so that the core doesn't see the request dto type
var displayPreferences = _jsonSerializer.DeserializeFromString<DisplayPreferences>(_jsonSerializer.SerializeToString(request));
HomeSectionType[] defaults =
{
HomeSectionType.SmallLibraryTiles,
HomeSectionType.Resume,
HomeSectionType.ResumeAudio,
HomeSectionType.LiveTv,
HomeSectionType.NextUp,
HomeSectionType.LatestMedia,
HomeSectionType.None,
};
var prefs = _displayPreferencesManager.GetDisplayPreferences(Guid.Parse(request.UserId), request.Client);
prefs.IndexBy = Enum.TryParse<IndexingKind>(request.IndexBy, true, out var indexBy) ? indexBy : (IndexingKind?)null;
prefs.ShowBackdrop = request.ShowBackdrop;
prefs.ShowSidebar = request.ShowSidebar;
prefs.ScrollDirection = request.ScrollDirection;
prefs.ChromecastVersion = request.CustomPrefs.TryGetValue("chromecastVersion", out var chromecastVersion)
? Enum.Parse<ChromecastVersion>(chromecastVersion, true)
: ChromecastVersion.Stable;
prefs.EnableNextVideoInfoOverlay = request.CustomPrefs.TryGetValue("enableNextVideoInfoOverlay", out var enableNextVideoInfoOverlay)
? bool.Parse(enableNextVideoInfoOverlay)
: true;
prefs.SkipBackwardLength = request.CustomPrefs.TryGetValue("skipBackLength", out var skipBackLength) ? int.Parse(skipBackLength) : 10000;
prefs.SkipForwardLength = request.CustomPrefs.TryGetValue("skipForwardLength", out var skipForwardLength) ? int.Parse(skipForwardLength) : 30000;
prefs.DashboardTheme = request.CustomPrefs.TryGetValue("dashboardTheme", out var theme) ? theme : string.Empty;
prefs.TvHome = request.CustomPrefs.TryGetValue("tvhome", out var home) ? home : string.Empty;
prefs.HomeSections.Clear();
foreach (var key in request.CustomPrefs.Keys.Where(key => key.StartsWith("homesection")))
{
var order = int.Parse(key.AsSpan().Slice("homesection".Length));
if (!Enum.TryParse<HomeSectionType>(request.CustomPrefs[key], true, out var type))
{
type = order < 7 ? defaults[order] : HomeSectionType.None;
}
prefs.HomeSections.Add(new HomeSection
{
Order = order,
Type = type
});
}
foreach (var key in request.CustomPrefs.Keys.Where(key => key.StartsWith("landing-")))
{
var itemPreferences = _displayPreferencesManager.GetItemDisplayPreferences(prefs.UserId, Guid.Parse(key.Substring("landing-".Length)), prefs.Client);
itemPreferences.ViewType = Enum.Parse<ViewType>(request.ViewType);
_displayPreferencesManager.SaveChanges(itemPreferences);
}
var itemPrefs = _displayPreferencesManager.GetItemDisplayPreferences(prefs.UserId, Guid.Empty, prefs.Client);
itemPrefs.SortBy = request.SortBy;
itemPrefs.SortOrder = request.SortOrder;
itemPrefs.RememberIndexing = request.RememberIndexing;
itemPrefs.RememberSorting = request.RememberSorting;
if (Enum.TryParse<ViewType>(request.ViewType, true, out var viewType))
{
itemPrefs.ViewType = viewType;
}
_displayPreferencesManager.SaveDisplayPreferences(displayPreferences, request.UserId, request.Client, CancellationToken.None);
_displayPreferencesManager.SaveChanges(prefs);
_displayPreferencesManager.SaveChanges(itemPrefs);
}
}
}

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;

@ -194,7 +194,8 @@ namespace MediaBrowser.Api.Playback.Hls
var paddedBitrate = Convert.ToInt32(bitrate * 1.15);
// Main stream
builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + paddedBitrate.ToString(CultureInfo.InvariantCulture));
builder.Append("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=")
.AppendLine(paddedBitrate.ToString(CultureInfo.InvariantCulture));
var playlistUrl = "hls/" + Path.GetFileName(firstPlaylist).Replace(".m3u8", "/stream.m3u8");
builder.AppendLine(playlistUrl);

@ -968,7 +968,8 @@ namespace MediaBrowser.Api.Playback.Hls
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
builder.AppendLine("#EXT-X-VERSION:3");
builder.AppendLine("#EXT-X-TARGETDURATION:" + Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(CultureInfo.InvariantCulture));
builder.Append("#EXT-X-TARGETDURATION:")
.AppendLine(Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(CultureInfo.InvariantCulture));
builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
var queryStringIndex = Request.RawUrl.IndexOf('?');
@ -983,14 +984,17 @@ namespace MediaBrowser.Api.Playback.Hls
foreach (var length in segmentLengths)
{
builder.AppendLine("#EXTINF:" + length.ToString("0.0000", CultureInfo.InvariantCulture) + ", nodesc");
builder.AppendLine(string.Format("hls1/{0}/{1}{2}{3}",
builder.Append("#EXTINF:")
.Append(length.ToString("0.0000", CultureInfo.InvariantCulture))
.AppendLine(", nodesc");
builder.AppendFormat(
CultureInfo.InvariantCulture,
"hls1/{0}/{1}{2}{3}",
name,
index.ToString(CultureInfo.InvariantCulture),
GetSegmentFileExtension(request),
queryString));
queryString).AppendLine();
index++;
}

@ -175,11 +175,12 @@ namespace MediaBrowser.Api.Subtitles
throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)");
}
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-TARGETDURATION:" + request.SegmentLength.ToString(CultureInfo.InvariantCulture));
builder.AppendLine("#EXT-X-VERSION:3");
builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
builder.AppendLine("#EXTM3U")
.Append("#EXT-X-TARGETDURATION:")
.AppendLine(request.SegmentLength.ToString(CultureInfo.InvariantCulture))
.AppendLine("#EXT-X-VERSION:3")
.AppendLine("#EXT-X-MEDIA-SEQUENCE:0")
.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
long positionTicks = 0;
@ -190,7 +191,9 @@ namespace MediaBrowser.Api.Subtitles
var remaining = runtime - positionTicks;
var lengthTicks = Math.Min(remaining, segmentLengthTicks);
builder.AppendLine("#EXTINF:" + TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture) + ",");
builder.Append("#EXTINF:")
.Append(TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture))
.AppendLine(",");
var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);

@ -1,6 +1,7 @@
using System;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;

@ -1,5 +1,6 @@
using System;
using System.Linq;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Services;
@ -466,8 +467,8 @@ namespace MediaBrowser.Api.UserLibrary
var sortOrderValue = sortOrders.Length > sortOrderIndex ? sortOrders[sortOrderIndex] : null;
var sortOrder = string.Equals(sortOrderValue, "Descending", StringComparison.OrdinalIgnoreCase)
? MediaBrowser.Model.Entities.SortOrder.Descending
: MediaBrowser.Model.Entities.SortOrder.Ascending;
? Jellyfin.Data.Enums.SortOrder.Descending
: Jellyfin.Data.Enums.SortOrder.Ascending;
result[i] = new ValueTuple<string, SortOrder>(vals[i], sortOrder);
}

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;

@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using Jellyfin.Data.Entities;
namespace MediaBrowser.Controller
{
/// <summary>
/// Manages the storage and retrieval of display preferences.
/// </summary>
public interface IDisplayPreferencesManager
{
/// <summary>
/// Gets the display preferences for the user and client.
/// </summary>
/// <param name="userId">The user's id.</param>
/// <param name="client">The client string.</param>
/// <returns>The associated display preferences.</returns>
DisplayPreferences GetDisplayPreferences(Guid userId, string client);
/// <summary>
/// Gets the default 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 item display preferences.</returns>
ItemDisplayPreferences GetItemDisplayPreferences(Guid userId, Guid itemId, string client);
/// <summary>
/// Gets all of the item display preferences for the user and client.
/// </summary>
/// <param name="userId">The user id.</param>
/// <param name="client">The client string.</param>
/// <returns>A list of item display preferences.</returns>
IList<ItemDisplayPreferences> ListItemDisplayPreferences(Guid userId, string client);
/// <summary>
/// Saves changes to the provided display preferences.
/// </summary>
/// <param name="preferences">The display preferences to save.</param>
void SaveChanges(DisplayPreferences preferences);
/// <summary>
/// Saves changes to the provided item display preferences.
/// </summary>
/// <param name="preferences">The item display preferences to save.</param>
void SaveChanges(ItemDisplayPreferences preferences);
}
}

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;

@ -1,53 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using MediaBrowser.Model.Entities;
namespace MediaBrowser.Controller.Persistence
{
/// <summary>
/// Interface IDisplayPreferencesRepository.
/// </summary>
public interface IDisplayPreferencesRepository : IRepository
{
/// <summary>
/// Saves display preferences for an item.
/// </summary>
/// <param name="displayPreferences">The display preferences.</param>
/// <param name="userId">The user id.</param>
/// <param name="client">The client.</param>
/// <param name="cancellationToken">The cancellation token.</param>
void SaveDisplayPreferences(
DisplayPreferences displayPreferences,
string userId,
string client,
CancellationToken cancellationToken);
/// <summary>
/// Saves all display preferences for a user.
/// </summary>
/// <param name="displayPreferences">The display preferences.</param>
/// <param name="userId">The user id.</param>
/// <param name="cancellationToken">The cancellation token.</param>
void SaveAllDisplayPreferences(
IEnumerable<DisplayPreferences> displayPreferences,
Guid userId,
CancellationToken cancellationToken);
/// <summary>
/// Gets the display preferences.
/// </summary>
/// <param name="displayPreferencesId">The display preferences id.</param>
/// <param name="userId">The user id.</param>
/// <param name="client">The client.</param>
/// <returns>Task{DisplayPreferences}.</returns>
DisplayPreferences GetDisplayPreferences(string displayPreferencesId, string userId, string client);
/// <summary>
/// Gets all display preferences for the given user.
/// </summary>
/// <param name="userId">The user id.</param>
/// <returns>Task{DisplayPreferences}.</returns>
IEnumerable<DisplayPreferences> GetAllDisplayPreferences(Guid userId);
}
}

@ -6,6 +6,7 @@ using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;

@ -1377,7 +1377,9 @@ namespace MediaBrowser.MediaEncoding.Probing
// OR -> COMMENT. SUBTITLE: DESCRIPTION
// e.g. -> 4/13. The Doctor's Wife: Science fiction drama. When he follows a Time Lord distress signal, the Doctor puts Amy, Rory and his beloved TARDIS in grave danger. Also in HD. [AD,S]
// e.g. -> CBeebies Bedtime Hour. The Mystery: Animated adventures of two friends who live on an island in the middle of the big city. Some of Abney and Teal's favourite objects are missing. [S]
if (string.IsNullOrWhiteSpace(subTitle) && !string.IsNullOrWhiteSpace(description) && description.Substring(0, Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)).Contains(":")) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename
if (string.IsNullOrWhiteSpace(subTitle)
&& !string.IsNullOrWhiteSpace(description)
&& description.AsSpan().Slice(0, Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)).IndexOf(':') != -1) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename
{
string[] parts = description.Split(':');
if (parts.Length > 0)
@ -1385,7 +1387,7 @@ namespace MediaBrowser.MediaEncoding.Probing
string subtitle = parts[0];
try
{
if (subtitle.Contains("/")) // It contains a episode number and season number
if (subtitle.Contains('/', StringComparison.Ordinal)) // It contains a episode number and season number
{
string[] numbers = subtitle.Split(' ');
video.IndexNumber = int.Parse(numbers[0].Replace(".", "").Split('/')[0]);
@ -1400,8 +1402,11 @@ namespace MediaBrowser.MediaEncoding.Probing
}
catch // Default parsing
{
if (subtitle.Contains(".")) // skip the comment, keep the subtitle
if (subtitle.Contains('.', StringComparison.Ordinal))
{
// skip the comment, keep the subtitle
description = string.Join(".", subtitle.Split('.'), 1, subtitle.Split('.').Length - 1).Trim(); // skip the first
}
else
{
description = subtitle.Trim(); // Clean up whitespaces and save it

@ -731,19 +731,19 @@ namespace MediaBrowser.MediaEncoding.Subtitles
var date = _fileSystem.GetLastWriteTimeUtc(mediaPath);
var filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension;
ReadOnlySpan<char> filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension;
var prefix = filename.Substring(0, 1);
var prefix = filename.Slice(0, 1);
return Path.Combine(SubtitleCachePath, prefix, filename);
return Path.Join(SubtitleCachePath, prefix, filename);
}
else
{
var filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension;
ReadOnlySpan<char> filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension;
var prefix = filename.Substring(0, 1);
var prefix = filename.Slice(0, 1);
return Path.Combine(SubtitleCachePath, prefix, filename);
return Path.Join(SubtitleCachePath, prefix, filename);
}
}

@ -1,6 +1,6 @@
#pragma warning disable CS1591
using MediaBrowser.Model.Entities;
using Jellyfin.Data.Enums;
namespace MediaBrowser.Model.Dlna
{

@ -1,22 +1,18 @@
#nullable disable
using System.Collections.Generic;
using Jellyfin.Data.Enums;
namespace MediaBrowser.Model.Entities
{
/// <summary>
/// Defines the display preferences for any item that supports them (usually Folders).
/// </summary>
public class DisplayPreferences
public class DisplayPreferencesDto
{
/// <summary>
/// The image scale.
/// Initializes a new instance of the <see cref="DisplayPreferencesDto" /> class.
/// </summary>
private const double ImageScale = .9;
/// <summary>
/// Initializes a new instance of the <see cref="DisplayPreferences" /> class.
/// </summary>
public DisplayPreferences()
public DisplayPreferencesDto()
{
RememberIndexing = false;
PrimaryImageHeight = 250;

@ -1,18 +0,0 @@
namespace MediaBrowser.Model.Entities
{
/// <summary>
/// Enum ScrollDirection.
/// </summary>
public enum ScrollDirection
{
/// <summary>
/// The horizontal.
/// </summary>
Horizontal,
/// <summary>
/// The vertical.
/// </summary>
Vertical
}
}

@ -1,18 +0,0 @@
namespace MediaBrowser.Model.Entities
{
/// <summary>
/// Enum SortOrder.
/// </summary>
public enum SortOrder
{
/// <summary>
/// The ascending.
/// </summary>
Ascending,
/// <summary>
/// The descending.
/// </summary>
Descending
}
}

@ -2,7 +2,7 @@
#pragma warning disable CS1591
using System;
using MediaBrowser.Model.Entities;
using Jellyfin.Data.Enums;
namespace MediaBrowser.Model.LiveTv
{

@ -1,6 +1,6 @@
#pragma warning disable CS1591
using MediaBrowser.Model.Entities;
using Jellyfin.Data.Enums;
namespace MediaBrowser.Model.LiveTv
{

@ -93,7 +93,7 @@ namespace MediaBrowser.Providers.MediaInfo
private string GetAudioImagePath(Audio item)
{
string filename = null;
string filename;
if (item.GetType() == typeof(Audio))
{
@ -116,9 +116,9 @@ namespace MediaBrowser.Providers.MediaInfo
filename = item.Id.ToString("N", CultureInfo.InvariantCulture) + ".jpg";
}
var prefix = filename.Substring(0, 1);
var prefix = filename.AsSpan().Slice(0, 1);
return Path.Combine(AudioImagesPath, prefix, filename);
return Path.Join(AudioImagesPath, prefix, filename);
}
public string AudioImagesPath => Path.Combine(_config.ApplicationPaths.CachePath, "extracted-audio-images");

@ -170,7 +170,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
item.SetProviderId(MetadataProvider.Imdb, result.imdbID);
if (result.Year.Length > 0
&& int.TryParse(result.Year.Substring(0, Math.Min(result.Year.Length, 4)), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedYear))
&& int.TryParse(result.Year.AsSpan().Slice(0, Math.Min(result.Year.Length, 4)), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedYear))
{
item.ProductionYear = parsedYear;
}

@ -62,7 +62,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4
&& int.TryParse(result.Year.Substring(0, 4), NumberStyles.Number, _usCulture, out var year)
&& int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, _usCulture, out var year)
&& year >= 0)
{
item.ProductionYear = year;
@ -163,7 +163,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
}
if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4
&& int.TryParse(result.Year.Substring(0, 4), NumberStyles.Number, _usCulture, out var year)
&& int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, _usCulture, out var year)
&& year >= 0)
{
item.ProductionYear = year;

@ -188,7 +188,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
for (var i = 0; i < episode.GuestStars.Length; ++i)
{
var currentActor = episode.GuestStars[i];
var roleStartIndex = currentActor.IndexOf('(');
var roleStartIndex = currentActor.IndexOf('(', StringComparison.Ordinal);
if (roleStartIndex == -1)
{
@ -207,7 +207,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb
for (var j = i + 1; j < episode.GuestStars.Length; ++j)
{
var currentRole = episode.GuestStars[j];
var roleEndIndex = currentRole.IndexOf(')');
var roleEndIndex = currentRole.IndexOf(')', StringComparison.Ordinal);
if (roleEndIndex == -1)
{

@ -13,7 +13,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
public BelongsToCollection Belongs_To_Collection { get; set; }
public int Budget { get; set; }
public long Budget { get; set; }
public List<Genre> Genres { get; set; }
@ -39,7 +39,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Models.Movies
public string Release_Date { get; set; }
public int Revenue { get; set; }
public long Revenue { get; set; }
public int Runtime { get; set; }

@ -251,9 +251,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
private static string GetPersonDataPath(IApplicationPaths appPaths, string tmdbId)
{
var letter = tmdbId.GetMD5().ToString().Substring(0, 1);
var letter = tmdbId.GetMD5().ToString().AsSpan().Slice(0, 1);
return Path.Combine(GetPersonsDataPath(appPaths), letter, tmdbId);
return Path.Join(GetPersonsDataPath(appPaths), letter, tmdbId);
}
internal static string GetPersonDataFilePath(IApplicationPaths appPaths, string tmdbId)

@ -222,8 +222,16 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (index != -1)
{
var tmdbId = xml.Substring(index + srch.Length).TrimEnd('/').Split('-')[0];
if (!string.IsNullOrWhiteSpace(tmdbId) && int.TryParse(tmdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
var tmdbId = xml.AsSpan().Slice(index + srch.Length).TrimEnd('/');
index = tmdbId.IndexOf('-');
if (index != -1)
{
tmdbId = tmdbId.Slice(0, index);
}
if (!tmdbId.IsEmpty
&& !tmdbId.IsWhiteSpace()
&& int.TryParse(tmdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
{
item.SetProviderId(MetadataProvider.Tmdb, value.ToString(UsCulture));
}
@ -237,8 +245,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (index != -1)
{
var tvdbId = xml.Substring(index + srch.Length).TrimEnd('/');
if (!string.IsNullOrWhiteSpace(tvdbId) && int.TryParse(tvdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
var tvdbId = xml.AsSpan().Slice(index + srch.Length).TrimEnd('/');
if (!tvdbId.IsEmpty
&& !tvdbId.IsWhiteSpace()
&& int.TryParse(tvdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
{
item.SetProviderId(MetadataProvider.Tvdb, value.ToString(UsCulture));
}
@ -442,8 +452,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
var val = reader.ReadElementContentAsString();
var hasAspectRatio = item as IHasAspectRatio;
if (!string.IsNullOrWhiteSpace(val) && hasAspectRatio != null)
if (!string.IsNullOrWhiteSpace(val)
&& item is IHasAspectRatio hasAspectRatio)
{
hasAspectRatio.AspectRatio = val;
}

@ -162,6 +162,7 @@ The following sections describe some more advanced scenarios for running the ser
It is not necessary to host the frontend web client as part of the backend server. Hosting these two components separately may be useful for frontend developers who would prefer to host the client in a separate webpack development server for a tighter development loop. See the [jellyfin-web](https://github.com/jellyfin/jellyfin-web#getting-started) repo for instructions on how to do this.
To instruct the server not to host the web content, there is a `nowebcontent` configuration flag that must be set. This can specified using the command line switch `--nowebcontent` or the environment variable `JELLYFIN_NOWEBCONTENT=true`.
To instruct the server not to host the web content, there is a `nowebclient` configuration flag that must be set. This can specified using the command line
switch `--nowebclient` or the environment variable `JELLYFIN_NOWEBCONTENT=true`.
Since this is a common scenario, there is also a separate launch profile defined for Visual Studio called `Jellyfin.Server (nowebcontent)` that can be selected from the 'Start Debugging' dropdown in the main toolbar.

@ -1,4 +1,4 @@
using System.Reflection;
[assembly: AssemblyVersion("10.6.0")]
[assembly: AssemblyFileVersion("10.6.0")]
[assembly: AssemblyVersion("10.7.0")]
[assembly: AssemblyFileVersion("10.7.0")]

@ -1,7 +1,7 @@
---
# We just wrap `build` so this is really it
name: "jellyfin"
version: "10.6.0"
version: "10.7.0"
packages:
- debian.amd64
- debian.arm64

@ -4,6 +4,7 @@
set -o errexit
set -o pipefail
set -o xtrace
usage() {
echo -e "bump_version - increase the shared version and generate changelogs"
@ -58,7 +59,7 @@ sed -i "s/${old_version_sed}/${new_version}/g" ${debian_equivs_file}
debian_changelog_file="debian/changelog"
debian_changelog_temp="$( mktemp )"
# Create new temp file with our changelog
echo -e "jellyfin (${new_version_deb}) unstable; urgency=medium
echo -e "jellyfin-server (${new_version_deb}) unstable; urgency=medium
* New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v${new_version}

6
debian/changelog vendored

@ -1,3 +1,9 @@
jellyfin-server (10.7.0-1) unstable; urgency=medium
* Forthcoming stable release
-- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 27 Jul 2020 19:09:45 -0400
jellyfin-server (10.6.0-2) unstable; urgency=medium
* Fix upgrade bug

@ -5,7 +5,7 @@ Homepage: https://jellyfin.org
Standards-Version: 3.9.2
Package: jellyfin
Version: 10.6.0
Version: 10.7.0
Maintainer: Jellyfin Packaging Team <packaging@jellyfin.org>
Depends: jellyfin-server, jellyfin-web
Description: Provides the Jellyfin Free Software Media System

@ -7,7 +7,7 @@
%endif
Name: jellyfin
Version: 10.6.0
Version: 10.7.0
Release: 1%{?dist}
Summary: The Free Software Media System
License: GPLv3
@ -139,6 +139,8 @@ fi
%systemd_postun_with_restart jellyfin.service
%changelog
* Mon Jul 27 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
- Forthcoming stable release
* Mon Mar 23 2020 Jellyfin Packaging Team <packaging@jellyfin.org>
- Forthcoming stable release
* Fri Oct 11 2019 Jellyfin Packaging Team <packaging@jellyfin.org>

Loading…
Cancel
Save