Merge remote-tracking branch 'upstream/master' into HEAD

pull/3340/head
crobibero 4 years ago
commit 589735f60c

@ -7,6 +7,7 @@
- [anthonylavado](https://github.com/anthonylavado)
- [Artiume](https://github.com/Artiume)
- [AThomsen](https://github.com/AThomsen)
- [barronpm](https://github.com/barronpm)
- [bilde2910](https://github.com/bilde2910)
- [bfayers](https://github.com/bfayers)
- [BnMcG](https://github.com/BnMcG)
@ -130,6 +131,7 @@
- [XVicarious](https://github.com/XVicarious)
- [YouKnowBlom](https://github.com/YouKnowBlom)
- [KristupasSavickas](https://github.com/KristupasSavickas)
- [Pusta](https://github.com/pusta)
# Emby Contributors

@ -4,11 +4,12 @@ using System;
using System.Linq;
using System.Threading.Tasks;
using Emby.Dlna.Service;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.TV;
@ -32,7 +33,8 @@ namespace Emby.Dlna.ContentDirectory
private readonly IMediaEncoder _mediaEncoder;
private readonly ITVSeriesManager _tvSeriesManager;
public ContentDirectory(IDlnaManager dlna,
public ContentDirectory(
IDlnaManager dlna,
IUserDataManager userDataManager,
IImageProcessor imageProcessor,
ILibraryManager libraryManager,
@ -131,7 +133,7 @@ namespace Emby.Dlna.ContentDirectory
foreach (var user in _userManager.Users)
{
if (user.Policy.IsAdministrator)
if (user.HasPermission(PermissionKind.IsAdministrator))
{
return user;
}

@ -10,6 +10,7 @@ using System.Threading;
using System.Xml;
using Emby.Dlna.Didl;
using Emby.Dlna.Service;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Drawing;
@ -17,7 +18,6 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaEncoding;
@ -28,6 +28,12 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
using Book = MediaBrowser.Controller.Entities.Book;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Genre = MediaBrowser.Controller.Entities.Genre;
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace Emby.Dlna.ContentDirectory
{
@ -731,7 +737,7 @@ namespace Emby.Dlna.ContentDirectory
return GetGenres(item, user, query);
}
var array = new ServerItem[]
var array = new[]
{
new ServerItem(item)
{
@ -1115,7 +1121,7 @@ namespace Emby.Dlna.ContentDirectory
private QueryResult<ServerItem> GetMusicPlaylists(User user, InternalItemsQuery query)
{
query.Parent = null;
query.IncludeItemTypes = new[] { typeof(Playlist).Name };
query.IncludeItemTypes = new[] { nameof(Playlist) };
query.SetUser(user);
query.Recursive = true;
@ -1132,10 +1138,9 @@ namespace Emby.Dlna.ContentDirectory
{
UserId = user.Id,
Limit = 50,
IncludeItemTypes = new[] { typeof(Audio).Name },
ParentId = parent == null ? Guid.Empty : parent.Id,
IncludeItemTypes = new[] { nameof(Audio) },
ParentId = parent?.Id ?? Guid.Empty,
GroupItems = true
}, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
return ToResult(items);
@ -1150,7 +1155,6 @@ namespace Emby.Dlna.ContentDirectory
Limit = query.Limit,
StartIndex = query.StartIndex,
UserId = query.User.Id
}, new[] { parent }, query.DtoOptions);
return ToResult(result);
@ -1167,7 +1171,6 @@ namespace Emby.Dlna.ContentDirectory
IncludeItemTypes = new[] { typeof(Episode).Name },
ParentId = parent == null ? Guid.Empty : parent.Id,
GroupItems = false
}, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
return ToResult(items);
@ -1177,14 +1180,14 @@ namespace Emby.Dlna.ContentDirectory
{
query.OrderBy = Array.Empty<(string, SortOrder)>();
var items = _userViewManager.GetLatestItems(new LatestItemsQuery
var items = _userViewManager.GetLatestItems(
new LatestItemsQuery
{
UserId = user.Id,
Limit = 50,
IncludeItemTypes = new[] { typeof(Movie).Name },
ParentId = parent == null ? Guid.Empty : parent.Id,
IncludeItemTypes = new[] { nameof(Movie) },
ParentId = parent?.Id ?? Guid.Empty,
GroupItems = true
}, query.DtoOptions).Select(i => i.Item1 ?? i.Item2.FirstOrDefault()).Where(i => i != null).ToArray();
return ToResult(items);
@ -1217,7 +1220,11 @@ namespace Emby.Dlna.ContentDirectory
Recursive = true,
ParentId = parentId,
GenreIds = new[] { item.Id },
IncludeItemTypes = new[] { typeof(Movie).Name, typeof(Series).Name },
IncludeItemTypes = new[]
{
nameof(Movie),
nameof(Series)
},
Limit = limit,
StartIndex = startIndex,
DtoOptions = GetDtoOptions()

@ -6,14 +6,13 @@ using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using Emby.Dlna.Configuration;
using Emby.Dlna.ContentDirectory;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Playlists;
@ -23,6 +22,13 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Genre = MediaBrowser.Controller.Entities.Genre;
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
using Season = MediaBrowser.Controller.Entities.TV.Season;
using Series = MediaBrowser.Controller.Entities.TV.Series;
using XmlAttribute = MediaBrowser.Model.Dlna.XmlAttribute;
namespace Emby.Dlna.Didl
{
@ -421,7 +427,6 @@ namespace Emby.Dlna.Didl
case StubType.FavoriteSeries: return _localization.GetLocalizedString("HeaderFavoriteShows");
case StubType.FavoriteEpisodes: return _localization.GetLocalizedString("HeaderFavoriteEpisodes");
case StubType.Series: return _localization.GetLocalizedString("Shows");
default: break;
}
}
@ -670,7 +675,7 @@ namespace Emby.Dlna.Didl
return;
}
MediaBrowser.Model.Dlna.XmlAttribute secAttribute = null;
XmlAttribute secAttribute = null;
foreach (var attribute in _profile.XmlRootAttributes)
{
if (string.Equals(attribute.Name, "xmlns:sec", StringComparison.OrdinalIgnoreCase))
@ -995,7 +1000,6 @@ namespace Emby.Dlna.Didl
}
AddImageResElement(item, writer, 160, 160, "jpg", "JPEG_TN");
}
private void AddImageResElement(

@ -7,6 +7,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Emby.Dlna.Didl;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Dlna;
using MediaBrowser.Controller.Drawing;
@ -22,6 +23,7 @@ using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Session;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using Photo = MediaBrowser.Controller.Entities.Photo;
namespace Emby.Dlna.PlayTo
{
@ -446,7 +448,13 @@ namespace Emby.Dlna.PlayTo
}
}
private PlaylistItem CreatePlaylistItem(BaseItem item, User user, long startPostionTicks, string mediaSourceId, int? audioStreamIndex, int? subtitleStreamIndex)
private PlaylistItem CreatePlaylistItem(
BaseItem item,
User user,
long startPostionTicks,
string mediaSourceId,
int? audioStreamIndex,
int? subtitleStreamIndex)
{
var deviceInfo = _device.Properties;

@ -4,6 +4,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Drawing;
@ -14,6 +15,7 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using Microsoft.Extensions.Logging;
using Photo = MediaBrowser.Controller.Entities.Photo;
namespace Emby.Drawing
{
@ -349,6 +351,13 @@ namespace Emby.Drawing
});
}
/// <inheritdoc />
public string GetImageCacheTag(User user)
{
return (user.ProfileImage.Path + user.ProfileImage.LastModified.Ticks).GetMD5()
.ToString("N", CultureInfo.InvariantCulture);
}
private async Task<(string path, DateTime dateModified)> GetSupportedImage(string originalImagePath, DateTime dateModified)
{
var inputFormat = Path.GetExtension(originalImagePath)

@ -4,6 +4,8 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
@ -101,7 +103,7 @@ namespace Emby.Notifications
switch (request.SendToUserMode.Value)
{
case SendToUserType.Admins:
return _userManager.Users.Where(i => i.Policy.IsAdministrator)
return _userManager.Users.Where(i => i.HasPermission(PermissionKind.IsAdministrator))
.Select(i => i.Id);
case SendToUserType.All:
return _userManager.UsersIds;
@ -117,7 +119,7 @@ namespace Emby.Notifications
var config = GetConfiguration();
return _userManager.Users
.Where(i => config.IsEnabledToSendToUser(request.NotificationType, i.Id.ToString("N", CultureInfo.InvariantCulture), i.Policy))
.Where(i => config.IsEnabledToSendToUser(request.NotificationType, i.Id.ToString("N", CultureInfo.InvariantCulture), i))
.Select(i => i.Id);
}
@ -142,7 +144,7 @@ namespace Emby.Notifications
User = user
};
_logger.LogDebug("Sending notification via {0} to user {1}", service.Name, user.Name);
_logger.LogDebug("Sending notification via {0} to user {1}", service.Name, user.Username);
try
{

@ -88,25 +88,26 @@ namespace Emby.Server.Implementations.Activity
_subManager.SubtitleDownloadFailure += OnSubtitleDownloadFailure;
_userManager.UserCreated += OnUserCreated;
_userManager.UserPasswordChanged += OnUserPasswordChanged;
_userManager.UserDeleted += OnUserDeleted;
_userManager.UserPolicyUpdated += OnUserPolicyUpdated;
_userManager.UserLockedOut += OnUserLockedOut;
_userManager.OnUserCreated += OnUserCreated;
_userManager.OnUserPasswordChanged += OnUserPasswordChanged;
_userManager.OnUserDeleted += OnUserDeleted;
_userManager.OnUserLockedOut += OnUserLockedOut;
return Task.CompletedTask;
}
private async void OnUserLockedOut(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
private async void OnUserLockedOut(object sender, GenericEventArgs<User> e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserLockedOutWithName"),
e.Argument.Name),
e.Argument.Username),
NotificationType.UserLockedOut.ToString(),
e.Argument.Id))
.ConfigureAwait(false);
e.Argument.Id)
{
LogSeverity = LogLevel.Error
}).ConfigureAwait(false);
}
private async void OnSubtitleDownloadFailure(object sender, SubtitleDownloadFailureEventArgs e)
@ -152,7 +153,7 @@ namespace Emby.Server.Implementations.Activity
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserStoppedPlayingItemWithValues"),
user.Name,
user.Username,
GetItemName(item),
e.DeviceName),
GetPlaybackStoppedNotificationType(item.MediaType),
@ -187,7 +188,7 @@ namespace Emby.Server.Implementations.Activity
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserStartedPlayingItemWithValues"),
user.Name,
user.Username,
GetItemName(item),
e.DeviceName),
GetPlaybackNotificationType(item.MediaType),
@ -304,49 +305,37 @@ namespace Emby.Server.Implementations.Activity
}).ConfigureAwait(false);
}
private async void OnUserPolicyUpdated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserPolicyUpdatedWithName"),
e.Argument.Name),
"UserPolicyUpdated",
e.Argument.Id))
.ConfigureAwait(false);
}
private async void OnUserDeleted(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
private async void OnUserDeleted(object sender, GenericEventArgs<User> e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserDeletedWithName"),
e.Argument.Name),
e.Argument.Username),
"UserDeleted",
Guid.Empty))
.ConfigureAwait(false);
}
private async void OnUserPasswordChanged(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
private async void OnUserPasswordChanged(object sender, GenericEventArgs<User> e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserPasswordChangedWithName"),
e.Argument.Name),
e.Argument.Username),
"UserPasswordChanged",
e.Argument.Id))
.ConfigureAwait(false);
}
private async void OnUserCreated(object sender, GenericEventArgs<MediaBrowser.Controller.Entities.User> e)
private async void OnUserCreated(object sender, GenericEventArgs<User> e)
{
await CreateLogEntry(new ActivityLog(
string.Format(
CultureInfo.InvariantCulture,
_localization.GetLocalizedString("UserCreatedWithName"),
e.Argument.Name),
e.Argument.Username),
"UserCreated",
e.Argument.Id))
.ConfigureAwait(false);
@ -510,11 +499,10 @@ namespace Emby.Server.Implementations.Activity
_subManager.SubtitleDownloadFailure -= OnSubtitleDownloadFailure;
_userManager.UserCreated -= OnUserCreated;
_userManager.UserPasswordChanged -= OnUserPasswordChanged;
_userManager.UserDeleted -= OnUserDeleted;
_userManager.UserPolicyUpdated -= OnUserPolicyUpdated;
_userManager.UserLockedOut -= OnUserLockedOut;
_userManager.OnUserCreated -= OnUserCreated;
_userManager.OnUserPasswordChanged -= OnUserPasswordChanged;
_userManager.OnUserDeleted -= OnUserDeleted;
_userManager.OnUserLockedOut -= OnUserLockedOut;
}
/// <summary>

@ -562,11 +562,8 @@ namespace Emby.Server.Implementations
serviceCollection.AddSingleton<IAuthenticationRepository, AuthenticationRepository>();
serviceCollection.AddSingleton<IUserRepository, SqliteUserRepository>();
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
serviceCollection.AddTransient(provider => new Lazy<IDtoService>(provider.GetRequiredService<IDtoService>));
serviceCollection.AddSingleton<IUserManager, UserManager>();
// TODO: Refactor to eliminate the circular dependency here so that Lazy<T> isn't required
// TODO: Add StartupOptions.FFmpegPath to IConfiguration and remove this custom activation
@ -659,15 +656,11 @@ namespace Emby.Server.Implementations
((SqliteDisplayPreferencesRepository)Resolve<IDisplayPreferencesRepository>()).Initialize();
((AuthenticationRepository)Resolve<IAuthenticationRepository>()).Initialize();
((SqliteUserRepository)Resolve<IUserRepository>()).Initialize();
SetStaticProperties();
var userManager = (UserManager)Resolve<IUserManager>();
userManager.Initialize();
var userDataRepo = (SqliteUserDataRepository)Resolve<IUserDataRepository>();
((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, userManager);
((SqliteItemRepository)Resolve<IItemRepository>()).Initialize(userDataRepo, Resolve<IUserManager>());
FindParts();
}
@ -750,7 +743,6 @@ namespace Emby.Server.Implementations
BaseItem.ProviderManager = Resolve<IProviderManager>();
BaseItem.LocalizationManager = Resolve<ILocalizationManager>();
BaseItem.ItemRepository = Resolve<IItemRepository>();
User.UserManager = Resolve<IUserManager>();
BaseItem.FileSystem = _fileSystemManager;
BaseItem.UserDataManager = Resolve<IUserDataManager>();
BaseItem.ChannelManager = Resolve<IChannelManager>();

@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Channels;
@ -13,8 +14,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Channels;
@ -24,6 +23,11 @@ using MediaBrowser.Model.IO;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
using Season = MediaBrowser.Controller.Entities.TV.Season;
using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace Emby.Server.Implementations.Channels
{
@ -791,7 +795,8 @@ namespace Emby.Server.Implementations.Channels
return result;
}
private async Task<ChannelItemResult> GetChannelItems(IChannel channel,
private async Task<ChannelItemResult> GetChannelItems(
IChannel channel,
User user,
string externalFolderId,
ChannelItemSortField? sortField,

@ -5,6 +5,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Collections;
using MediaBrowser.Controller.Configuration;

@ -1,4 +1,4 @@
#pragma warning disable CS1591
#pragma warning disable CS1591
using System;
using System.Collections.Generic;

@ -4,6 +4,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;

@ -1,240 +0,0 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Persistence;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
namespace Emby.Server.Implementations.Data
{
/// <summary>
/// Class SQLiteUserRepository
/// </summary>
public class SqliteUserRepository : BaseSqliteRepository, IUserRepository
{
private readonly JsonSerializerOptions _jsonOptions;
public SqliteUserRepository(
ILogger<SqliteUserRepository> logger,
IServerApplicationPaths appPaths)
: base(logger)
{
_jsonOptions = JsonDefaults.GetOptions();
DbFilePath = Path.Combine(appPaths.DataPath, "users.db");
}
/// <summary>
/// Gets the name of the repository
/// </summary>
/// <value>The name.</value>
public string Name => "SQLite";
/// <summary>
/// Opens the connection to the database.
/// </summary>
public void Initialize()
{
using (var connection = GetConnection())
{
var localUsersTableExists = TableExists(connection, "LocalUsersv2");
connection.RunQueries(new[] {
"create table if not exists LocalUsersv2 (Id INTEGER PRIMARY KEY, guid GUID NOT NULL, data BLOB NOT NULL)",
"drop index if exists idx_users"
});
if (!localUsersTableExists && TableExists(connection, "Users"))
{
TryMigrateToLocalUsersTable(connection);
}
RemoveEmptyPasswordHashes(connection);
}
}
private void TryMigrateToLocalUsersTable(ManagedConnection connection)
{
try
{
connection.RunQueries(new[]
{
"INSERT INTO LocalUsersv2 (guid, data) SELECT guid,data from users"
});
}
catch (Exception ex)
{
Logger.LogError(ex, "Error migrating users database");
}
}
private void RemoveEmptyPasswordHashes(ManagedConnection connection)
{
foreach (var user in RetrieveAllUsers(connection))
{
// If the user password is the sha1 hash of the empty string, remove it
if (!string.Equals(user.Password, "DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal)
&& !string.Equals(user.Password, "$SHA1$DA39A3EE5E6B4B0D3255BFEF95601890AFD80709", StringComparison.Ordinal))
{
continue;
}
user.Password = null;
var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions);
connection.RunInTransaction(db =>
{
using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
{
statement.TryBind("@InternalId", user.InternalId);
statement.TryBind("@data", serialized);
statement.MoveNext();
}
}, TransactionMode);
}
}
/// <summary>
/// Save a user in the repo
/// </summary>
public void CreateUser(User user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions);
using (var connection = GetConnection())
{
connection.RunInTransaction(db =>
{
using (var statement = db.PrepareStatement("insert into LocalUsersv2 (guid, data) values (@guid, @data)"))
{
statement.TryBind("@guid", user.Id.ToByteArray());
statement.TryBind("@data", serialized);
statement.MoveNext();
}
var createdUser = GetUser(user.Id, connection);
if (createdUser == null)
{
throw new ApplicationException("created user should never be null");
}
user.InternalId = createdUser.InternalId;
}, TransactionMode);
}
}
public void UpdateUser(User user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var serialized = JsonSerializer.SerializeToUtf8Bytes(user, _jsonOptions);
using (var connection = GetConnection())
{
connection.RunInTransaction(db =>
{
using (var statement = db.PrepareStatement("update LocalUsersv2 set data=@data where Id=@InternalId"))
{
statement.TryBind("@InternalId", user.InternalId);
statement.TryBind("@data", serialized);
statement.MoveNext();
}
}, TransactionMode);
}
}
private User GetUser(Guid guid, ManagedConnection connection)
{
using (var statement = connection.PrepareStatement("select id,guid,data from LocalUsersv2 where guid=@guid"))
{
statement.TryBind("@guid", guid);
foreach (var row in statement.ExecuteQuery())
{
return GetUser(row);
}
}
return null;
}
private User GetUser(IReadOnlyList<IResultSetValue> row)
{
var id = row[0].ToInt64();
var guid = row[1].ReadGuidFromBlob();
var user = JsonSerializer.Deserialize<User>(row[2].ToBlob(), _jsonOptions);
user.InternalId = id;
user.Id = guid;
return user;
}
/// <summary>
/// Retrieve all users from the database
/// </summary>
/// <returns>IEnumerable{User}.</returns>
public List<User> RetrieveAllUsers()
{
using (var connection = GetConnection(true))
{
return new List<User>(RetrieveAllUsers(connection));
}
}
/// <summary>
/// Retrieve all users from the database
/// </summary>
/// <returns>IEnumerable{User}.</returns>
private IEnumerable<User> RetrieveAllUsers(ManagedConnection connection)
{
foreach (var row in connection.Query("select id,guid,data from LocalUsersv2"))
{
yield return GetUser(row);
}
}
/// <summary>
/// Deletes the user.
/// </summary>
/// <param name="user">The user.</param>
/// <returns>Task.</returns>
/// <exception cref="ArgumentNullException">user</exception>
public void DeleteUser(User user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
using (var connection = GetConnection())
{
connection.RunInTransaction(db =>
{
using (var statement = db.PrepareStatement("delete from LocalUsersv2 where Id=@id"))
{
statement.TryBind("@id", user.InternalId);
statement.MoveNext();
}
}, TransactionMode);
}
}
}
}

@ -5,10 +5,11 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using Jellyfin.Data.Enums;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Devices;
@ -16,7 +17,6 @@ using MediaBrowser.Model.Events;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.Users;
namespace Emby.Server.Implementations.Devices
{
@ -27,11 +27,10 @@ namespace Emby.Server.Implementations.Devices
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;
private readonly object _capabilitiesSyncLock = new object();
public DeviceManager(
IAuthenticationRepository authRepo,
IJsonSerializer json,
@ -175,7 +174,12 @@ namespace Emby.Server.Implementations.Devices
throw new ArgumentNullException(nameof(deviceId));
}
if (!CanAccessDevice(user.Policy, deviceId))
if (user.HasPermission(PermissionKind.EnableAllDevices) || user.HasPermission(PermissionKind.IsAdministrator))
{
return true;
}
if (!user.GetPreference(PreferenceKind.EnabledDevices).Contains(deviceId, StringComparer.OrdinalIgnoreCase))
{
var capabilities = GetCapabilities(deviceId);
@ -187,20 +191,5 @@ namespace Emby.Server.Implementations.Devices
return true;
}
private static bool CanAccessDevice(UserPolicy policy, string id)
{
if (policy.EnableAllDevices)
{
return true;
}
if (policy.IsAdministrator)
{
return true;
}
return policy.EnabledDevices.Contains(id, StringComparer.OrdinalIgnoreCase);
}
}
}

@ -6,14 +6,14 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
@ -24,6 +24,14 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Microsoft.Extensions.Logging;
using Book = MediaBrowser.Controller.Entities.Book;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
using Person = MediaBrowser.Controller.Entities.Person;
using Photo = MediaBrowser.Controller.Entities.Photo;
using Season = MediaBrowser.Controller.Entities.TV.Season;
using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace Emby.Server.Implementations.Dto
{
@ -384,7 +392,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.ChildCount))
{
dto.ChildCount = dto.ChildCount ?? GetChildCount(folder, user);
dto.ChildCount ??= GetChildCount(folder, user);
}
}
@ -414,7 +422,7 @@ namespace Emby.Server.Implementations.Dto
if (options.ContainsField(ItemFields.BasicSyncInfo))
{
var userCanSync = user != null && user.Policy.EnableContentDownloading;
var userCanSync = user != null && user.HasPermission(PermissionKind.EnableContentDownloading);
if (userCanSync && item.SupportsExternalTransfer)
{
dto.SupportsSync = true;

@ -6,6 +6,7 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;

@ -4,6 +4,7 @@ using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Plugins;
@ -64,7 +65,7 @@ namespace Emby.Server.Implementations.EntryPoints
private async Task SendMessage(string name, TimerEventInfo info)
{
var users = _userManager.Users.Where(i => i.Policy.EnableLiveTvAccess).Select(i => i.Id).ToList();
var users = _userManager.Users.Where(i => i.HasPermission(PermissionKind.EnableLiveTvAccess)).Select(i => i.Id).ToList();
try
{

@ -1,77 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Tasks;
namespace Emby.Server.Implementations.EntryPoints
{
/// <summary>
/// Class RefreshUsersMetadata.
/// </summary>
public class RefreshUsersMetadata : IScheduledTask, IConfigurableScheduledTask
{
/// <summary>
/// The user manager.
/// </summary>
private readonly IUserManager _userManager;
private readonly IFileSystem _fileSystem;
/// <summary>
/// Initializes a new instance of the <see cref="RefreshUsersMetadata" /> class.
/// </summary>
public RefreshUsersMetadata(IUserManager userManager, IFileSystem fileSystem)
{
_userManager = userManager;
_fileSystem = fileSystem;
}
/// <inheritdoc />
public string Name => "Refresh Users";
/// <inheritdoc />
public string Key => "RefreshUsers";
/// <inheritdoc />
public string Description => "Refresh user infos";
/// <inheritdoc />
public string Category => "Library";
/// <inheritdoc />
public bool IsHidden => true;
/// <inheritdoc />
public bool IsEnabled => true;
/// <inheritdoc />
public bool IsLogged => true;
/// <inheritdoc />
public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress)
{
foreach (var user in _userManager.Users)
{
cancellationToken.ThrowIfCancellationRequested();
await user.RefreshMetadata(new MetadataRefreshOptions(new DirectoryService(_fileSystem)), cancellationToken).ConfigureAwait(false);
}
}
/// <inheritdoc />
public IEnumerable<TaskTriggerInfo> GetDefaultTriggers()
{
return new[]
{
new TaskTriggerInfo
{
IntervalTicks = TimeSpan.FromDays(1).Ticks,
Type = TaskTriggerInfo.TriggerInterval
}
};
}
}
}

@ -3,10 +3,10 @@ using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Session;
@ -68,10 +68,8 @@ namespace Emby.Server.Implementations.EntryPoints
/// <inheritdoc />
public Task RunAsync()
{
_userManager.UserDeleted += OnUserDeleted;
_userManager.UserUpdated += OnUserUpdated;
_userManager.UserPolicyUpdated += OnUserPolicyUpdated;
_userManager.UserConfigurationUpdated += OnUserConfigurationUpdated;
_userManager.OnUserDeleted += OnUserDeleted;
_userManager.OnUserUpdated += OnUserUpdated;
_appHost.HasPendingRestartChanged += OnHasPendingRestartChanged;
@ -153,20 +151,6 @@ namespace Emby.Server.Implementations.EntryPoints
await SendMessageToUserSession(e.Argument, "UserDeleted", e.Argument.Id.ToString("N", CultureInfo.InvariantCulture)).ConfigureAwait(false);
}
private async void OnUserPolicyUpdated(object sender, GenericEventArgs<User> e)
{
var dto = _userManager.GetUserDto(e.Argument);
await SendMessageToUserSession(e.Argument, "UserPolicyUpdated", dto).ConfigureAwait(false);
}
private async void OnUserConfigurationUpdated(object sender, GenericEventArgs<User> e)
{
var dto = _userManager.GetUserDto(e.Argument);
await SendMessageToUserSession(e.Argument, "UserConfigurationUpdated", dto).ConfigureAwait(false);
}
private async Task SendMessageToAdminSessions<T>(string name, T data)
{
try
@ -210,10 +194,8 @@ namespace Emby.Server.Implementations.EntryPoints
{
if (dispose)
{
_userManager.UserDeleted -= OnUserDeleted;
_userManager.UserUpdated -= OnUserUpdated;
_userManager.UserPolicyUpdated -= OnUserPolicyUpdated;
_userManager.UserConfigurationUpdated -= OnUserConfigurationUpdated;
_userManager.OnUserDeleted -= OnUserDeleted;
_userManager.OnUserUpdated -= OnUserUpdated;
_installationManager.PluginUninstalled -= OnPluginUninstalled;
_installationManager.PackageInstalling -= OnPackageInstalling;

@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using MediaBrowser.Controller.Net;

@ -3,10 +3,11 @@
using System;
using System.Linq;
using Emby.Server.Implementations.SocketSharp;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
@ -90,7 +91,8 @@ namespace Emby.Server.Implementations.HttpServer.Security
!string.IsNullOrEmpty(auth.Client) &&
!string.IsNullOrEmpty(auth.Device))
{
_sessionManager.LogSessionActivity(auth.Client,
_sessionManager.LogSessionActivity(
auth.Client,
auth.Version,
auth.DeviceId,
auth.Device,
@ -104,21 +106,21 @@ namespace Emby.Server.Implementations.HttpServer.Security
private void ValidateUserAccess(
User user,
IRequest request,
IAuthenticationAttributes authAttribtues,
IAuthenticationAttributes authAttributes,
AuthorizationInfo auth)
{
if (user.Policy.IsDisabled)
if (user.HasPermission(PermissionKind.IsDisabled))
{
throw new SecurityException("User account has been disabled.");
}
if (!user.Policy.EnableRemoteAccess && !_networkManager.IsInLocalNetwork(request.RemoteIp))
if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !_networkManager.IsInLocalNetwork(request.RemoteIp))
{
throw new SecurityException("User account has been disabled.");
}
if (!user.Policy.IsAdministrator
&& !authAttribtues.EscapeParentalControl
if (!user.HasPermission(PermissionKind.IsAdministrator)
&& !authAttributes.EscapeParentalControl
&& !user.IsParentalScheduleAllowed())
{
request.Response.Headers.Add("X-Application-Error-Code", "ParentalControl");
@ -186,7 +188,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
{
if (roles.Contains("admin", StringComparer.OrdinalIgnoreCase))
{
if (user == null || !user.Policy.IsAdministrator)
if (user == null || !user.HasPermission(PermissionKind.IsAdministrator))
{
throw new SecurityException("User does not have admin access.");
}
@ -194,7 +196,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (roles.Contains("delete", StringComparer.OrdinalIgnoreCase))
{
if (user == null || !user.Policy.EnableContentDeletion)
if (user == null || !user.HasPermission(PermissionKind.EnableContentDeletion))
{
throw new SecurityException("User does not have delete access.");
}
@ -202,7 +204,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (roles.Contains("download", StringComparer.OrdinalIgnoreCase))
{
if (user == null || !user.Policy.EnableContentDownloading)
if (user == null || !user.HasPermission(PermissionKind.EnableContentDownloading))
{
throw new SecurityException("User does not have download access.");
}

@ -149,9 +149,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
{
info.User = _userManager.GetUserById(tokenInfo.UserId);
if (info.User != null && !string.Equals(info.User.Name, tokenInfo.UserName, StringComparison.OrdinalIgnoreCase))
if (info.User != null && !string.Equals(info.User.Username, tokenInfo.UserName, StringComparison.OrdinalIgnoreCase))
{
tokenInfo.UserName = info.User.Name;
tokenInfo.UserName = info.User.Username;
updateToken = true;
}
}

@ -1,7 +1,7 @@
#pragma warning disable CS1591
using System;
using MediaBrowser.Controller.Entities;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;

@ -234,10 +234,12 @@ namespace Emby.Server.Implementations.HttpServer
private Task SendKeepAliveResponse()
{
LastKeepAliveDate = DateTime.UtcNow;
return SendAsync(new WebSocketMessage<string>
{
MessageType = "KeepAlive"
}, CancellationToken.None);
return SendAsync(
new WebSocketMessage<string>
{
MessageId = Guid.NewGuid(),
MessageType = "KeepAlive"
}, CancellationToken.None);
}
/// <inheritdoc />

@ -17,6 +17,8 @@ using Emby.Server.Implementations.Library.Resolvers;
using Emby.Server.Implementations.Library.Validators;
using Emby.Server.Implementations.Playlists;
using Emby.Server.Implementations.ScheduledTasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller;
@ -25,7 +27,6 @@ using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.IO;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
@ -46,6 +47,9 @@ using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Tasks;
using MediaBrowser.Providers.MediaInfo;
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;
@ -1539,7 +1543,8 @@ namespace Emby.Server.Implementations.Library
}
// Handle grouping
if (user != null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType) && user.Configuration.GroupedFolders.Length > 0)
if (user != null && !string.IsNullOrEmpty(view.ViewType) && UserView.IsEligibleForGrouping(view.ViewType)
&& user.GetPreference(PreferenceKind.GroupedFolders).Length > 0)
{
return GetUserRootFolder()
.GetChildren(user, true)

@ -7,6 +7,8 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Entities;
@ -14,7 +16,6 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Persistence;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
@ -190,10 +191,7 @@ namespace Emby.Server.Implementations.Library
{
if (string.Equals(item.MediaType, MediaType.Audio, StringComparison.OrdinalIgnoreCase))
{
if (!user.Policy.EnableAudioPlaybackTranscoding)
{
source.SupportsTranscoding = false;
}
source.SupportsTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding);
}
}
}
@ -352,7 +350,9 @@ namespace Emby.Server.Implementations.Library
private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
{
if (userData.SubtitleStreamIndex.HasValue && user.Configuration.RememberSubtitleSelections && user.Configuration.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection)
if (userData.SubtitleStreamIndex.HasValue
&& user.RememberSubtitleSelections
&& user.SubtitleMode != SubtitlePlaybackMode.None && allowRememberingSelection)
{
var index = userData.SubtitleStreamIndex.Value;
// Make sure the saved index is still valid
@ -363,26 +363,27 @@ namespace Emby.Server.Implementations.Library
}
}
var preferredSubs = string.IsNullOrEmpty(user.Configuration.SubtitleLanguagePreference)
? Array.Empty<string>() : NormalizeLanguage(user.Configuration.SubtitleLanguagePreference);
var preferredSubs = string.IsNullOrEmpty(user.SubtitleLanguagePreference)
? Array.Empty<string>() : NormalizeLanguage(user.SubtitleLanguagePreference);
var defaultAudioIndex = source.DefaultAudioStreamIndex;
var audioLangage = defaultAudioIndex == null
? null
: source.MediaStreams.Where(i => i.Type == MediaStreamType.Audio && i.Index == defaultAudioIndex).Select(i => i.Language).FirstOrDefault();
source.DefaultSubtitleStreamIndex = MediaStreamSelector.GetDefaultSubtitleStreamIndex(source.MediaStreams,
source.DefaultSubtitleStreamIndex = MediaStreamSelector.GetDefaultSubtitleStreamIndex(
source.MediaStreams,
preferredSubs,
user.Configuration.SubtitleMode,
user.SubtitleMode,
audioLangage);
MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs,
user.Configuration.SubtitleMode, audioLangage);
MediaStreamSelector.SetSubtitleStreamScores(source.MediaStreams, preferredSubs, user.SubtitleMode, audioLangage);
}
private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
{
if (userData.AudioStreamIndex.HasValue && user.Configuration.RememberAudioSelections && allowRememberingSelection)
if (userData.AudioStreamIndex.HasValue && user.RememberAudioSelections && allowRememberingSelection)
{
var index = userData.AudioStreamIndex.Value;
// Make sure the saved index is still valid
@ -393,11 +394,11 @@ namespace Emby.Server.Implementations.Library
}
}
var preferredAudio = string.IsNullOrEmpty(user.Configuration.AudioLanguagePreference)
var preferredAudio = string.IsNullOrEmpty(user.AudioLanguagePreference)
? Array.Empty<string>()
: NormalizeLanguage(user.Configuration.AudioLanguagePreference);
: NormalizeLanguage(user.AudioLanguagePreference);
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.Configuration.PlayDefaultAudioTrack);
source.DefaultAudioStreamIndex = MediaStreamSelector.GetDefaultAudioStreamIndex(source.MediaStreams, preferredAudio, user.PlayDefaultAudioTrack);
}
public void SetDefaultAudioAndSubtitleStreamIndexes(BaseItem item, MediaSourceInfo source, User user)
@ -534,7 +535,7 @@ namespace Emby.Server.Implementations.Library
mediaSource.RunTimeTicks = null;
}
var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Audio);
var audioStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Audio);
if (audioStream == null || audioStream.Index == -1)
{
@ -545,7 +546,7 @@ namespace Emby.Server.Implementations.Library
mediaSource.DefaultAudioStreamIndex = audioStream.Index;
}
var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaBrowser.Model.Entities.MediaStreamType.Video);
var videoStream = mediaSource.MediaStreams.FirstOrDefault(i => i.Type == MediaStreamType.Video);
if (videoStream != null)
{
if (!videoStream.BitRate.HasValue)
@ -556,17 +557,14 @@ namespace Emby.Server.Implementations.Library
{
videoStream.BitRate = 30000000;
}
else if (width >= 1900)
{
videoStream.BitRate = 20000000;
}
else if (width >= 1200)
{
videoStream.BitRate = 8000000;
}
else if (width >= 700)
{
videoStream.BitRate = 2000000;
@ -670,13 +668,14 @@ namespace Emby.Server.Implementations.Library
mediaSource.AnalyzeDurationMs = 3000;
}
mediaInfo = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest
mediaInfo = await _mediaEncoder.GetMediaInfo(
new MediaInfoRequest
{
MediaSource = mediaSource,
MediaType = isAudio ? DlnaProfileType.Audio : DlnaProfileType.Video,
ExtractChapters = false
}, cancellationToken).ConfigureAwait(false);
},
cancellationToken).ConfigureAwait(false);
if (cacheFilePath != null)
{

@ -3,7 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using MediaBrowser.Model.Configuration;
using Jellyfin.Data.Enums;
using MediaBrowser.Model.Entities;
namespace Emby.Server.Implementations.Library

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@ -10,6 +11,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
namespace Emby.Server.Implementations.Library
{
@ -75,7 +77,6 @@ namespace Emby.Server.Implementations.Library
{
return Guid.Empty;
}
}).Where(i => !i.Equals(Guid.Empty)).ToArray();
return GetInstantMixFromGenreIds(genreIds, user, dtoOptions);
@ -105,32 +106,27 @@ namespace Emby.Server.Implementations.Library
return GetInstantMixFromGenreIds(new[] { item.Id }, user, dtoOptions);
}
var playlist = item as Playlist;
if (playlist != null)
if (item is Playlist playlist)
{
return GetInstantMixFromPlaylist(playlist, user, dtoOptions);
}
var album = item as MusicAlbum;
if (album != null)
if (item is MusicAlbum album)
{
return GetInstantMixFromAlbum(album, user, dtoOptions);
}
var artist = item as MusicArtist;
if (artist != null)
if (item is MusicArtist artist)
{
return GetInstantMixFromArtist(artist, user, dtoOptions);
}
var song = item as Audio;
if (song != null)
if (item is Audio song)
{
return GetInstantMixFromSong(song, user, dtoOptions);
}
var folder = item as Folder;
if (folder != null)
if (item is Folder folder)
{
return GetInstantMixFromFolder(folder, user, dtoOptions);
}

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@ -12,6 +13,8 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Search;
using Microsoft.Extensions.Logging;
using Genre = MediaBrowser.Controller.Entities.Genre;
using Person = MediaBrowser.Controller.Entities.Person;
namespace Emby.Server.Implementations.Library
{

@ -5,6 +5,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@ -13,6 +14,7 @@ using MediaBrowser.Controller.Persistence;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using Microsoft.Extensions.Logging;
using Book = MediaBrowser.Controller.Entities.Book;
namespace Emby.Server.Implementations.Library
{

File diff suppressed because it is too large Load Diff

@ -5,6 +5,8 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
@ -17,6 +19,8 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
using Genre = MediaBrowser.Controller.Entities.Genre;
using Person = MediaBrowser.Controller.Entities.Person;
namespace Emby.Server.Implementations.Library
{
@ -125,12 +129,12 @@ namespace Emby.Server.Implementations.Library
if (!query.IncludeHidden)
{
list = list.Where(i => !user.Configuration.MyMediaExcludes.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))).ToList();
list = list.Where(i => !user.GetPreference(PreferenceKind.MyMediaExcludes).Contains(i.Id.ToString("N", CultureInfo.InvariantCulture))).ToList();
}
var sorted = _libraryManager.Sort(list, user, new[] { ItemSortBy.SortName }, SortOrder.Ascending).ToList();
var orders = user.Configuration.OrderedViews.ToList();
var orders = user.GetPreference(PreferenceKind.OrderedViews).ToList();
return list
.OrderBy(i =>
@ -165,7 +169,13 @@ namespace Emby.Server.Implementations.Library
return GetUserSubViewWithName(name, parentId, type, sortName);
}
private Folder GetUserView(List<ICollectionFolder> parents, string viewType, string localizationKey, string sortName, User user, string[] presetViews)
private Folder GetUserView(
List<ICollectionFolder> parents,
string viewType,
string localizationKey,
string sortName,
Jellyfin.Data.Entities.User user,
string[] presetViews)
{
if (parents.Count == 1 && parents.All(i => string.Equals(i.CollectionType, viewType, StringComparison.OrdinalIgnoreCase)))
{
@ -270,7 +280,8 @@ namespace Emby.Server.Implementations.Library
{
parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder)
.Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
.Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes)
.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
.ToList();
}
@ -331,12 +342,11 @@ namespace Emby.Server.Implementations.Library
var excludeItemTypes = includeItemTypes.Length == 0 && mediaTypes.Count == 0 ? new[]
{
typeof(Person).Name,
typeof(Studio).Name,
typeof(Year).Name,
typeof(MusicGenre).Name,
typeof(Genre).Name
nameof(Person),
nameof(Studio),
nameof(Year),
nameof(MusicGenre),
nameof(Genre)
} : Array.Empty<string>();
var query = new InternalItemsQuery(user)

@ -7,6 +7,8 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Emby.Server.Implementations.Library;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
@ -14,8 +16,6 @@ using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Persistence;
@ -31,6 +31,8 @@ using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Tasks;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
namespace Emby.Server.Implementations.LiveTv
{
@ -696,7 +698,6 @@ namespace Emby.Server.Implementations.LiveTv
{
Path = info.ThumbImageUrl,
Type = ImageType.Thumb
}, 0);
}
}
@ -709,7 +710,6 @@ namespace Emby.Server.Implementations.LiveTv
{
Path = info.LogoImageUrl,
Type = ImageType.Logo
}, 0);
}
}
@ -722,7 +722,6 @@ namespace Emby.Server.Implementations.LiveTv
{
Path = info.BackdropImageUrl,
Type = ImageType.Backdrop
}, 0);
}
}
@ -760,7 +759,8 @@ namespace Emby.Server.Implementations.LiveTv
var dto = _dtoService.GetBaseItemDto(program, new DtoOptions(), user);
var list = new List<Tuple<BaseItemDto, string, string>>() {
var list = new List<Tuple<BaseItemDto, string, string>>
{
new Tuple<BaseItemDto, string, string>(dto, program.ExternalId, program.ExternalSeriesId)
};
@ -2167,20 +2167,19 @@ namespace Emby.Server.Implementations.LiveTv
var info = new LiveTvInfo
{
Services = services,
IsEnabled = services.Length > 0
IsEnabled = services.Length > 0,
EnabledUsers = _userManager.Users
.Where(IsLiveTvEnabled)
.Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
.ToArray()
};
info.EnabledUsers = _userManager.Users
.Where(IsLiveTvEnabled)
.Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
.ToArray();
return info;
}
private bool IsLiveTvEnabled(User user)
{
return user.Policy.EnableLiveTvAccess && (Services.Count > 1 || GetConfiguration().TunerHosts.Length > 0);
return user.HasPermission(PermissionKind.EnableLiveTvAccess) && (Services.Count > 1 || GetConfiguration().TunerHosts.Length > 0);
}
public IEnumerable<User> GetEnabledUsers()

@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Querying;
@ -44,7 +45,7 @@ namespace Emby.Server.Implementations.Playlists
}
query.Recursive = true;
query.IncludeItemTypes = new string[] { "Playlist" };
query.IncludeItemTypes = new[] { "Playlist" };
query.Parent = null;
return LibraryManager.GetItemsResult(query);
}

@ -7,6 +7,7 @@ using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
@ -21,6 +22,8 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using PlaylistsNET.Content;
using PlaylistsNET.Models;
using Genre = MediaBrowser.Controller.Entities.Genre;
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
namespace Emby.Server.Implementations.Playlists
{

@ -7,6 +7,8 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Events;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller;
@ -15,7 +17,6 @@ using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
@ -28,7 +29,9 @@ using MediaBrowser.Model.Library;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Session;
using MediaBrowser.Model.SyncPlay;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
namespace Emby.Server.Implementations.Session
{
@ -283,11 +286,18 @@ namespace Emby.Server.Implementations.Session
if (user != null)
{
var userLastActivityDate = user.LastActivityDate ?? DateTime.MinValue;
user.LastActivityDate = activityDate;
if ((activityDate - userLastActivityDate).TotalSeconds > 60)
{
_userManager.UpdateUser(user);
try
{
user.LastActivityDate = activityDate;
_userManager.UpdateUser(user);
}
catch (DbUpdateConcurrencyException e)
{
_logger.LogWarning(e, "Error updating user's last activity date.");
}
}
}
@ -434,7 +444,13 @@ namespace Emby.Server.Implementations.Session
/// <param name="remoteEndPoint">The remote end point.</param>
/// <param name="user">The user.</param>
/// <returns>SessionInfo.</returns>
private SessionInfo GetSessionInfo(string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user)
private SessionInfo GetSessionInfo(
string appName,
string appVersion,
string deviceId,
string deviceName,
string remoteEndPoint,
User user)
{
CheckDisposed();
@ -447,14 +463,13 @@ namespace Emby.Server.Implementations.Session
CheckDisposed();
var sessionInfo = _activeConnections.GetOrAdd(key, k =>
{
return CreateSession(k, appName, appVersion, deviceId, deviceName, remoteEndPoint, user);
});
var sessionInfo = _activeConnections.GetOrAdd(
key,
k => CreateSession(k, appName, appVersion, deviceId, deviceName, remoteEndPoint, user));
sessionInfo.UserId = user == null ? Guid.Empty : user.Id;
sessionInfo.UserName = user?.Name;
sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user, ImageType.Primary);
sessionInfo.UserId = user?.Id ?? Guid.Empty;
sessionInfo.UserName = user?.Username;
sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user);
sessionInfo.RemoteEndPoint = remoteEndPoint;
sessionInfo.Client = appName;
@ -473,7 +488,14 @@ namespace Emby.Server.Implementations.Session
return sessionInfo;
}
private SessionInfo CreateSession(string key, string appName, string appVersion, string deviceId, string deviceName, string remoteEndPoint, User user)
private SessionInfo CreateSession(
string key,
string appName,
string appVersion,
string deviceId,
string deviceName,
string remoteEndPoint,
User user)
{
var sessionInfo = new SessionInfo(this, _logger)
{
@ -483,11 +505,11 @@ namespace Emby.Server.Implementations.Session
Id = key.GetMD5().ToString("N", CultureInfo.InvariantCulture)
};
var username = user?.Name;
var username = user?.Username;
sessionInfo.UserId = user?.Id ?? Guid.Empty;
sessionInfo.UserName = username;
sessionInfo.UserPrimaryImageTag = user == null ? null : GetImageCacheTag(user, ImageType.Primary);
sessionInfo.UserPrimaryImageTag = user?.ProfileImage == null ? null : GetImageCacheTag(user);
sessionInfo.RemoteEndPoint = remoteEndPoint;
if (string.IsNullOrEmpty(deviceName))
@ -535,10 +557,7 @@ namespace Emby.Server.Implementations.Session
private void StartIdleCheckTimer()
{
if (_idleTimer == null)
{
_idleTimer = new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
}
_idleTimer ??= new Timer(CheckForIdlePlayback, null, TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
}
private void StopIdleCheckTimer()
@ -786,7 +805,7 @@ namespace Emby.Server.Implementations.Session
{
var changed = false;
if (user.Configuration.RememberAudioSelections)
if (user.RememberAudioSelections)
{
if (data.AudioStreamIndex != info.AudioStreamIndex)
{
@ -803,7 +822,7 @@ namespace Emby.Server.Implementations.Session
}
}
if (user.Configuration.RememberSubtitleSelections)
if (user.RememberSubtitleSelections)
{
if (data.SubtitleStreamIndex != info.SubtitleStreamIndex)
{
@ -1114,13 +1133,13 @@ namespace Emby.Server.Implementations.Session
if (items.Any(i => i.GetPlayAccess(user) != PlayAccess.Full))
{
throw new ArgumentException(
string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Name));
string.Format(CultureInfo.InvariantCulture, "{0} is not allowed to play media.", user.Username));
}
}
if (user != null
&& command.ItemIds.Length == 1
&& user.Configuration.EnableNextEpisodeAutoPlay
&& user.EnableNextEpisodeAutoPlay
&& _libraryManager.GetItemById(command.ItemIds[0]) is Episode episode)
{
var series = episode.Series;
@ -1191,7 +1210,7 @@ namespace Emby.Server.Implementations.Session
DtoOptions = new DtoOptions(false)
{
EnableImages = false,
Fields = new ItemFields[]
Fields = new[]
{
ItemFields.SortName
}
@ -1353,7 +1372,7 @@ namespace Emby.Server.Implementations.Session
list.Add(new SessionUserInfo
{
UserId = userId,
UserName = user.Name
UserName = user.Username
});
session.AdditionalUsers = list.ToArray();
@ -1513,7 +1532,7 @@ namespace Emby.Server.Implementations.Session
DeviceName = deviceName,
UserId = user.Id,
AccessToken = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture),
UserName = user.Name
UserName = user.Username
};
_logger.LogInformation("Creating new access token for user {0}", user.Id);
@ -1710,15 +1729,15 @@ namespace Emby.Server.Implementations.Session
return info;
}
private string GetImageCacheTag(BaseItem item, ImageType type)
private string GetImageCacheTag(User user)
{
try
{
return _imageProcessor.GetImageCacheTag(item, type);
return _imageProcessor.GetImageCacheTag(user);
}
catch (Exception ex)
catch (Exception e)
{
_logger.LogError(ex, "Error getting image information for {Type}", type);
_logger.LogError(e, "Error getting image information for profile image");
return null;
}
}
@ -1827,7 +1846,10 @@ namespace Emby.Server.Implementations.Session
{
CheckDisposed();
var adminUserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToList();
var adminUserIds = _userManager.Users
.Where(i => i.HasPermission(PermissionKind.IsAdministrator))
.Select(i => i.Id)
.ToList();
return SendMessageToUserSessions(adminUserIds, name, data, cancellationToken);
}

@ -1,6 +1,7 @@
#pragma warning disable CS1591
using System;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;

@ -1,4 +1,5 @@
using System;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;

@ -1,5 +1,6 @@
#pragma warning disable CS1591
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;

@ -1,5 +1,6 @@
#pragma warning disable CS1591
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;

@ -1,5 +1,6 @@
#pragma warning disable CS1591
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;

@ -1,3 +1,4 @@
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;

@ -3,13 +3,13 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using Microsoft.Extensions.Logging;
using MediaBrowser.Controller.Entities;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Session;
using MediaBrowser.Controller.SyncPlay;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.SyncPlay;
using Microsoft.Extensions.Logging;
namespace Emby.Server.Implementations.SyncPlay
{
@ -109,14 +109,6 @@ namespace Emby.Server.Implementations.SyncPlay
_disposed = true;
}
private void CheckDisposed()
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().Name);
}
}
private void OnSessionManagerSessionEnded(object sender, SessionEventArgs e)
{
var session = e.SessionInfo;
@ -149,38 +141,24 @@ namespace Emby.Server.Implementations.SyncPlay
var item = _libraryManager.GetItemById(itemId);
// Check ParentalRating access
var hasParentalRatingAccess = true;
if (user.Policy.MaxParentalRating.HasValue)
{
hasParentalRatingAccess = item.InheritedParentalRatingValue <= user.Policy.MaxParentalRating;
}
var hasParentalRatingAccess = !user.MaxParentalAgeRating.HasValue
|| item.InheritedParentalRatingValue <= user.MaxParentalAgeRating;
if (!user.Policy.EnableAllFolders && hasParentalRatingAccess)
if (!user.HasPermission(PermissionKind.EnableAllFolders) && hasParentalRatingAccess)
{
var collections = _libraryManager.GetCollectionFolders(item).Select(
folder => folder.Id.ToString("N", CultureInfo.InvariantCulture)
);
var intersect = collections.Intersect(user.Policy.EnabledFolders);
return intersect.Any();
}
else
{
return hasParentalRatingAccess;
folder => folder.Id.ToString("N", CultureInfo.InvariantCulture));
return collections.Intersect(user.GetPreference(PreferenceKind.EnabledFolders)).Any();
}
return hasParentalRatingAccess;
}
private Guid? GetSessionGroup(SessionInfo session)
{
ISyncPlayController group;
_sessionToGroupMap.TryGetValue(session.Id, out group);
if (group != null)
{
return group.GetGroupId();
}
else
{
return null;
}
_sessionToGroupMap.TryGetValue(session.Id, out var group);
return group?.GetGroupId();
}
/// <inheritdoc />
@ -188,7 +166,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
var user = _userManager.GetUserById(session.UserId);
if (user.Policy.SyncPlayAccess != SyncPlayAccess.CreateAndJoinGroups)
if (user.SyncPlayAccess != SyncPlayAccess.CreateAndJoinGroups)
{
_logger.LogWarning("NewGroup: {0} does not have permission to create groups.", session.Id);
@ -196,7 +174,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.CreateGroupDenied
};
_sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@ -219,7 +197,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
var user = _userManager.GetUserById(session.UserId);
if (user.Policy.SyncPlayAccess == SyncPlayAccess.None)
if (user.SyncPlayAccess == SyncPlayAccess.None)
{
_logger.LogWarning("JoinGroup: {0} does not have access to SyncPlay.", session.Id);
@ -227,7 +205,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.JoinGroupDenied
};
_sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@ -244,7 +222,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.GroupDoesNotExist
};
_sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@ -257,7 +235,7 @@ namespace Emby.Server.Implementations.SyncPlay
GroupId = group.GetGroupId().ToString(),
Type = GroupUpdateType.LibraryAccessDenied
};
_sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@ -281,8 +259,7 @@ namespace Emby.Server.Implementations.SyncPlay
// TODO: determine what happens to users that are in a group and get their permissions revoked
lock (_groupsLock)
{
ISyncPlayController group;
_sessionToGroupMap.TryGetValue(session.Id, out group);
_sessionToGroupMap.TryGetValue(session.Id, out var group);
if (group == null)
{
@ -292,7 +269,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.NotInGroup
};
_sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@ -311,7 +288,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
var user = _userManager.GetUserById(session.UserId);
if (user.Policy.SyncPlayAccess == SyncPlayAccess.None)
if (user.SyncPlayAccess == SyncPlayAccess.None)
{
return new List<GroupInfoView>();
}
@ -341,7 +318,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
var user = _userManager.GetUserById(session.UserId);
if (user.Policy.SyncPlayAccess == SyncPlayAccess.None)
if (user.SyncPlayAccess == SyncPlayAccess.None)
{
_logger.LogWarning("HandleRequest: {0} does not have access to SyncPlay.", session.Id);
@ -349,14 +326,13 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.JoinGroupDenied
};
_sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
lock (_groupsLock)
{
ISyncPlayController group;
_sessionToGroupMap.TryGetValue(session.Id, out group);
_sessionToGroupMap.TryGetValue(session.Id, out var group);
if (group == null)
{
@ -366,7 +342,7 @@ namespace Emby.Server.Implementations.SyncPlay
{
Type = GroupUpdateType.NotInGroup
};
_sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), error, CancellationToken.None);
_sessionManager.SendSyncPlayGroupUpdate(session.Id, error, CancellationToken.None);
return;
}
@ -393,8 +369,7 @@ namespace Emby.Server.Implementations.SyncPlay
throw new InvalidOperationException("Session not in any group!");
}
ISyncPlayController tempGroup;
_sessionToGroupMap.Remove(session.Id, out tempGroup);
_sessionToGroupMap.Remove(session.Id, out var tempGroup);
if (!tempGroup.GetGroupId().Equals(group.GetGroupId()))
{

@ -4,13 +4,17 @@ using System;
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.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.TV;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace Emby.Server.Implementations.TV
{
@ -73,7 +77,8 @@ namespace Emby.Server.Implementations.TV
{
parents = _libraryManager.GetUserRootFolder().GetChildren(user, true)
.Where(i => i is Folder)
.Where(i => !user.Configuration.LatestItemsExcludes.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
.Where(i => !user.GetPreference(PreferenceKind.LatestItemExcludes)
.Contains(i.Id.ToString("N", CultureInfo.InvariantCulture)))
.ToArray();
}
@ -191,7 +196,7 @@ namespace Emby.Server.Implementations.TV
{
AncestorWithPresentationUniqueKey = null,
SeriesPresentationUniqueKey = seriesKey,
IncludeItemTypes = new[] { typeof(Episode).Name },
IncludeItemTypes = new[] { nameof(Episode) },
OrderBy = new[] { new ValueTuple<string, SortOrder>(ItemSortBy.SortName, SortOrder.Descending) },
IsPlayed = true,
Limit = 1,
@ -204,7 +209,6 @@ namespace Emby.Server.Implementations.TV
},
EnableImages = false
}
}).FirstOrDefault();
Func<Episode> getEpisode = () =>
@ -219,7 +223,7 @@ namespace Emby.Server.Implementations.TV
IsPlayed = false,
IsVirtualItem = false,
ParentIndexNumberNotEquals = 0,
MinSortName = lastWatchedEpisode == null ? null : lastWatchedEpisode.SortName,
MinSortName = lastWatchedEpisode?.SortName,
DtoOptions = dtoOptions
}).Cast<Episode>().FirstOrDefault();

@ -3,6 +3,7 @@ using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Net;
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Logging;
@ -56,10 +57,10 @@ namespace Jellyfin.Api.Auth
var claims = new[]
{
new Claim(ClaimTypes.Name, user.Name),
new Claim(ClaimTypes.Name, user.Username),
new Claim(
ClaimTypes.Role,
value: user.Policy.IsAdministrator ? UserRoles.Administrator : UserRoles.User)
value: user.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User)
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);

@ -113,8 +113,14 @@ namespace Jellyfin.Api.Controllers
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<StartupUserDto> GetFirstUser()
{
// TODO: Remove this method when startup wizard no longer requires an existing user.
_userManager.Initialize();
var user = _userManager.Users.First();
return new StartupUserDto { Name = user.Name, Password = user.Password };
return new StartupUserDto
{
Name = user.Username,
Password = user.Password
};
}
/// <summary>
@ -132,9 +138,9 @@ namespace Jellyfin.Api.Controllers
{
var user = _userManager.Users.First();
user.Name = startupUserDto.Name;
user.Username = startupUserDto.Name;
_userManager.UpdateUser(user);
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
if (!string.IsNullOrEmpty(startupUserDto.Password))
{

@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using Jellyfin.Data.Enums;
namespace Jellyfin.Data
{
public static class DayOfWeekHelper
{
public static List<DayOfWeek> GetDaysOfWeek(DynamicDayOfWeek day)
{
var days = new List<DayOfWeek>(7);
if (day == DynamicDayOfWeek.Sunday
|| day == DynamicDayOfWeek.Weekend
|| day == DynamicDayOfWeek.Everyday)
{
days.Add(DayOfWeek.Sunday);
}
if (day == DynamicDayOfWeek.Monday
|| day == DynamicDayOfWeek.Weekday
|| day == DynamicDayOfWeek.Everyday)
{
days.Add(DayOfWeek.Monday);
}
if (day == DynamicDayOfWeek.Tuesday
|| day == DynamicDayOfWeek.Weekday
|| day == DynamicDayOfWeek.Everyday)
{
days.Add(DayOfWeek.Tuesday);
}
if (day == DynamicDayOfWeek.Wednesday
|| day == DynamicDayOfWeek.Weekday
|| day == DynamicDayOfWeek.Everyday)
{
days.Add(DayOfWeek.Wednesday);
}
if (day == DynamicDayOfWeek.Thursday
|| day == DynamicDayOfWeek.Weekday
|| day == DynamicDayOfWeek.Everyday)
{
days.Add(DayOfWeek.Thursday);
}
if (day == DynamicDayOfWeek.Friday
|| day == DynamicDayOfWeek.Weekday
|| day == DynamicDayOfWeek.Everyday)
{
days.Add(DayOfWeek.Friday);
}
if (day == DynamicDayOfWeek.Saturday
|| day == DynamicDayOfWeek.Weekend
|| day == DynamicDayOfWeek.Everyday)
{
days.Add(DayOfWeek.Saturday);
}
return days;
}
}
}

@ -0,0 +1,91 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
using System.Xml.Serialization;
using Jellyfin.Data.Enums;
namespace Jellyfin.Data.Entities
{
/// <summary>
/// An entity representing a user's access schedule.
/// </summary>
public class AccessSchedule
{
/// <summary>
/// Initializes a new instance of the <see cref="AccessSchedule"/> class.
/// </summary>
/// <param name="dayOfWeek">The day of the week.</param>
/// <param name="startHour">The start hour.</param>
/// <param name="endHour">The end hour.</param>
/// <param name="userId">The associated user's id.</param>
public AccessSchedule(DynamicDayOfWeek dayOfWeek, double startHour, double endHour, Guid userId)
{
UserId = userId;
DayOfWeek = dayOfWeek;
StartHour = startHour;
EndHour = endHour;
}
/// <summary>
/// Initializes a new instance of the <see cref="AccessSchedule"/> class.
/// Default constructor. Protected due to required properties, but present because EF needs it.
/// </summary>
protected AccessSchedule()
{
}
/// <summary>
/// Gets or sets the id of this instance.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[XmlIgnore]
[Key]
[Required]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; protected set; }
/// <summary>
/// Gets or sets the id of the associated user.
/// </summary>
[XmlIgnore]
[Required]
public Guid UserId { get; protected set; }
/// <summary>
/// Gets or sets the day of week.
/// </summary>
/// <value>The day of week.</value>
[Required]
public DynamicDayOfWeek DayOfWeek { get; set; }
/// <summary>
/// Gets or sets the start hour.
/// </summary>
/// <value>The start hour.</value>
[Required]
public double StartHour { get; set; }
/// <summary>
/// Gets or sets the end hour.
/// </summary>
/// <value>The end hour.</value>
[Required]
public double EndHour { get; set; }
/// <summary>
/// Static create function (for use in LINQ queries, etc.)
/// </summary>
/// <param name="dayOfWeek">The day of the week.</param>
/// <param name="startHour">The start hour.</param>
/// <param name="endHour">The end hour.</param>
/// <param name="userId">The associated user's id.</param>
/// <returns>The newly created instance.</returns>
public static AccessSchedule Create(DynamicDayOfWeek dayOfWeek, double startHour, double endHour, Guid userId)
{
return new AccessSchedule(dayOfWeek, startHour, endHour, userId);
}
}
}

@ -2,19 +2,32 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using Jellyfin.Data.Enums;
namespace Jellyfin.Data.Entities
{
public partial class Group
/// <summary>
/// An entity representing a group.
/// </summary>
public partial class Group : IHasPermissions, ISavingChanges
{
partial void Init();
/// <summary>
/// Default constructor. Protected due to required properties, but present because EF needs it.
/// Initializes a new instance of the <see cref="Group"/> class.
/// Public constructor with required data.
/// </summary>
protected Group()
/// <param name="name">The name of the group.</param>
public Group(string name)
{
GroupPermissions = new HashSet<Permission>();
if (string.IsNullOrEmpty(name))
{
throw new ArgumentNullException(nameof(name));
}
Name = name;
Id = Guid.NewGuid();
Permissions = new HashSet<Permission>();
ProviderMappings = new HashSet<ProviderMapping>();
Preferences = new HashSet<Preference>();
@ -22,66 +35,45 @@ namespace Jellyfin.Data.Entities
}
/// <summary>
/// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
/// </summary>
public static Group CreateGroupUnsafe()
{
return new Group();
}
/// <summary>
/// Public constructor with required data
/// Initializes a new instance of the <see cref="Group"/> class.
/// Default constructor. Protected due to required properties, but present because EF needs it.
/// </summary>
/// <param name="name"></param>
/// <param name="_user0"></param>
public Group(string name, User _user0)
protected Group()
{
if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));
this.Name = name;
if (_user0 == null) throw new ArgumentNullException(nameof(_user0));
_user0.Groups.Add(this);
this.GroupPermissions = new HashSet<Permission>();
this.ProviderMappings = new HashSet<ProviderMapping>();
this.Preferences = new HashSet<Preference>();
Init();
}
/// <summary>
/// Static create function (for use in LINQ queries, etc.)
/// </summary>
/// <param name="name"></param>
/// <param name="_user0"></param>
public static Group Create(string name, User _user0)
{
return new Group(name, _user0);
}
/*************************************************************************
* Properties
*************************************************************************/
/// <summary>
/// Identity, Indexed, Required
/// Gets or sets the id of this group.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[Key]
[Required]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; protected set; }
public Guid Id { get; protected set; }
/// <summary>
/// Required, Max length = 255
/// Gets or sets the group's name.
/// </summary>
/// <remarks>
/// Required, Max length = 255.
/// </remarks>
[Required]
[MaxLength(255)]
[StringLength(255)]
public string Name { get; set; }
/// <summary>
/// Required, ConcurrenyToken
/// Gets or sets the row version.
/// </summary>
/// <remarks>
/// Required, Concurrency Token.
/// </remarks>
[ConcurrencyCheck]
[Required]
public uint RowVersion { get; set; }
@ -96,7 +88,7 @@ namespace Jellyfin.Data.Entities
*************************************************************************/
[ForeignKey("Permission_GroupPermissions_Id")]
public virtual ICollection<Permission> GroupPermissions { get; protected set; }
public virtual ICollection<Permission> Permissions { get; protected set; }
[ForeignKey("ProviderMapping_ProviderMappings_Id")]
public virtual ICollection<ProviderMapping> ProviderMappings { get; protected set; }
@ -104,6 +96,27 @@ namespace Jellyfin.Data.Entities
[ForeignKey("Preference_Preferences_Id")]
public virtual ICollection<Preference> Preferences { get; protected set; }
/// <summary>
/// Static create function (for use in LINQ queries, etc.)
/// </summary>
/// <param name="name">The name of this group</param>
public static Group Create(string name)
{
return new Group(name);
}
/// <inheritdoc/>
public bool HasPermission(PermissionKind kind)
{
return Permissions.First(p => p.Kind == kind).Value;
}
/// <inheritdoc/>
public void SetPermission(PermissionKind kind, bool value)
{
Permissions.First(p => p.Kind == kind).Value = value;
}
partial void Init();
}
}

@ -0,0 +1,30 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace Jellyfin.Data.Entities
{
public class ImageInfo
{
public ImageInfo(string path)
{
Path = path;
LastModified = DateTime.UtcNow;
}
[Key]
[Required]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; protected set; }
public Guid? UserId { get; protected set; }
[Required]
[MaxLength(512)]
[StringLength(512)]
public string Path { get; set; }
[Required]
public DateTime LastModified { get; set; }
}
}

@ -1,64 +1,35 @@
using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Runtime.CompilerServices;
using Jellyfin.Data.Enums;
namespace Jellyfin.Data.Entities
{
public partial class Permission
/// <summary>
/// An entity representing whether the associated user has a specific permission.
/// </summary>
public partial class Permission : ISavingChanges
{
partial void Init();
/// <summary>
/// Default constructor. Protected due to required properties, but present because EF needs it.
/// </summary>
protected Permission()
{
Init();
}
/// <summary>
/// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
/// Initializes a new instance of the <see cref="Permission"/> class.
/// Public constructor with required data.
/// </summary>
public static Permission CreatePermissionUnsafe()
/// <param name="kind">The permission kind.</param>
/// <param name="value">The value of this permission.</param>
public Permission(PermissionKind kind, bool value)
{
return new Permission();
}
/// <summary>
/// Public constructor with required data
/// </summary>
/// <param name="kind"></param>
/// <param name="value"></param>
/// <param name="_user0"></param>
/// <param name="_group1"></param>
public Permission(Enums.PermissionKind kind, bool value, User _user0, Group _group1)
{
this.Kind = kind;
this.Value = value;
if (_user0 == null) throw new ArgumentNullException(nameof(_user0));
_user0.Permissions.Add(this);
if (_group1 == null) throw new ArgumentNullException(nameof(_group1));
_group1.GroupPermissions.Add(this);
Kind = kind;
Value = value;
Init();
}
/// <summary>
/// Static create function (for use in LINQ queries, etc.)
/// Initializes a new instance of the <see cref="Permission"/> class.
/// Default constructor. Protected due to required properties, but present because EF needs it.
/// </summary>
/// <param name="kind"></param>
/// <param name="value"></param>
/// <param name="_user0"></param>
/// <param name="_group1"></param>
public static Permission Create(Enums.PermissionKind kind, bool value, User _user0, Group _group1)
protected Permission()
{
return new Permission(kind, value, _user0, _group1);
Init();
}
/*************************************************************************
@ -66,79 +37,61 @@ namespace Jellyfin.Data.Entities
*************************************************************************/
/// <summary>
/// Identity, Indexed, Required
/// Gets or sets the id of this permission.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[Key]
[Required]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; protected set; }
/// <summary>
/// Backing field for Kind
/// </summary>
protected Enums.PermissionKind _Kind;
/// <summary>
/// When provided in a partial class, allows value of Kind to be changed before setting.
/// </summary>
partial void SetKind(Enums.PermissionKind oldValue, ref Enums.PermissionKind newValue);
/// <summary>
/// When provided in a partial class, allows value of Kind to be changed before returning.
/// </summary>
partial void GetKind(ref Enums.PermissionKind result);
/// <summary>
/// Required
/// Gets or sets the type of this permission.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[Required]
public Enums.PermissionKind Kind
{
get
{
Enums.PermissionKind value = _Kind;
GetKind(ref value);
return (_Kind = value);
}
set
{
Enums.PermissionKind oldValue = _Kind;
SetKind(oldValue, ref value);
if (oldValue != value)
{
_Kind = value;
OnPropertyChanged();
}
}
}
public PermissionKind Kind { get; protected set; }
/// <summary>
/// Required
/// Gets or sets a value indicating whether the associated user has this permission.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[Required]
public bool Value { get; set; }
/// <summary>
/// Required, ConcurrenyToken
/// Gets or sets the row version.
/// </summary>
/// <remarks>
/// Required, ConcurrencyToken.
/// </remarks>
[ConcurrencyCheck]
[Required]
public uint RowVersion { get; set; }
public void OnSavingChanges()
/// <summary>
/// Static create function (for use in LINQ queries, etc.)
/// </summary>
/// <param name="kind">The permission kind.</param>
/// <param name="value">The value of this permission.</param>
/// <returns>The newly created instance.</returns>
public static Permission Create(PermissionKind kind, bool value)
{
RowVersion++;
return new Permission(kind, value);
}
/*************************************************************************
* Navigation properties
*************************************************************************/
public virtual event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
/// <inheritdoc/>
public void OnSavingChanges()
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
RowVersion++;
}
partial void Init();
}
}

@ -1,63 +1,33 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Jellyfin.Data.Enums;
namespace Jellyfin.Data.Entities
{
public partial class Preference
/// <summary>
/// An entity representing a preference attached to a user or group.
/// </summary>
public class Preference : ISavingChanges
{
partial void Init();
/// <summary>
/// Default constructor. Protected due to required properties, but present because EF needs it.
/// </summary>
protected Preference()
{
Init();
}
/// <summary>
/// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
/// Initializes a new instance of the <see cref="Preference"/> class.
/// Public constructor with required data.
/// </summary>
public static Preference CreatePreferenceUnsafe()
/// <param name="kind">The preference kind.</param>
/// <param name="value">The value.</param>
public Preference(PreferenceKind kind, string value)
{
return new Preference();
}
/// <summary>
/// Public constructor with required data
/// </summary>
/// <param name="kind"></param>
/// <param name="value"></param>
/// <param name="_user0"></param>
/// <param name="_group1"></param>
public Preference(Enums.PreferenceKind kind, string value, User _user0, Group _group1)
{
this.Kind = kind;
if (string.IsNullOrEmpty(value)) throw new ArgumentNullException(nameof(value));
this.Value = value;
if (_user0 == null) throw new ArgumentNullException(nameof(_user0));
_user0.Preferences.Add(this);
if (_group1 == null) throw new ArgumentNullException(nameof(_group1));
_group1.Preferences.Add(this);
Init();
Kind = kind;
Value = value ?? throw new ArgumentNullException(nameof(value));
}
/// <summary>
/// Static create function (for use in LINQ queries, etc.)
/// Initializes a new instance of the <see cref="Preference"/> class.
/// Default constructor. Protected due to required properties, but present because EF needs it.
/// </summary>
/// <param name="kind"></param>
/// <param name="value"></param>
/// <param name="_user0"></param>
/// <param name="_group1"></param>
public static Preference Create(Enums.PreferenceKind kind, string value, User _user0, Group _group1)
protected Preference()
{
return new Preference(kind, value, _user0, _group1);
}
/*************************************************************************
@ -65,43 +35,61 @@ namespace Jellyfin.Data.Entities
*************************************************************************/
/// <summary>
/// Identity, Indexed, Required
/// Gets or sets the id of this preference.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[Key]
[Required]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; protected set; }
/// <summary>
/// Required
/// Gets or sets the type of this preference.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[Required]
public Enums.PreferenceKind Kind { get; set; }
public PreferenceKind Kind { get; protected set; }
/// <summary>
/// Required, Max length = 65535
/// Gets or sets the value of this preference.
/// </summary>
/// <remarks>
/// Required, Max length = 65535.
/// </remarks>
[Required]
[MaxLength(65535)]
[StringLength(65535)]
public string Value { get; set; }
/// <summary>
/// Required, ConcurrenyToken
/// Gets or sets the row version.
/// </summary>
/// <remarks>
/// Required, ConcurrencyToken.
/// </remarks>
[ConcurrencyCheck]
[Required]
public uint RowVersion { get; set; }
/// <summary>
/// Static create function (for use in LINQ queries, etc.)
/// </summary>
/// <param name="kind">The preference kind.</param>
/// <param name="value">The value.</param>
/// <returns>The new instance.</returns>
public static Preference Create(PreferenceKind kind, string value)
{
return new Preference(kind, value);
}
/// <inheritdoc/>
public void OnSavingChanges()
{
RowVersion++;
}
/*************************************************************************
* Navigation properties
*************************************************************************/
}
}

@ -43,12 +43,6 @@ namespace Jellyfin.Data.Entities
if (string.IsNullOrEmpty(providerdata)) throw new ArgumentNullException(nameof(providerdata));
this.ProviderData = providerdata;
if (_user0 == null) throw new ArgumentNullException(nameof(_user0));
_user0.ProviderMappings.Add(this);
if (_group1 == null) throw new ArgumentNullException(nameof(_group1));
_group1.ProviderMappings.Add(this);
Init();
}

@ -19,14 +19,6 @@ namespace Jellyfin.Data.Entities
Init();
}
/// <summary>
/// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
/// </summary>
public static Series CreateSeriesUnsafe()
{
return new Series();
}
/// <summary>
/// Public constructor with required data
/// </summary>
@ -57,27 +49,27 @@ namespace Jellyfin.Data.Entities
/// <summary>
/// Backing field for AirsDayOfWeek
/// </summary>
protected Enums.Weekday? _AirsDayOfWeek;
protected DayOfWeek? _AirsDayOfWeek;
/// <summary>
/// When provided in a partial class, allows value of AirsDayOfWeek to be changed before setting.
/// </summary>
partial void SetAirsDayOfWeek(Enums.Weekday? oldValue, ref Enums.Weekday? newValue);
partial void SetAirsDayOfWeek(DayOfWeek? oldValue, ref DayOfWeek? newValue);
/// <summary>
/// When provided in a partial class, allows value of AirsDayOfWeek to be changed before returning.
/// </summary>
partial void GetAirsDayOfWeek(ref Enums.Weekday? result);
partial void GetAirsDayOfWeek(ref DayOfWeek? result);
public Enums.Weekday? AirsDayOfWeek
public DayOfWeek? AirsDayOfWeek
{
get
{
Enums.Weekday? value = _AirsDayOfWeek;
DayOfWeek? value = _AirsDayOfWeek;
GetAirsDayOfWeek(ref value);
return (_AirsDayOfWeek = value);
}
set
{
Enums.Weekday? oldValue = _AirsDayOfWeek;
DayOfWeek? oldValue = _AirsDayOfWeek;
SetAirsDayOfWeek(oldValue, ref value);
if (oldValue != value)
{

@ -2,234 +2,506 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
namespace Jellyfin.Data.Entities
{
public partial class User
/// <summary>
/// An entity representing a user.
/// </summary>
public partial class User : IHasPermissions, ISavingChanges
{
partial void Init();
/// <summary>
/// The values being delimited here are Guids, so commas work as they do not appear in Guids.
/// </summary>
private const char Delimiter = ',';
/// <summary>
/// Default constructor. Protected due to required properties, but present because EF needs it.
/// Initializes a new instance of the <see cref="User"/> class.
/// Public constructor with required data.
/// </summary>
protected User()
/// <param name="username">The username for the new user.</param>
/// <param name="authenticationProviderId">The Id of the user's authentication provider.</param>
/// <param name="passwordResetProviderId">The Id of the user's password reset provider.</param>
public User(string username, string authenticationProviderId, string passwordResetProviderId)
{
Groups = new HashSet<Group>();
if (string.IsNullOrEmpty(username))
{
throw new ArgumentNullException(nameof(username));
}
if (string.IsNullOrEmpty(authenticationProviderId))
{
throw new ArgumentNullException(nameof(authenticationProviderId));
}
if (string.IsNullOrEmpty(passwordResetProviderId))
{
throw new ArgumentNullException(nameof(passwordResetProviderId));
}
Username = username;
AuthenticationProviderId = authenticationProviderId;
PasswordResetProviderId = passwordResetProviderId;
AccessSchedules = new HashSet<AccessSchedule>();
// Groups = new HashSet<Group>();
Permissions = new HashSet<Permission>();
ProviderMappings = new HashSet<ProviderMapping>();
Preferences = new HashSet<Preference>();
// ProviderMappings = new HashSet<ProviderMapping>();
// Set default values
Id = Guid.NewGuid();
InvalidLoginAttemptCount = 0;
EnableUserPreferenceAccess = true;
MustUpdatePassword = false;
DisplayMissingEpisodes = false;
DisplayCollectionsView = false;
HidePlayedInLatest = true;
RememberAudioSelections = true;
RememberSubtitleSelections = true;
EnableNextEpisodeAutoPlay = true;
EnableAutoLogin = false;
PlayDefaultAudioTrack = true;
SubtitleMode = SubtitlePlaybackMode.Default;
SyncPlayAccess = SyncPlayAccess.CreateAndJoinGroups;
AddDefaultPermissions();
AddDefaultPreferences();
Init();
}
/// <summary>
/// Replaces default constructor, since it's protected. Caller assumes responsibility for setting all required values before saving.
/// </summary>
public static User CreateUserUnsafe()
{
return new User();
}
/// <summary>
/// Public constructor with required data
/// Initializes a new instance of the <see cref="User"/> class.
/// Default constructor. Protected due to required properties, but present because EF needs it.
/// </summary>
/// <param name="username"></param>
/// <param name="mustupdatepassword"></param>
/// <param name="audiolanguagepreference"></param>
/// <param name="authenticationproviderid"></param>
/// <param name="invalidloginattemptcount"></param>
/// <param name="subtitlemode"></param>
/// <param name="playdefaultaudiotrack"></param>
public User(string username, bool mustupdatepassword, string audiolanguagepreference, string authenticationproviderid, int invalidloginattemptcount, string subtitlemode, bool playdefaultaudiotrack)
protected User()
{
if (string.IsNullOrEmpty(username)) throw new ArgumentNullException(nameof(username));
this.Username = username;
this.MustUpdatePassword = mustupdatepassword;
if (string.IsNullOrEmpty(audiolanguagepreference)) throw new ArgumentNullException(nameof(audiolanguagepreference));
this.AudioLanguagePreference = audiolanguagepreference;
if (string.IsNullOrEmpty(authenticationproviderid)) throw new ArgumentNullException(nameof(authenticationproviderid));
this.AuthenticationProviderId = authenticationproviderid;
this.InvalidLoginAttemptCount = invalidloginattemptcount;
if (string.IsNullOrEmpty(subtitlemode)) throw new ArgumentNullException(nameof(subtitlemode));
this.SubtitleMode = subtitlemode;
this.PlayDefaultAudioTrack = playdefaultaudiotrack;
this.Groups = new HashSet<Group>();
this.Permissions = new HashSet<Permission>();
this.ProviderMappings = new HashSet<ProviderMapping>();
this.Preferences = new HashSet<Preference>();
Init();
}
/// <summary>
/// Static create function (for use in LINQ queries, etc.)
/// </summary>
/// <param name="username"></param>
/// <param name="mustupdatepassword"></param>
/// <param name="audiolanguagepreference"></param>
/// <param name="authenticationproviderid"></param>
/// <param name="invalidloginattemptcount"></param>
/// <param name="subtitlemode"></param>
/// <param name="playdefaultaudiotrack"></param>
public static User Create(string username, bool mustupdatepassword, string audiolanguagepreference, string authenticationproviderid, int invalidloginattemptcount, string subtitlemode, bool playdefaultaudiotrack)
{
return new User(username, mustupdatepassword, audiolanguagepreference, authenticationproviderid, invalidloginattemptcount, subtitlemode, playdefaultaudiotrack);
}
/*************************************************************************
* Properties
*************************************************************************/
/// <summary>
/// Identity, Indexed, Required
/// Gets or sets the Id of the user.
/// </summary>
/// <remarks>
/// Identity, Indexed, Required.
/// </remarks>
[Key]
[Required]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int Id { get; protected set; }
[JsonIgnore]
public Guid Id { get; set; }
/// <summary>
/// Required, Max length = 255
/// Gets or sets the user's name.
/// </summary>
/// <remarks>
/// Required, Max length = 255.
/// </remarks>
[Required]
[MaxLength(255)]
[StringLength(255)]
public string Username { get; set; }
/// <summary>
/// Max length = 65535
/// Gets or sets the user's password, or <c>null</c> if none is set.
/// </summary>
/// <remarks>
/// Max length = 65535.
/// </remarks>
[MaxLength(65535)]
[StringLength(65535)]
public string Password { get; set; }
/// <summary>
/// Required
/// Gets or sets the user's easy password, or <c>null</c> if none is set.
/// </summary>
/// <remarks>
/// Max length = 65535.
/// </remarks>
[MaxLength(65535)]
[StringLength(65535)]
public string EasyPassword { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the user must update their password.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[Required]
public bool MustUpdatePassword { get; set; }
/// <summary>
/// Required, Max length = 255
/// Gets or sets the audio language preference.
/// </summary>
[Required]
/// <remarks>
/// Max length = 255.
/// </remarks>
[MaxLength(255)]
[StringLength(255)]
public string AudioLanguagePreference { get; set; }
/// <summary>
/// Required, Max length = 255
/// Gets or sets the authentication provider id.
/// </summary>
/// <remarks>
/// Required, Max length = 255.
/// </remarks>
[Required]
[MaxLength(255)]
[StringLength(255)]
public string AuthenticationProviderId { get; set; }
/// <summary>
/// Max length = 65535
/// Gets or sets the password reset provider id.
/// </summary>
[MaxLength(65535)]
[StringLength(65535)]
public string GroupedFolders { get; set; }
/// <remarks>
/// Required, Max length = 255.
/// </remarks>
[Required]
[MaxLength(255)]
[StringLength(255)]
public string PasswordResetProviderId { get; set; }
/// <summary>
/// Required
/// Gets or sets the invalid login attempt count.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[Required]
public int InvalidLoginAttemptCount { get; set; }
/// <summary>
/// Max length = 65535
/// Gets or sets the last activity date.
/// </summary>
[MaxLength(65535)]
[StringLength(65535)]
public string LatestItemExcludes { get; set; }
public int? LoginAttemptsBeforeLockout { get; set; }
public DateTime? LastActivityDate { get; set; }
/// <summary>
/// Max length = 65535
/// Gets or sets the last login date.
/// </summary>
[MaxLength(65535)]
[StringLength(65535)]
public string MyMediaExcludes { get; set; }
public DateTime? LastLoginDate { get; set; }
/// <summary>
/// Max length = 65535
/// Gets or sets the number of login attempts the user can make before they are locked out.
/// </summary>
[MaxLength(65535)]
[StringLength(65535)]
public string OrderedViews { get; set; }
public int? LoginAttemptsBeforeLockout { get; set; }
/// <summary>
/// Required, Max length = 255
/// Gets or sets the subtitle mode.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[Required]
[MaxLength(255)]
[StringLength(255)]
public string SubtitleMode { get; set; }
public SubtitlePlaybackMode SubtitleMode { get; set; }
/// <summary>
/// Required
/// Gets or sets a value indicating whether the default audio track should be played.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[Required]
public bool PlayDefaultAudioTrack { get; set; }
/// <summary>
/// Max length = 255
/// Gets or sets the subtitle language preference.
/// </summary>
/// <remarks>
/// Max length = 255.
/// </remarks>
[MaxLength(255)]
[StringLength(255)]
public string SubtitleLanguagePrefernce { get; set; }
public string SubtitleLanguagePreference { get; set; }
public bool? DisplayMissingEpisodes { get; set; }
/// <summary>
/// Gets or sets a value indicating whether missing episodes should be displayed.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[Required]
public bool DisplayMissingEpisodes { get; set; }
public bool? DisplayCollectionsView { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to display the collections view.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[Required]
public bool DisplayCollectionsView { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the user has a local password.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[Required]
public bool EnableLocalPassword { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the server should hide played content in "Latest".
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[Required]
public bool HidePlayedInLatest { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to remember audio selections on played content.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[Required]
public bool RememberAudioSelections { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to remember subtitle selections on played content.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[Required]
public bool RememberSubtitleSelections { get; set; }
/// <summary>
/// Gets or sets a value indicating whether to enable auto-play for the next episode.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[Required]
public bool EnableNextEpisodeAutoPlay { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the user should auto-login.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[Required]
public bool EnableAutoLogin { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the user can change their preferences.
/// </summary>
/// <remarks>
/// Required.
/// </remarks>
[Required]
public bool EnableUserPreferenceAccess { get; set; }
public bool? HidePlayedInLatest { get; set; }
/// <summary>
/// Gets or sets the maximum parental age rating.
/// </summary>
public int? MaxParentalAgeRating { get; set; }
public bool? RememberAudioSelections { get; set; }
/// <summary>
/// Gets or sets the remote client bitrate limit.
/// </summary>
public int? RemoteClientBitrateLimit { get; set; }
public bool? RememberSubtitleSelections { get; set; }
/// <summary>
/// Gets or sets the internal id.
/// This is a temporary stopgap for until the library db is migrated.
/// This corresponds to the value of the index of this user in the library db.
/// </summary>
[Required]
public long InternalId { get; set; }
public bool? EnableNextEpisodeAutoPlay { get; set; }
/// <summary>
/// Gets or sets the user's profile image. Can be <c>null</c>.
/// </summary>
// [ForeignKey("UserId")]
public virtual ImageInfo ProfileImage { get; set; }
public bool? EnableUserPreferenceAccess { get; set; }
[Required]
public SyncPlayAccess SyncPlayAccess { get; set; }
/// <summary>
/// Required, ConcurrenyToken
/// Gets or sets the row version.
/// </summary>
/// <remarks>
/// Required, Concurrency Token.
/// </remarks>
[ConcurrencyCheck]
[Required]
public uint RowVersion { get; set; }
public void OnSavingChanges()
{
RowVersion++;
}
/*************************************************************************
* Navigation properties
*************************************************************************/
[ForeignKey("Group_Groups_Id")]
/// <summary>
/// Gets or sets the list of access schedules this user has.
/// </summary>
public virtual ICollection<AccessSchedule> AccessSchedules { get; protected set; }
/*
/// <summary>
/// Gets or sets the list of groups this user is a member of.
/// </summary>
[ForeignKey("Group_Groups_Guid")]
public virtual ICollection<Group> Groups { get; protected set; }
*/
[ForeignKey("Permission_Permissions_Id")]
/// <summary>
/// Gets or sets the list of permissions this user has.
/// </summary>
[ForeignKey("Permission_Permissions_Guid")]
public virtual ICollection<Permission> Permissions { get; protected set; }
/*
/// <summary>
/// Gets or sets the list of provider mappings this user has.
/// </summary>
[ForeignKey("ProviderMapping_ProviderMappings_Id")]
public virtual ICollection<ProviderMapping> ProviderMappings { get; protected set; }
*/
[ForeignKey("Preference_Preferences_Id")]
/// <summary>
/// Gets or sets the list of preferences this user has.
/// </summary>
[ForeignKey("Preference_Preferences_Guid")]
public virtual ICollection<Preference> Preferences { get; protected set; }
/// <summary>
/// Static create function (for use in LINQ queries, etc.)
/// </summary>
/// <param name="username">The username for the created user.</param>
/// <param name="authenticationProviderId">The Id of the user's authentication provider.</param>
/// <param name="passwordResetProviderId">The Id of the user's password reset provider.</param>
/// <returns>The created instance.</returns>
public static User Create(string username, string authenticationProviderId, string passwordResetProviderId)
{
return new User(username, authenticationProviderId, passwordResetProviderId);
}
/// <inheritdoc/>
public void OnSavingChanges()
{
RowVersion++;
}
/// <summary>
/// Checks whether the user has the specified permission.
/// </summary>
/// <param name="kind">The permission kind.</param>
/// <returns><c>True</c> if the user has the specified permission.</returns>
public bool HasPermission(PermissionKind kind)
{
return Permissions.First(p => p.Kind == kind).Value;
}
/// <summary>
/// Sets the given permission kind to the provided value.
/// </summary>
/// <param name="kind">The permission kind.</param>
/// <param name="value">The value to set.</param>
public void SetPermission(PermissionKind kind, bool value)
{
Permissions.First(p => p.Kind == kind).Value = value;
}
/// <summary>
/// Gets the user's preferences for the given preference kind.
/// </summary>
/// <param name="preference">The preference kind.</param>
/// <returns>A string array containing the user's preferences.</returns>
public string[] GetPreference(PreferenceKind preference)
{
var val = Preferences.First(p => p.Kind == preference).Value;
return Equals(val, string.Empty) ? Array.Empty<string>() : val.Split(Delimiter);
}
/// <summary>
/// Sets the specified preference to the given value.
/// </summary>
/// <param name="preference">The preference kind.</param>
/// <param name="values">The values.</param>
public void SetPreference(PreferenceKind preference, string[] values)
{
Preferences.First(p => p.Kind == preference).Value
= string.Join(Delimiter.ToString(CultureInfo.InvariantCulture), values);
}
/// <summary>
/// Checks whether this user is currently allowed to use the server.
/// </summary>
/// <returns><c>True</c> if the current time is within an access schedule, or there are no access schedules.</returns>
public bool IsParentalScheduleAllowed()
{
return AccessSchedules.Count == 0
|| AccessSchedules.Any(i => IsParentalScheduleAllowed(i, DateTime.UtcNow));
}
/// <summary>
/// Checks whether the provided folder is in this user's grouped folders.
/// </summary>
/// <param name="id">The Guid of the folder.</param>
/// <returns><c>True</c> if the folder is in the user's grouped folders.</returns>
public bool IsFolderGrouped(Guid id)
{
return GetPreference(PreferenceKind.GroupedFolders).Any(i => new Guid(i) == id);
}
private static bool IsParentalScheduleAllowed(AccessSchedule schedule, DateTime date)
{
var localTime = date.ToLocalTime();
var hour = localTime.TimeOfDay.TotalHours;
return DayOfWeekHelper.GetDaysOfWeek(schedule.DayOfWeek).Contains(localTime.DayOfWeek)
&& hour >= schedule.StartHour
&& hour <= schedule.EndHour;
}
// TODO: make these user configurable?
private void AddDefaultPermissions()
{
Permissions.Add(new Permission(PermissionKind.IsAdministrator, false));
Permissions.Add(new Permission(PermissionKind.IsDisabled, false));
Permissions.Add(new Permission(PermissionKind.IsHidden, true));
Permissions.Add(new Permission(PermissionKind.EnableAllChannels, true));
Permissions.Add(new Permission(PermissionKind.EnableAllDevices, true));
Permissions.Add(new Permission(PermissionKind.EnableAllFolders, true));
Permissions.Add(new Permission(PermissionKind.EnableContentDeletion, false));
Permissions.Add(new Permission(PermissionKind.EnableContentDownloading, true));
Permissions.Add(new Permission(PermissionKind.EnableMediaConversion, true));
Permissions.Add(new Permission(PermissionKind.EnableMediaPlayback, true));
Permissions.Add(new Permission(PermissionKind.EnablePlaybackRemuxing, true));
Permissions.Add(new Permission(PermissionKind.EnablePublicSharing, true));
Permissions.Add(new Permission(PermissionKind.EnableRemoteAccess, true));
Permissions.Add(new Permission(PermissionKind.EnableSyncTranscoding, true));
Permissions.Add(new Permission(PermissionKind.EnableAudioPlaybackTranscoding, true));
Permissions.Add(new Permission(PermissionKind.EnableLiveTvAccess, true));
Permissions.Add(new Permission(PermissionKind.EnableLiveTvManagement, true));
Permissions.Add(new Permission(PermissionKind.EnableSharedDeviceControl, true));
Permissions.Add(new Permission(PermissionKind.EnableVideoPlaybackTranscoding, true));
Permissions.Add(new Permission(PermissionKind.ForceRemoteSourceTranscoding, false));
Permissions.Add(new Permission(PermissionKind.EnableRemoteControlOfOtherUsers, false));
}
private void AddDefaultPreferences()
{
foreach (var val in Enum.GetValues(typeof(PreferenceKind)).Cast<PreferenceKind>())
{
Preferences.Add(new Preference(val, string.Empty));
}
}
partial void Init();
}
}

@ -1,6 +1,6 @@
#pragma warning disable CS1591
namespace MediaBrowser.Model.Configuration
namespace Jellyfin.Data.Enums
{
public enum DynamicDayOfWeek
{

@ -1,26 +1,113 @@
namespace Jellyfin.Data.Enums
{
/// <summary>
/// The types of user permissions.
/// </summary>
public enum PermissionKind
{
IsAdministrator,
IsHidden,
IsDisabled,
BlockUnrateditems,
EnbleSharedDeviceControl,
EnableRemoteAccess,
EnableLiveTvManagement,
EnableLiveTvAccess,
EnableMediaPlayback,
EnableAudioPlaybackTranscoding,
EnableVideoPlaybackTranscoding,
EnableContentDeletion,
EnableContentDownloading,
EnableSyncTranscoding,
EnableMediaConversion,
EnableAllDevices,
EnableAllChannels,
EnableAllFolders,
EnablePublicSharing,
AccessSchedules
/// <summary>
/// Whether the user is an administrator.
/// </summary>
IsAdministrator = 0,
/// <summary>
/// Whether the user is hidden.
/// </summary>
IsHidden = 1,
/// <summary>
/// Whether the user is disabled.
/// </summary>
IsDisabled = 2,
/// <summary>
/// Whether the user can control shared devices.
/// </summary>
EnableSharedDeviceControl = 3,
/// <summary>
/// Whether the user can access the server remotely.
/// </summary>
EnableRemoteAccess = 4,
/// <summary>
/// Whether the user can manage live tv.
/// </summary>
EnableLiveTvManagement = 5,
/// <summary>
/// Whether the user can access live tv.
/// </summary>
EnableLiveTvAccess = 6,
/// <summary>
/// Whether the user can play media.
/// </summary>
EnableMediaPlayback = 7,
/// <summary>
/// Whether the server should transcode audio for the user if requested.
/// </summary>
EnableAudioPlaybackTranscoding = 8,
/// <summary>
/// Whether the server should transcode video for the user if requested.
/// </summary>
EnableVideoPlaybackTranscoding = 9,
/// <summary>
/// Whether the user can delete content.
/// </summary>
EnableContentDeletion = 10,
/// <summary>
/// Whether the user can download content.
/// </summary>
EnableContentDownloading = 11,
/// <summary>
/// Whether to enable sync transcoding for the user.
/// </summary>
EnableSyncTranscoding = 12,
/// <summary>
/// Whether the user can do media conversion.
/// </summary>
EnableMediaConversion = 13,
/// <summary>
/// Whether the user has access to all devices.
/// </summary>
EnableAllDevices = 14,
/// <summary>
/// Whether the user has access to all channels.
/// </summary>
EnableAllChannels = 15,
/// <summary>
/// Whether the user has access to all folders.
/// </summary>
EnableAllFolders = 16,
/// <summary>
/// Whether to enable public sharing for the user.
/// </summary>
EnablePublicSharing = 17,
/// <summary>
/// Whether the user can remotely control other users.
/// </summary>
EnableRemoteControlOfOtherUsers = 18,
/// <summary>
/// Whether the user is permitted to do playback remuxing.
/// </summary>
EnablePlaybackRemuxing = 19,
/// <summary>
/// Whether the server should force transcoding on remote connections for the user.
/// </summary>
ForceRemoteSourceTranscoding = 20
}
}

@ -1,13 +1,68 @@
namespace Jellyfin.Data.Enums
{
/// <summary>
/// The types of user preferences.
/// </summary>
public enum PreferenceKind
{
MaxParentalRating,
BlockedTags,
RemoteClientBitrateLimit,
EnabledDevices,
EnabledChannels,
EnabledFolders,
EnableContentDeletionFromFolders
/// <summary>
/// A list of blocked tags.
/// </summary>
BlockedTags = 0,
/// <summary>
/// A list of blocked channels.
/// </summary>
BlockedChannels = 1,
/// <summary>
/// A list of blocked media folders.
/// </summary>
BlockedMediaFolders = 2,
/// <summary>
/// A list of enabled devices.
/// </summary>
EnabledDevices = 3,
/// <summary>
/// A list of enabled channels
/// </summary>
EnabledChannels = 4,
/// <summary>
/// A list of enabled folders.
/// </summary>
EnabledFolders = 5,
/// <summary>
/// A list of folders to allow content deletion from.
/// </summary>
EnableContentDeletionFromFolders = 6,
/// <summary>
/// A list of latest items to exclude.
/// </summary>
LatestItemExcludes = 7,
/// <summary>
/// A list of media to exclude.
/// </summary>
MyMediaExcludes = 8,
/// <summary>
/// A list of grouped folders.
/// </summary>
GroupedFolders = 9,
/// <summary>
/// A list of unrated items to block.
/// </summary>
BlockUnratedItems = 10,
/// <summary>
/// A list of ordered views.
/// </summary>
OrderedViews = 11
}
}

@ -1,6 +1,6 @@
#pragma warning disable CS1591
#pragma warning disable CS1591
namespace MediaBrowser.Model.Configuration
namespace Jellyfin.Data.Enums
{
public enum SubtitlePlaybackMode
{

@ -1,4 +1,4 @@
namespace MediaBrowser.Model.Configuration
namespace Jellyfin.Data.Enums
{
/// <summary>
/// Enum SyncPlayAccess.
@ -8,16 +8,16 @@ namespace MediaBrowser.Model.Configuration
/// <summary>
/// User can create groups and join them.
/// </summary>
CreateAndJoinGroups,
CreateAndJoinGroups = 0,
/// <summary>
/// User can only join already existing groups.
/// </summary>
JoinGroups,
JoinGroups = 1,
/// <summary>
/// SyncPlay is disabled for the user.
/// </summary>
None
None = 2
}
}

@ -1,6 +1,6 @@
#pragma warning disable CS1591
namespace MediaBrowser.Model.Configuration
namespace Jellyfin.Data.Enums
{
public enum UnratedItem
{

@ -1,13 +0,0 @@
namespace Jellyfin.Data.Enums
{
public enum Weekday
{
Sunday,
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday
}
}

@ -0,0 +1,31 @@
using System.Collections.Generic;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
namespace Jellyfin.Data
{
/// <summary>
/// An abstraction representing an entity that has permissions.
/// </summary>
public interface IHasPermissions
{
/// <summary>
/// Gets a collection containing this entity's permissions.
/// </summary>
ICollection<Permission> Permissions { get; }
/// <summary>
/// Checks whether this entity has the specified permission kind.
/// </summary>
/// <param name="kind">The kind of permission.</param>
/// <returns><c>true</c> if this entity has the specified permission, <c>false</c> otherwise.</returns>
bool HasPermission(PermissionKind kind);
/// <summary>
/// Sets the specified permission to the provided value.
/// </summary>
/// <param name="kind">The kind of permission.</param>
/// <param name="value">The value to set.</param>
void SetPermission(PermissionKind kind, bool value);
}
}

@ -21,6 +21,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="3.1.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="3.1.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies" Version="3.1.5" />
</ItemGroup>
</Project>

@ -21,8 +21,6 @@
<ItemGroup>
<Compile Include="..\SharedVersion.cs" />
<Compile Remove="Migrations\20200430214405_InitialSchema.cs" />
<Compile Remove="Migrations\20200430214405_InitialSchema.Designer.cs" />
</ItemGroup>
<ItemGroup>

@ -1,9 +1,4 @@
#pragma warning disable CS1591
#pragma warning disable SA1201 // Constuctors should not follow properties
#pragma warning disable SA1516 // Elements should be followed by a blank line
#pragma warning disable SA1623 // Property's documentation should begin with gets or sets
#pragma warning disable SA1629 // Documentation should end with a period
#pragma warning disable SA1648 // Inheritdoc should be used with inheriting class
using System.Linq;
using Jellyfin.Data;
@ -15,7 +10,30 @@ namespace Jellyfin.Server.Implementations
/// <inheritdoc/>
public partial class JellyfinDb : DbContext
{
/// <summary>
/// Initializes a new instance of the <see cref="JellyfinDb"/> class.
/// </summary>
/// <param name="options">The database context options.</param>
public JellyfinDb(DbContextOptions<JellyfinDb> options) : base(options)
{
}
/// <summary>
/// Gets or sets the default connection string.
/// </summary>
public static string ConnectionString { get; set; } = @"Data Source=jellyfin.db";
public virtual DbSet<AccessSchedule> AccessSchedules { get; set; }
public virtual DbSet<ActivityLog> ActivityLogs { get; set; }
public virtual DbSet<ImageInfo> ImageInfos { get; set; }
public virtual DbSet<Permission> Permissions { get; set; }
public virtual DbSet<Preference> Preferences { get; set; }
public virtual DbSet<User> Users { get; set; }
/*public virtual DbSet<Artwork> Artwork { get; set; }
public virtual DbSet<Book> Books { get; set; }
public virtual DbSet<BookMetadata> BookMetadata { get; set; }
@ -42,12 +60,10 @@ namespace Jellyfin.Server.Implementations
public virtual DbSet<MovieMetadata> MovieMetadata { get; set; }
public virtual DbSet<MusicAlbum> MusicAlbums { get; set; }
public virtual DbSet<MusicAlbumMetadata> MusicAlbumMetadata { get; set; }
public virtual DbSet<Permission> Permissions { get; set; }
public virtual DbSet<Person> People { get; set; }
public virtual DbSet<PersonRole> PersonRoles { get; set; }
public virtual DbSet<Photo> Photo { get; set; }
public virtual DbSet<PhotoMetadata> PhotoMetadata { get; set; }
public virtual DbSet<Preference> Preferences { get; set; }
public virtual DbSet<ProviderMapping> ProviderMappings { get; set; }
public virtual DbSet<Rating> Ratings { get; set; }
@ -62,20 +78,21 @@ namespace Jellyfin.Server.Implementations
public virtual DbSet<Series> Series { get; set; }
public virtual DbSet<SeriesMetadata> SeriesMetadata { get; set; }
public virtual DbSet<Track> Tracks { get; set; }
public virtual DbSet<TrackMetadata> TrackMetadata { get; set; }
public virtual DbSet<User> Users { get; set; } */
/// <summary>
/// Gets or sets the default connection string.
/// </summary>
public static string ConnectionString { get; set; } = @"Data Source=jellyfin.db";
public virtual DbSet<TrackMetadata> TrackMetadata { get; set; }*/
/// <inheritdoc />
public JellyfinDb(DbContextOptions<JellyfinDb> options) : base(options)
/// <inheritdoc/>
public override int SaveChanges()
{
}
foreach (var saveEntity in ChangeTracker.Entries()
.Where(e => e.State == EntityState.Modified)
.Select(entry => entry.Entity)
.OfType<ISavingChanges>())
{
saveEntity.OnSavingChanges();
}
partial void CustomInit(DbContextOptionsBuilder optionsBuilder);
return base.SaveChanges();
}
/// <inheritdoc />
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
@ -83,9 +100,6 @@ namespace Jellyfin.Server.Implementations
CustomInit(optionsBuilder);
}
partial void OnModelCreatingImpl(ModelBuilder modelBuilder);
partial void OnModelCreatedImpl(ModelBuilder modelBuilder);
/// <inheritdoc />
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@ -105,16 +119,10 @@ namespace Jellyfin.Server.Implementations
OnModelCreatedImpl(modelBuilder);
}
public override int SaveChanges()
{
foreach (var saveEntity in ChangeTracker.Entries()
.Where(e => e.State == EntityState.Modified)
.OfType<ISavingChanges>())
{
saveEntity.OnSavingChanges();
}
partial void CustomInit(DbContextOptionsBuilder optionsBuilder);
return base.SaveChanges();
}
partial void OnModelCreatingImpl(ModelBuilder modelBuilder);
partial void OnModelCreatedImpl(ModelBuilder modelBuilder);
}
}

@ -18,7 +18,7 @@ namespace Jellyfin.Server.Implementations
public JellyfinDbProvider(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
serviceProvider.GetService<JellyfinDb>().Database.Migrate();
serviceProvider.GetRequiredService<JellyfinDb>().Database.Migrate();
}
/// <summary>

@ -0,0 +1,312 @@
#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("20200613202153_AddUsers")]
partial class AddUsers
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("jellyfin")
.HasAnnotation("ProductVersion", "3.1.4");
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.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.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.ImageInfo", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithOne("ProfileImage")
.HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId");
});
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,197 @@
#pragma warning disable CS1591
#pragma warning disable SA1601
using System;
using Microsoft.EntityFrameworkCore.Migrations;
namespace Jellyfin.Server.Implementations.Migrations
{
public partial class AddUsers : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Users",
schema: "jellyfin",
columns: table => new
{
Id = table.Column<Guid>(nullable: false),
Username = table.Column<string>(maxLength: 255, nullable: false),
Password = table.Column<string>(maxLength: 65535, nullable: true),
EasyPassword = table.Column<string>(maxLength: 65535, nullable: true),
MustUpdatePassword = table.Column<bool>(nullable: false),
AudioLanguagePreference = table.Column<string>(maxLength: 255, nullable: true),
AuthenticationProviderId = table.Column<string>(maxLength: 255, nullable: false),
PasswordResetProviderId = table.Column<string>(maxLength: 255, nullable: false),
InvalidLoginAttemptCount = table.Column<int>(nullable: false),
LastActivityDate = table.Column<DateTime>(nullable: true),
LastLoginDate = table.Column<DateTime>(nullable: true),
LoginAttemptsBeforeLockout = table.Column<int>(nullable: true),
SubtitleMode = table.Column<int>(nullable: false),
PlayDefaultAudioTrack = table.Column<bool>(nullable: false),
SubtitleLanguagePreference = table.Column<string>(maxLength: 255, nullable: true),
DisplayMissingEpisodes = table.Column<bool>(nullable: false),
DisplayCollectionsView = table.Column<bool>(nullable: false),
EnableLocalPassword = table.Column<bool>(nullable: false),
HidePlayedInLatest = table.Column<bool>(nullable: false),
RememberAudioSelections = table.Column<bool>(nullable: false),
RememberSubtitleSelections = table.Column<bool>(nullable: false),
EnableNextEpisodeAutoPlay = table.Column<bool>(nullable: false),
EnableAutoLogin = table.Column<bool>(nullable: false),
EnableUserPreferenceAccess = table.Column<bool>(nullable: false),
MaxParentalAgeRating = table.Column<int>(nullable: true),
RemoteClientBitrateLimit = table.Column<int>(nullable: true),
InternalId = table.Column<long>(nullable: false),
SyncPlayAccess = table.Column<int>(nullable: false),
RowVersion = table.Column<uint>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
migrationBuilder.CreateTable(
name: "AccessSchedules",
schema: "jellyfin",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<Guid>(nullable: false),
DayOfWeek = table.Column<int>(nullable: false),
StartHour = table.Column<double>(nullable: false),
EndHour = table.Column<double>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AccessSchedules", x => x.Id);
table.ForeignKey(
name: "FK_AccessSchedules_Users_UserId",
column: x => x.UserId,
principalSchema: "jellyfin",
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "ImageInfos",
schema: "jellyfin",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<Guid>(nullable: true),
Path = table.Column<string>(maxLength: 512, nullable: false),
LastModified = table.Column<DateTime>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ImageInfos", x => x.Id);
table.ForeignKey(
name: "FK_ImageInfos_Users_UserId",
column: x => x.UserId,
principalSchema: "jellyfin",
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "Permissions",
schema: "jellyfin",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Kind = table.Column<int>(nullable: false),
Value = table.Column<bool>(nullable: false),
RowVersion = table.Column<uint>(nullable: false),
Permission_Permissions_Guid = table.Column<Guid>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Permissions", x => x.Id);
table.ForeignKey(
name: "FK_Permissions_Users_Permission_Permissions_Guid",
column: x => x.Permission_Permissions_Guid,
principalSchema: "jellyfin",
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "Preferences",
schema: "jellyfin",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
Kind = table.Column<int>(nullable: false),
Value = table.Column<string>(maxLength: 65535, nullable: false),
RowVersion = table.Column<uint>(nullable: false),
Preference_Preferences_Guid = table.Column<Guid>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Preferences", x => x.Id);
table.ForeignKey(
name: "FK_Preferences_Users_Preference_Preferences_Guid",
column: x => x.Preference_Preferences_Guid,
principalSchema: "jellyfin",
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateIndex(
name: "IX_AccessSchedules_UserId",
schema: "jellyfin",
table: "AccessSchedules",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_ImageInfos_UserId",
schema: "jellyfin",
table: "ImageInfos",
column: "UserId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Permissions_Permission_Permissions_Guid",
schema: "jellyfin",
table: "Permissions",
column: "Permission_Permissions_Guid");
migrationBuilder.CreateIndex(
name: "IX_Preferences_Preference_Preferences_Guid",
schema: "jellyfin",
table: "Preferences",
column: "Preference_Preferences_Guid");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AccessSchedules",
schema: "jellyfin");
migrationBuilder.DropTable(
name: "ImageInfos",
schema: "jellyfin");
migrationBuilder.DropTable(
name: "Permissions",
schema: "jellyfin");
migrationBuilder.DropTable(
name: "Preferences",
schema: "jellyfin");
migrationBuilder.DropTable(
name: "Users",
schema: "jellyfin");
}
}
}

@ -1,7 +1,9 @@
// <auto-generated />
using System;
using Jellyfin.Server.Implementations;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace Jellyfin.Server.Implementations.Migrations
{
@ -13,7 +15,32 @@ namespace Jellyfin.Server.Implementations.Migrations
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("jellyfin")
.HasAnnotation("ProductVersion", "3.1.3");
.HasAnnotation("ProductVersion", "3.1.4");
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 =>
{
@ -60,6 +87,221 @@ namespace Jellyfin.Server.Implementations.Migrations
b.ToTable("ActivityLogs");
});
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.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.ImageInfo", b =>
{
b.HasOne("Jellyfin.Data.Entities.User", null)
.WithOne("ProfileImage")
.HasForeignKey("Jellyfin.Data.Entities.ImageInfo", "UserId");
});
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
}
}

@ -1,14 +1,16 @@
#nullable enable
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common;
using MediaBrowser.Common.Cryptography;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Cryptography;
namespace Emby.Server.Implementations.Library
namespace Jellyfin.Server.Implementations.Users
{
/// <summary>
/// The default authentication provider.
@ -47,7 +49,7 @@ namespace Emby.Server.Implementations.Library
{
if (resolvedUser == null)
{
throw new AuthenticationException($"Specified user does not exist.");
throw new AuthenticationException("Specified user does not exist.");
}
bool success = false;
@ -61,7 +63,7 @@ namespace Emby.Server.Implementations.Library
});
}
byte[] passwordbytes = Encoding.UTF8.GetBytes(password);
byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
PasswordHash readyHash = PasswordHash.Parse(resolvedUser.Password);
if (_cryptographyProvider.GetSupportedHashMethods().Contains(readyHash.Id)
@ -69,7 +71,7 @@ namespace Emby.Server.Implementations.Library
{
byte[] calculatedHash = _cryptographyProvider.ComputeHash(
readyHash.Id,
passwordbytes,
passwordBytes,
readyHash.Salt.ToArray());
if (readyHash.Hash.SequenceEqual(calculatedHash))
@ -95,7 +97,7 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public bool HasPassword(User user)
=> !string.IsNullOrEmpty(user.Password);
=> !string.IsNullOrEmpty(user?.Password);
/// <inheritdoc />
public Task ChangePassword(User user, string newPassword)
@ -129,7 +131,7 @@ namespace Emby.Server.Implementations.Library
}
/// <inheritdoc />
public string GetEasyPasswordHash(User user)
public string? GetEasyPasswordHash(User user)
{
return string.IsNullOrEmpty(user.EasyPassword)
? null

@ -1,8 +1,11 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Configuration;
@ -10,7 +13,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Serialization;
using MediaBrowser.Model.Users;
namespace Emby.Server.Implementations.Library
namespace Jellyfin.Server.Implementations.Users
{
/// <summary>
/// The default password reset provider.
@ -51,54 +54,49 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
{
SerializablePasswordReset spr;
List<string> usersreset = new List<string>();
foreach (var resetfile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*"))
var usersReset = new List<string>();
foreach (var resetFile in Directory.EnumerateFiles(_passwordResetFileBaseDir, $"{BaseResetFileName}*"))
{
using (var str = File.OpenRead(resetfile))
SerializablePasswordReset spr;
await using (var str = File.OpenRead(resetFile))
{
spr = await _jsonSerializer.DeserializeFromStreamAsync<SerializablePasswordReset>(str).ConfigureAwait(false);
}
if (spr.ExpirationDate < DateTime.Now)
if (spr.ExpirationDate < DateTime.UtcNow)
{
File.Delete(resetfile);
File.Delete(resetFile);
}
else if (string.Equals(
spr.Pin.Replace("-", string.Empty, StringComparison.Ordinal),
pin.Replace("-", string.Empty, StringComparison.Ordinal),
StringComparison.InvariantCultureIgnoreCase))
{
var resetUser = _userManager.GetUserByName(spr.UserName);
if (resetUser == null)
{
throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found");
}
var resetUser = _userManager.GetUserByName(spr.UserName)
?? throw new ResourceNotFoundException($"User with a username of {spr.UserName} not found");
await _userManager.ChangePassword(resetUser, pin).ConfigureAwait(false);
usersreset.Add(resetUser.Name);
File.Delete(resetfile);
usersReset.Add(resetUser.Username);
File.Delete(resetFile);
}
}
if (usersreset.Count < 1)
if (usersReset.Count < 1)
{
throw new ResourceNotFoundException($"No Users found with a password reset request matching pin {pin}");
}
else
return new PinRedeemResult
{
return new PinRedeemResult
{
Success = true,
UsersReset = usersreset.ToArray()
};
}
Success = true,
UsersReset = usersReset.ToArray()
};
}
/// <inheritdoc />
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(MediaBrowser.Controller.Entities.User user, bool isInNetwork)
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(User user, bool isInNetwork)
{
string pin = string.Empty;
string pin;
using (var cryptoRandom = RandomNumberGenerator.Create())
{
byte[] bytes = new byte[4];
@ -106,30 +104,33 @@ namespace Emby.Server.Implementations.Library
pin = BitConverter.ToString(bytes);
}
DateTime expireTime = DateTime.Now.AddMinutes(30);
string filePath = _passwordResetFileBase + user.InternalId + ".json";
DateTime expireTime = DateTime.UtcNow.AddMinutes(30);
string filePath = _passwordResetFileBase + user.Id + ".json";
SerializablePasswordReset spr = new SerializablePasswordReset
{
ExpirationDate = expireTime,
Pin = pin,
PinFile = filePath,
UserName = user.Name
UserName = user.Username
};
using (FileStream fileStream = File.OpenWrite(filePath))
await using (FileStream fileStream = File.OpenWrite(filePath))
{
_jsonSerializer.SerializeToStream(spr, fileStream);
await fileStream.FlushAsync().ConfigureAwait(false);
}
user.EasyPassword = pin;
await _userManager.UpdateUserAsync(user).ConfigureAwait(false);
return new ForgotPasswordResult
{
Action = ForgotPasswordAction.PinCode,
PinExpirationDate = expireTime,
PinFile = filePath
};
}
#nullable disable
private class SerializablePasswordReset : PasswordPinCreationResult
{
public string Pin { get; set; }

@ -0,0 +1,67 @@
#nullable enable
#pragma warning disable CS1591
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Plugins;
using MediaBrowser.Controller.Security;
using MediaBrowser.Controller.Session;
using MediaBrowser.Model.Events;
namespace Jellyfin.Server.Implementations.Users
{
public sealed class DeviceAccessEntryPoint : IServerEntryPoint
{
private readonly IUserManager _userManager;
private readonly IAuthenticationRepository _authRepo;
private readonly IDeviceManager _deviceManager;
private readonly ISessionManager _sessionManager;
public DeviceAccessEntryPoint(IUserManager userManager, IAuthenticationRepository authRepo, IDeviceManager deviceManager, ISessionManager sessionManager)
{
_userManager = userManager;
_authRepo = authRepo;
_deviceManager = deviceManager;
_sessionManager = sessionManager;
}
public Task RunAsync()
{
_userManager.OnUserUpdated += OnUserUpdated;
return Task.CompletedTask;
}
public void Dispose()
{
}
private void OnUserUpdated(object? sender, GenericEventArgs<User> e)
{
var user = e.Argument;
if (!user.HasPermission(PermissionKind.EnableAllDevices))
{
UpdateDeviceAccess(user);
}
}
private void UpdateDeviceAccess(User user)
{
var existing = _authRepo.Get(new AuthenticationInfoQuery
{
UserId = user.Id
}).Items;
foreach (var authInfo in existing)
{
if (!string.IsNullOrEmpty(authInfo.DeviceId) && !_deviceManager.CanAccessDevice(user, authInfo.DeviceId))
{
_sessionManager.Logout(authInfo);
}
}
}
}
}

@ -1,8 +1,10 @@
#nullable enable
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Entities;
namespace Emby.Server.Implementations.Library
namespace Jellyfin.Server.Implementations.Users
{
/// <summary>
/// An invalid authentication provider.
@ -39,12 +41,6 @@ namespace Emby.Server.Implementations.Library
// Nothing here
}
/// <inheritdoc />
public string GetPasswordHash(User user)
{
return string.Empty;
}
/// <inheritdoc />
public string GetEasyPasswordHash(User user)
{

@ -0,0 +1,841 @@
#nullable enable
#pragma warning disable CA1307
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common;
using MediaBrowser.Common.Cryptography;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Authentication;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Cryptography;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Events;
using MediaBrowser.Model.Users;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Server.Implementations.Users
{
/// <summary>
/// Manages the creation and retrieval of <see cref="User"/> instances.
/// </summary>
public class UserManager : IUserManager
{
private readonly JellyfinDbProvider _dbProvider;
private readonly ICryptoProvider _cryptoProvider;
private readonly INetworkManager _networkManager;
private readonly IApplicationHost _appHost;
private readonly IImageProcessor _imageProcessor;
private readonly ILogger<UserManager> _logger;
private IAuthenticationProvider[] _authenticationProviders = null!;
private DefaultAuthenticationProvider _defaultAuthenticationProvider = null!;
private InvalidAuthProvider _invalidAuthProvider = null!;
private IPasswordResetProvider[] _passwordResetProviders = null!;
private DefaultPasswordResetProvider _defaultPasswordResetProvider = null!;
/// <summary>
/// Initializes a new instance of the <see cref="UserManager"/> class.
/// </summary>
/// <param name="dbProvider">The database provider.</param>
/// <param name="cryptoProvider">The cryptography provider.</param>
/// <param name="networkManager">The network manager.</param>
/// <param name="appHost">The application host.</param>
/// <param name="imageProcessor">The image processor.</param>
/// <param name="logger">The logger.</param>
public UserManager(
JellyfinDbProvider dbProvider,
ICryptoProvider cryptoProvider,
INetworkManager networkManager,
IApplicationHost appHost,
IImageProcessor imageProcessor,
ILogger<UserManager> logger)
{
_dbProvider = dbProvider;
_cryptoProvider = cryptoProvider;
_networkManager = networkManager;
_appHost = appHost;
_imageProcessor = imageProcessor;
_logger = logger;
}
/// <inheritdoc/>
public event EventHandler<GenericEventArgs<User>>? OnUserPasswordChanged;
/// <inheritdoc/>
public event EventHandler<GenericEventArgs<User>>? OnUserUpdated;
/// <inheritdoc/>
public event EventHandler<GenericEventArgs<User>>? OnUserCreated;
/// <inheritdoc/>
public event EventHandler<GenericEventArgs<User>>? OnUserDeleted;
/// <inheritdoc/>
public event EventHandler<GenericEventArgs<User>>? OnUserLockedOut;
/// <inheritdoc/>
public IEnumerable<User> Users => _dbProvider.CreateContext().Users;
/// <inheritdoc/>
public IEnumerable<Guid> UsersIds => _dbProvider.CreateContext().Users.Select(u => u.Id);
/// <inheritdoc/>
public User? GetUserById(Guid id)
{
if (id == Guid.Empty)
{
throw new ArgumentException("Guid can't be empty", nameof(id));
}
return _dbProvider.CreateContext().Users.Find(id);
}
/// <inheritdoc/>
public User? GetUserByName(string name)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentException("Invalid username", nameof(name));
}
// This can't use an overload with StringComparer because that would cause the query to
// have to be evaluated client-side.
return _dbProvider.CreateContext().Users.FirstOrDefault(u => string.Equals(u.Username, name));
}
/// <inheritdoc/>
public async Task RenameUser(User user, string newName)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (string.IsNullOrWhiteSpace(newName))
{
throw new ArgumentException("Invalid username", nameof(newName));
}
if (user.Username.Equals(newName, StringComparison.Ordinal))
{
throw new ArgumentException("The new and old names must be different.");
}
if (Users.Any(u => u.Id != user.Id && u.Username.Equals(newName, StringComparison.OrdinalIgnoreCase)))
{
throw new ArgumentException(string.Format(
CultureInfo.InvariantCulture,
"A user with the name '{0}' already exists.",
newName));
}
user.Username = newName;
await UpdateUserAsync(user).ConfigureAwait(false);
OnUserUpdated?.Invoke(this, new GenericEventArgs<User>(user));
}
/// <inheritdoc/>
public void UpdateUser(User user)
{
var dbContext = _dbProvider.CreateContext();
dbContext.Users.Update(user);
dbContext.SaveChanges();
}
/// <inheritdoc/>
public async Task UpdateUserAsync(User user)
{
var dbContext = _dbProvider.CreateContext();
dbContext.Users.Update(user);
await dbContext.SaveChangesAsync().ConfigureAwait(false);
}
/// <inheritdoc/>
public User CreateUser(string name)
{
if (!IsValidUsername(name))
{
throw new ArgumentException("Usernames can contain unicode symbols, numbers (0-9), dashes (-), underscores (_), apostrophes ('), and periods (.)");
}
var dbContext = _dbProvider.CreateContext();
// TODO: Remove after user item data is migrated.
var max = dbContext.Users.Any() ? dbContext.Users.Select(u => u.InternalId).Max() : 0;
var newUser = new User(
name,
_defaultAuthenticationProvider.GetType().FullName,
_defaultPasswordResetProvider.GetType().FullName)
{
InternalId = max + 1
};
dbContext.Users.Add(newUser);
dbContext.SaveChanges();
OnUserCreated?.Invoke(this, new GenericEventArgs<User>(newUser));
return newUser;
}
/// <inheritdoc/>
public void DeleteUser(User user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var dbContext = _dbProvider.CreateContext();
if (dbContext.Users.Find(user.Id) == null)
{
throw new ArgumentException(string.Format(
CultureInfo.InvariantCulture,
"The user cannot be deleted because there is no user with the Name {0} and Id {1}.",
user.Username,
user.Id));
}
if (dbContext.Users.Count() == 1)
{
throw new InvalidOperationException(string.Format(
CultureInfo.InvariantCulture,
"The user '{0}' cannot be deleted because there must be at least one user in the system.",
user.Username));
}
if (user.HasPermission(PermissionKind.IsAdministrator)
&& Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
{
throw new ArgumentException(
string.Format(
CultureInfo.InvariantCulture,
"The user '{0}' cannot be deleted because there must be at least one admin user in the system.",
user.Username),
nameof(user));
}
dbContext.Users.Remove(user);
dbContext.SaveChanges();
OnUserDeleted?.Invoke(this, new GenericEventArgs<User>(user));
}
/// <inheritdoc/>
public Task ResetPassword(User user)
{
return ChangePassword(user, string.Empty);
}
/// <inheritdoc/>
public void ResetEasyPassword(User user)
{
ChangeEasyPassword(user, string.Empty, null);
}
/// <inheritdoc/>
public async Task ChangePassword(User user, string newPassword)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
await GetAuthenticationProvider(user).ChangePassword(user, newPassword).ConfigureAwait(false);
await UpdateUserAsync(user).ConfigureAwait(false);
OnUserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
}
/// <inheritdoc/>
public void ChangeEasyPassword(User user, string newPassword, string? newPasswordSha1)
{
GetAuthenticationProvider(user).ChangeEasyPassword(user, newPassword, newPasswordSha1);
UpdateUser(user);
OnUserPasswordChanged?.Invoke(this, new GenericEventArgs<User>(user));
}
/// <inheritdoc/>
public UserDto GetUserDto(User user, string? remoteEndPoint = null)
{
var hasPassword = GetAuthenticationProvider(user).HasPassword(user);
return new UserDto
{
Name = user.Username,
Id = user.Id,
ServerId = _appHost.SystemId,
HasPassword = hasPassword,
HasConfiguredPassword = hasPassword,
HasConfiguredEasyPassword = !string.IsNullOrEmpty(user.EasyPassword),
EnableAutoLogin = user.EnableAutoLogin,
LastLoginDate = user.LastLoginDate,
LastActivityDate = user.LastActivityDate,
PrimaryImageTag = user.ProfileImage != null ? _imageProcessor.GetImageCacheTag(user) : null,
Configuration = new UserConfiguration
{
SubtitleMode = user.SubtitleMode,
HidePlayedInLatest = user.HidePlayedInLatest,
EnableLocalPassword = user.EnableLocalPassword,
PlayDefaultAudioTrack = user.PlayDefaultAudioTrack,
DisplayCollectionsView = user.DisplayCollectionsView,
DisplayMissingEpisodes = user.DisplayMissingEpisodes,
AudioLanguagePreference = user.AudioLanguagePreference,
RememberAudioSelections = user.RememberAudioSelections,
EnableNextEpisodeAutoPlay = user.EnableNextEpisodeAutoPlay,
RememberSubtitleSelections = user.RememberSubtitleSelections,
SubtitleLanguagePreference = user.SubtitleLanguagePreference ?? string.Empty,
OrderedViews = user.GetPreference(PreferenceKind.OrderedViews),
GroupedFolders = user.GetPreference(PreferenceKind.GroupedFolders),
MyMediaExcludes = user.GetPreference(PreferenceKind.MyMediaExcludes),
LatestItemsExcludes = user.GetPreference(PreferenceKind.LatestItemExcludes)
},
Policy = new UserPolicy
{
MaxParentalRating = user.MaxParentalAgeRating,
EnableUserPreferenceAccess = user.EnableUserPreferenceAccess,
RemoteClientBitrateLimit = user.RemoteClientBitrateLimit ?? 0,
AuthenticationProviderId = user.AuthenticationProviderId,
PasswordResetProviderId = user.PasswordResetProviderId,
InvalidLoginAttemptCount = user.InvalidLoginAttemptCount,
LoginAttemptsBeforeLockout = user.LoginAttemptsBeforeLockout ?? -1,
IsAdministrator = user.HasPermission(PermissionKind.IsAdministrator),
IsHidden = user.HasPermission(PermissionKind.IsHidden),
IsDisabled = user.HasPermission(PermissionKind.IsDisabled),
EnableSharedDeviceControl = user.HasPermission(PermissionKind.EnableSharedDeviceControl),
EnableRemoteAccess = user.HasPermission(PermissionKind.EnableRemoteAccess),
EnableLiveTvManagement = user.HasPermission(PermissionKind.EnableLiveTvManagement),
EnableLiveTvAccess = user.HasPermission(PermissionKind.EnableLiveTvAccess),
EnableMediaPlayback = user.HasPermission(PermissionKind.EnableMediaPlayback),
EnableAudioPlaybackTranscoding = user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding),
EnableVideoPlaybackTranscoding = user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
EnableContentDeletion = user.HasPermission(PermissionKind.EnableContentDeletion),
EnableContentDownloading = user.HasPermission(PermissionKind.EnableContentDownloading),
EnableSyncTranscoding = user.HasPermission(PermissionKind.EnableSyncTranscoding),
EnableMediaConversion = user.HasPermission(PermissionKind.EnableMediaConversion),
EnableAllChannels = user.HasPermission(PermissionKind.EnableAllChannels),
EnableAllDevices = user.HasPermission(PermissionKind.EnableAllDevices),
EnableAllFolders = user.HasPermission(PermissionKind.EnableAllFolders),
EnableRemoteControlOfOtherUsers = user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers),
EnablePlaybackRemuxing = user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
ForceRemoteSourceTranscoding = user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding),
EnablePublicSharing = user.HasPermission(PermissionKind.EnablePublicSharing),
AccessSchedules = user.AccessSchedules.ToArray(),
BlockedTags = user.GetPreference(PreferenceKind.BlockedTags),
EnabledChannels = user.GetPreference(PreferenceKind.EnabledChannels),
EnabledDevices = user.GetPreference(PreferenceKind.EnabledDevices),
EnabledFolders = user.GetPreference(PreferenceKind.EnabledFolders),
EnableContentDeletionFromFolders = user.GetPreference(PreferenceKind.EnableContentDeletionFromFolders),
SyncPlayAccess = user.SyncPlayAccess,
BlockedChannels = user.GetPreference(PreferenceKind.BlockedChannels),
BlockedMediaFolders = user.GetPreference(PreferenceKind.BlockedMediaFolders),
BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems).Select(Enum.Parse<UnratedItem>).ToArray()
}
};
}
/// <inheritdoc/>
public async Task<User?> AuthenticateUser(
string username,
string password,
string passwordSha1,
string remoteEndPoint,
bool isUserSession)
{
if (string.IsNullOrWhiteSpace(username))
{
_logger.LogInformation("Authentication request without username has been denied (IP: {IP}).", remoteEndPoint);
throw new ArgumentNullException(nameof(username));
}
var user = Users.ToList().FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
bool success;
IAuthenticationProvider? authenticationProvider;
if (user != null)
{
var authResult = await AuthenticateLocalUser(username, password, user, remoteEndPoint)
.ConfigureAwait(false);
authenticationProvider = authResult.authenticationProvider;
success = authResult.success;
}
else
{
var authResult = await AuthenticateLocalUser(username, password, null, remoteEndPoint)
.ConfigureAwait(false);
authenticationProvider = authResult.authenticationProvider;
string updatedUsername = authResult.username;
success = authResult.success;
if (success
&& authenticationProvider != null
&& !(authenticationProvider is DefaultAuthenticationProvider))
{
// Trust the username returned by the authentication provider
username = updatedUsername;
// Search the database for the user again
// the authentication provider might have created it
user = Users
.ToList().FirstOrDefault(i => string.Equals(username, i.Username, StringComparison.OrdinalIgnoreCase));
if (authenticationProvider is IHasNewUserPolicy hasNewUserPolicy)
{
UpdatePolicy(user.Id, hasNewUserPolicy.GetNewUserPolicy());
await UpdateUserAsync(user).ConfigureAwait(false);
}
}
}
if (success && user != null && authenticationProvider != null)
{
var providerId = authenticationProvider.GetType().FullName;
if (!string.Equals(providerId, user.AuthenticationProviderId, StringComparison.OrdinalIgnoreCase))
{
user.AuthenticationProviderId = providerId;
await UpdateUserAsync(user).ConfigureAwait(false);
}
}
if (user == null)
{
_logger.LogInformation(
"Authentication request for {UserName} has been denied (IP: {IP}).",
username,
remoteEndPoint);
throw new AuthenticationException("Invalid username or password entered.");
}
if (user.HasPermission(PermissionKind.IsDisabled))
{
_logger.LogInformation(
"Authentication request for {UserName} has been denied because this account is currently disabled (IP: {IP}).",
username,
remoteEndPoint);
throw new SecurityException(
$"The {user.Username} account is currently disabled. Please consult with your administrator.");
}
if (!user.HasPermission(PermissionKind.EnableRemoteAccess) &&
!_networkManager.IsInLocalNetwork(remoteEndPoint))
{
_logger.LogInformation(
"Authentication request for {UserName} forbidden: remote access disabled and user not in local network (IP: {IP}).",
username,
remoteEndPoint);
throw new SecurityException("Forbidden.");
}
if (!user.IsParentalScheduleAllowed())
{
_logger.LogInformation(
"Authentication request for {UserName} is not allowed at this time due parental restrictions (IP: {IP}).",
username,
remoteEndPoint);
throw new SecurityException("User is not allowed access at this time.");
}
// Update LastActivityDate and LastLoginDate, then save
if (success)
{
if (isUserSession)
{
user.LastActivityDate = user.LastLoginDate = DateTime.UtcNow;
}
user.InvalidLoginAttemptCount = 0;
await UpdateUserAsync(user).ConfigureAwait(false);
_logger.LogInformation("Authentication request for {UserName} has succeeded.", user.Username);
}
else
{
IncrementInvalidLoginAttemptCount(user);
_logger.LogInformation(
"Authentication request for {UserName} has been denied (IP: {IP}).",
user.Username,
remoteEndPoint);
}
return success ? user : null;
}
/// <inheritdoc/>
public async Task<ForgotPasswordResult> StartForgotPasswordProcess(string enteredUsername, bool isInNetwork)
{
var user = string.IsNullOrWhiteSpace(enteredUsername) ? null : GetUserByName(enteredUsername);
if (user != null && isInNetwork)
{
var passwordResetProvider = GetPasswordResetProvider(user);
return await passwordResetProvider.StartForgotPasswordProcess(user, isInNetwork).ConfigureAwait(false);
}
return new ForgotPasswordResult
{
Action = ForgotPasswordAction.InNetworkRequired,
PinFile = string.Empty
};
}
/// <inheritdoc/>
public async Task<PinRedeemResult> RedeemPasswordResetPin(string pin)
{
foreach (var provider in _passwordResetProviders)
{
var result = await provider.RedeemPasswordResetPin(pin).ConfigureAwait(false);
if (result.Success)
{
return result;
}
}
return new PinRedeemResult
{
Success = false,
UsersReset = Array.Empty<string>()
};
}
/// <inheritdoc/>
public void AddParts(IEnumerable<IAuthenticationProvider> authenticationProviders, IEnumerable<IPasswordResetProvider> passwordResetProviders)
{
_authenticationProviders = authenticationProviders.ToArray();
_passwordResetProviders = passwordResetProviders.ToArray();
_invalidAuthProvider = _authenticationProviders.OfType<InvalidAuthProvider>().First();
_defaultAuthenticationProvider = _authenticationProviders.OfType<DefaultAuthenticationProvider>().First();
_defaultPasswordResetProvider = _passwordResetProviders.OfType<DefaultPasswordResetProvider>().First();
}
/// <inheritdoc />
public void Initialize()
{
// TODO: Refactor the startup wizard so that it doesn't require a user to already exist.
var dbContext = _dbProvider.CreateContext();
if (dbContext.Users.Any())
{
return;
}
var defaultName = Environment.UserName;
if (string.IsNullOrWhiteSpace(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 = CreateUser(defaultName);
newUser.SetPermission(PermissionKind.IsAdministrator, true);
newUser.SetPermission(PermissionKind.EnableContentDeletion, true);
newUser.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, true);
dbContext.Users.Update(newUser);
dbContext.SaveChanges();
}
/// <inheritdoc/>
public NameIdPair[] GetAuthenticationProviders()
{
return _authenticationProviders
.Where(provider => provider.IsEnabled)
.OrderBy(i => i is DefaultAuthenticationProvider ? 0 : 1)
.ThenBy(i => i.Name)
.Select(i => new NameIdPair
{
Name = i.Name,
Id = i.GetType().FullName
})
.ToArray();
}
/// <inheritdoc/>
public NameIdPair[] GetPasswordResetProviders()
{
return _passwordResetProviders
.Where(provider => provider.IsEnabled)
.OrderBy(i => i is DefaultPasswordResetProvider ? 0 : 1)
.ThenBy(i => i.Name)
.Select(i => new NameIdPair
{
Name = i.Name,
Id = i.GetType().FullName
})
.ToArray();
}
/// <inheritdoc/>
public void UpdateConfiguration(Guid userId, UserConfiguration config)
{
var dbContext = _dbProvider.CreateContext();
var user = dbContext.Users.Find(userId) ?? throw new ArgumentException("No user exists with given Id!");
user.SubtitleMode = config.SubtitleMode;
user.HidePlayedInLatest = config.HidePlayedInLatest;
user.EnableLocalPassword = config.EnableLocalPassword;
user.PlayDefaultAudioTrack = config.PlayDefaultAudioTrack;
user.DisplayCollectionsView = config.DisplayCollectionsView;
user.DisplayMissingEpisodes = config.DisplayMissingEpisodes;
user.AudioLanguagePreference = config.AudioLanguagePreference;
user.RememberAudioSelections = config.RememberAudioSelections;
user.EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay;
user.RememberSubtitleSelections = config.RememberSubtitleSelections;
user.SubtitleLanguagePreference = config.SubtitleLanguagePreference;
user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
dbContext.Update(user);
dbContext.SaveChanges();
}
/// <inheritdoc/>
public void UpdatePolicy(Guid userId, UserPolicy policy)
{
var dbContext = _dbProvider.CreateContext();
var user = dbContext.Users.Find(userId) ?? throw new ArgumentException("No user exists with given Id!");
// The default number of login attempts is 3, but for some god forsaken reason it's sent to the server as "0"
int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
{
-1 => null,
0 => 3,
_ => policy.LoginAttemptsBeforeLockout
};
user.MaxParentalAgeRating = policy.MaxParentalRating;
user.EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess;
user.RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit;
user.AuthenticationProviderId = policy.AuthenticationProviderId;
user.PasswordResetProviderId = policy.PasswordResetProviderId;
user.InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount;
user.LoginAttemptsBeforeLockout = maxLoginAttempts;
user.SyncPlayAccess = policy.SyncPlayAccess;
user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding);
user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding);
user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
user.AccessSchedules.Clear();
foreach (var policyAccessSchedule in policy.AccessSchedules)
{
user.AccessSchedules.Add(policyAccessSchedule);
}
// TODO: fix this at some point
user.SetPreference(
PreferenceKind.BlockUnratedItems,
policy.BlockUnratedItems?.Select(i => i.ToString()).ToArray() ?? Array.Empty<string>());
user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
dbContext.Update(user);
dbContext.SaveChanges();
}
/// <inheritdoc/>
public void ClearProfileImage(User user)
{
var dbContext = _dbProvider.CreateContext();
dbContext.Remove(user.ProfileImage);
dbContext.SaveChanges();
}
private static bool IsValidUsername(string name)
{
// This is some regex that matches only on unicode "word" characters, as well as -, _ and @
// In theory this will cut out most if not all 'control' characters which should help minimize any weirdness
// Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.)
return Regex.IsMatch(name, @"^[\w\-'._@]*$");
}
private IAuthenticationProvider GetAuthenticationProvider(User user)
{
return GetAuthenticationProviders(user)[0];
}
private IPasswordResetProvider GetPasswordResetProvider(User user)
{
return GetPasswordResetProviders(user)[0];
}
private IList<IAuthenticationProvider> GetAuthenticationProviders(User? user)
{
var authenticationProviderId = user?.AuthenticationProviderId;
var providers = _authenticationProviders.Where(i => i.IsEnabled).ToList();
if (!string.IsNullOrEmpty(authenticationProviderId))
{
providers = providers.Where(i => string.Equals(authenticationProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase)).ToList();
}
if (providers.Count == 0)
{
// Assign the user to the InvalidAuthProvider since no configured auth provider was valid/found
_logger.LogWarning(
"User {Username} was found with invalid/missing Authentication Provider {AuthenticationProviderId}. Assigning user to InvalidAuthProvider until this is corrected",
user?.Username,
user?.AuthenticationProviderId);
providers = new List<IAuthenticationProvider>
{
_invalidAuthProvider
};
}
return providers;
}
private IList<IPasswordResetProvider> GetPasswordResetProviders(User user)
{
var passwordResetProviderId = user?.PasswordResetProviderId;
var providers = _passwordResetProviders.Where(i => i.IsEnabled).ToArray();
if (!string.IsNullOrEmpty(passwordResetProviderId))
{
providers = providers.Where(i =>
string.Equals(passwordResetProviderId, i.GetType().FullName, StringComparison.OrdinalIgnoreCase))
.ToArray();
}
if (providers.Length == 0)
{
providers = new IPasswordResetProvider[]
{
_defaultPasswordResetProvider
};
}
return providers;
}
private async Task<(IAuthenticationProvider? authenticationProvider, string username, bool success)> AuthenticateLocalUser(
string username,
string password,
User? user,
string remoteEndPoint)
{
bool success = false;
IAuthenticationProvider? authenticationProvider = null;
foreach (var provider in GetAuthenticationProviders(user))
{
var providerAuthResult =
await AuthenticateWithProvider(provider, username, password, user).ConfigureAwait(false);
var updatedUsername = providerAuthResult.username;
success = providerAuthResult.success;
if (success)
{
authenticationProvider = provider;
username = updatedUsername;
break;
}
}
if (!success
&& _networkManager.IsInLocalNetwork(remoteEndPoint)
&& user?.EnableLocalPassword == true
&& !string.IsNullOrEmpty(user.EasyPassword))
{
// Check easy password
var passwordHash = PasswordHash.Parse(user.EasyPassword);
var hash = _cryptoProvider.ComputeHash(
passwordHash.Id,
Encoding.UTF8.GetBytes(password),
passwordHash.Salt.ToArray());
success = passwordHash.Hash.SequenceEqual(hash);
}
return (authenticationProvider, username, success);
}
private async Task<(string username, bool success)> AuthenticateWithProvider(
IAuthenticationProvider provider,
string username,
string password,
User? resolvedUser)
{
try
{
var authenticationResult = provider is IRequiresResolvedUser requiresResolvedUser
? await requiresResolvedUser.Authenticate(username, password, resolvedUser).ConfigureAwait(false)
: await provider.Authenticate(username, password).ConfigureAwait(false);
if (authenticationResult.Username != username)
{
_logger.LogDebug("Authentication provider provided updated username {1}", authenticationResult.Username);
username = authenticationResult.Username;
}
return (username, true);
}
catch (AuthenticationException ex)
{
_logger.LogError(ex, "Error authenticating with provider {Provider}", provider.Name);
return (username, false);
}
}
private void IncrementInvalidLoginAttemptCount(User user)
{
user.InvalidLoginAttemptCount++;
int? maxInvalidLogins = user.LoginAttemptsBeforeLockout;
if (maxInvalidLogins.HasValue && user.InvalidLoginAttemptCount >= maxInvalidLogins)
{
user.SetPermission(PermissionKind.IsDisabled, true);
OnUserLockedOut?.Invoke(this, new GenericEventArgs<User>(user));
_logger.LogWarning(
"Disabling user {Username} due to {Attempts} unsuccessful login attempts.",
user.Username,
user.InvalidLoginAttemptCount);
}
UpdateUser(user);
}
}
}

@ -7,8 +7,10 @@ using Emby.Server.Implementations;
using Jellyfin.Drawing.Skia;
using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Activity;
using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Activity;
using MediaBrowser.Model.IO;
using Microsoft.EntityFrameworkCore;
@ -63,12 +65,15 @@ namespace Jellyfin.Server
// TODO: Set up scoping and use AddDbContextPool
serviceCollection.AddDbContext<JellyfinDb>(
options => options.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"),
ServiceLifetime.Transient);
options => options
.UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}")
.UseLazyLoadingProxies(),
ServiceLifetime.Transient);
serviceCollection.AddSingleton<JellyfinDbProvider>();
serviceCollection.AddSingleton<IActivityManager, ActivityManager>();
serviceCollection.AddSingleton<IUserManager, UserManager>();
base.RegisterServices(serviceCollection);
}
@ -79,7 +84,11 @@ namespace Jellyfin.Server
/// <inheritdoc />
protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal()
{
// Jellyfin.Server
yield return typeof(CoreAppHost).Assembly;
// Jellyfin.Server.Implementations
yield return typeof(JellyfinDb).Assembly;
}
/// <inheritdoc />

@ -19,7 +19,8 @@ namespace Jellyfin.Server.Migrations
typeof(Routines.DisableTranscodingThrottling),
typeof(Routines.CreateUserLoggingConfigFile),
typeof(Routines.MigrateActivityLogDb),
typeof(Routines.RemoveDuplicateExtras)
typeof(Routines.RemoveDuplicateExtras),
typeof(Routines.MigrateUserDb)
};
/// <summary>

@ -0,0 +1,208 @@
using System;
using System.IO;
using Emby.Server.Implementations.Data;
using Emby.Server.Implementations.Serialization;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using Jellyfin.Server.Implementations;
using Jellyfin.Server.Implementations.Users;
using MediaBrowser.Common.Json;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Users;
using Microsoft.Extensions.Logging;
using SQLitePCL.pretty;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace Jellyfin.Server.Migrations.Routines
{
/// <summary>
/// The migration routine for migrating the user database to EF Core.
/// </summary>
public class MigrateUserDb : IMigrationRoutine
{
private const string DbFilename = "users.db";
private readonly ILogger<MigrateUserDb> _logger;
private readonly IServerApplicationPaths _paths;
private readonly JellyfinDbProvider _provider;
private readonly MyXmlSerializer _xmlSerializer;
/// <summary>
/// Initializes a new instance of the <see cref="MigrateUserDb"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="paths">The server application paths.</param>
/// <param name="provider">The database provider.</param>
/// <param name="xmlSerializer">The xml serializer.</param>
public MigrateUserDb(
ILogger<MigrateUserDb> logger,
IServerApplicationPaths paths,
JellyfinDbProvider provider,
MyXmlSerializer xmlSerializer)
{
_logger = logger;
_paths = paths;
_provider = provider;
_xmlSerializer = xmlSerializer;
}
/// <inheritdoc/>
public Guid Id => Guid.Parse("5C4B82A2-F053-4009-BD05-B6FCAD82F14C");
/// <inheritdoc/>
public string Name => "MigrateUserDatabase";
/// <inheritdoc/>
public void Perform()
{
var dataPath = _paths.DataPath;
_logger.LogInformation("Migrating the user database may take a while, do not stop Jellyfin.");
using (var connection = SQLite3.Open(Path.Combine(dataPath, DbFilename), ConnectionFlags.ReadOnly, null))
{
var dbContext = _provider.CreateContext();
var queryResult = connection.Query("SELECT * FROM LocalUsersv2");
dbContext.RemoveRange(dbContext.Users);
dbContext.SaveChanges();
foreach (var entry in queryResult)
{
UserMockup mockup = JsonSerializer.Deserialize<UserMockup>(entry[2].ToBlob(), JsonDefaults.GetOptions());
var userDataDir = Path.Combine(_paths.UserConfigurationDirectoryPath, mockup.Name);
var config = File.Exists(Path.Combine(userDataDir, "config.xml"))
? (UserConfiguration)_xmlSerializer.DeserializeFromFile(typeof(UserConfiguration), Path.Combine(userDataDir, "config.xml"))
: new UserConfiguration();
var policy = File.Exists(Path.Combine(userDataDir, "policy.xml"))
? (UserPolicy)_xmlSerializer.DeserializeFromFile(typeof(UserPolicy), Path.Combine(userDataDir, "policy.xml"))
: new UserPolicy();
policy.AuthenticationProviderId = policy.AuthenticationProviderId?.Replace(
"Emby.Server.Implementations.Library",
"Jellyfin.Server.Implementations.Users",
StringComparison.Ordinal)
?? typeof(DefaultAuthenticationProvider).FullName;
policy.PasswordResetProviderId = typeof(DefaultPasswordResetProvider).FullName;
int? maxLoginAttempts = policy.LoginAttemptsBeforeLockout switch
{
-1 => null,
0 => 3,
_ => policy.LoginAttemptsBeforeLockout
};
var user = new User(mockup.Name, policy.AuthenticationProviderId, policy.PasswordResetProviderId)
{
Id = entry[1].ReadGuidFromBlob(),
InternalId = entry[0].ToInt64(),
MaxParentalAgeRating = policy.MaxParentalRating,
EnableUserPreferenceAccess = policy.EnableUserPreferenceAccess,
RemoteClientBitrateLimit = policy.RemoteClientBitrateLimit,
InvalidLoginAttemptCount = policy.InvalidLoginAttemptCount,
LoginAttemptsBeforeLockout = maxLoginAttempts,
SubtitleMode = config.SubtitleMode,
HidePlayedInLatest = config.HidePlayedInLatest,
EnableLocalPassword = config.EnableLocalPassword,
PlayDefaultAudioTrack = config.PlayDefaultAudioTrack,
DisplayCollectionsView = config.DisplayCollectionsView,
DisplayMissingEpisodes = config.DisplayMissingEpisodes,
AudioLanguagePreference = config.AudioLanguagePreference,
RememberAudioSelections = config.RememberAudioSelections,
EnableNextEpisodeAutoPlay = config.EnableNextEpisodeAutoPlay,
RememberSubtitleSelections = config.RememberSubtitleSelections,
SubtitleLanguagePreference = config.SubtitleLanguagePreference,
Password = mockup.Password,
EasyPassword = mockup.EasyPassword,
LastLoginDate = mockup.LastLoginDate,
LastActivityDate = mockup.LastActivityDate
};
if (mockup.ImageInfos.Length > 0)
{
ItemImageInfo info = mockup.ImageInfos[0];
user.ProfileImage = new ImageInfo(info.Path)
{
LastModified = info.DateModified
};
}
user.SetPermission(PermissionKind.IsAdministrator, policy.IsAdministrator);
user.SetPermission(PermissionKind.IsHidden, policy.IsHidden);
user.SetPermission(PermissionKind.IsDisabled, policy.IsDisabled);
user.SetPermission(PermissionKind.EnableSharedDeviceControl, policy.EnableSharedDeviceControl);
user.SetPermission(PermissionKind.EnableRemoteAccess, policy.EnableRemoteAccess);
user.SetPermission(PermissionKind.EnableLiveTvManagement, policy.EnableLiveTvManagement);
user.SetPermission(PermissionKind.EnableLiveTvAccess, policy.EnableLiveTvAccess);
user.SetPermission(PermissionKind.EnableMediaPlayback, policy.EnableMediaPlayback);
user.SetPermission(PermissionKind.EnableAudioPlaybackTranscoding, policy.EnableAudioPlaybackTranscoding);
user.SetPermission(PermissionKind.EnableVideoPlaybackTranscoding, policy.EnableVideoPlaybackTranscoding);
user.SetPermission(PermissionKind.EnableContentDeletion, policy.EnableContentDeletion);
user.SetPermission(PermissionKind.EnableContentDownloading, policy.EnableContentDownloading);
user.SetPermission(PermissionKind.EnableSyncTranscoding, policy.EnableSyncTranscoding);
user.SetPermission(PermissionKind.EnableMediaConversion, policy.EnableMediaConversion);
user.SetPermission(PermissionKind.EnableAllChannels, policy.EnableAllChannels);
user.SetPermission(PermissionKind.EnableAllDevices, policy.EnableAllDevices);
user.SetPermission(PermissionKind.EnableAllFolders, policy.EnableAllFolders);
user.SetPermission(PermissionKind.EnableRemoteControlOfOtherUsers, policy.EnableRemoteControlOfOtherUsers);
user.SetPermission(PermissionKind.EnablePlaybackRemuxing, policy.EnablePlaybackRemuxing);
user.SetPermission(PermissionKind.ForceRemoteSourceTranscoding, policy.ForceRemoteSourceTranscoding);
user.SetPermission(PermissionKind.EnablePublicSharing, policy.EnablePublicSharing);
foreach (var policyAccessSchedule in policy.AccessSchedules)
{
user.AccessSchedules.Add(policyAccessSchedule);
}
user.SetPreference(PreferenceKind.BlockedTags, policy.BlockedTags);
user.SetPreference(PreferenceKind.EnabledChannels, policy.EnabledChannels);
user.SetPreference(PreferenceKind.EnabledDevices, policy.EnabledDevices);
user.SetPreference(PreferenceKind.EnabledFolders, policy.EnabledFolders);
user.SetPreference(PreferenceKind.EnableContentDeletionFromFolders, policy.EnableContentDeletionFromFolders);
user.SetPreference(PreferenceKind.OrderedViews, config.OrderedViews);
user.SetPreference(PreferenceKind.GroupedFolders, config.GroupedFolders);
user.SetPreference(PreferenceKind.MyMediaExcludes, config.MyMediaExcludes);
user.SetPreference(PreferenceKind.LatestItemExcludes, config.LatestItemsExcludes);
dbContext.Users.Add(user);
}
dbContext.SaveChanges();
}
try
{
File.Move(Path.Combine(dataPath, DbFilename), Path.Combine(dataPath, DbFilename + ".old"));
var journalPath = Path.Combine(dataPath, DbFilename + "-journal");
if (File.Exists(journalPath))
{
File.Move(journalPath, Path.Combine(dataPath, DbFilename + ".old-journal"));
}
}
catch (IOException e)
{
_logger.LogError(e, "Error renaming legacy user database to 'users.db.old'");
}
}
#nullable disable
internal class UserMockup
{
public string Password { get; set; }
public string EasyPassword { get; set; }
public DateTime? LastLoginDate { get; set; }
public DateTime? LastActivityDate { get; set; }
public string Name { get; set; }
public ItemImageInfo[] ImageInfos { get; set; }
}
}
}

@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Linq;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
@ -94,8 +95,8 @@ namespace MediaBrowser.Api
var authenticatedUser = auth.User;
// If they're going to update the record of another user, they must be an administrator
if ((!userId.Equals(auth.UserId) && !authenticatedUser.Policy.IsAdministrator)
|| (restrictUserPreferences && !authenticatedUser.Policy.EnableUserPreferenceAccess))
if ((!userId.Equals(auth.UserId) && !authenticatedUser.HasPermission(PermissionKind.IsAdministrator))
|| (restrictUserPreferences && !authenticatedUser.EnableUserPreferenceAccess))
{
throw new SecurityException("Unauthorized access.");
}

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

@ -18,9 +18,11 @@ using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
using User = Jellyfin.Data.Entities.User;
namespace MediaBrowser.Api.Images
{
@ -408,14 +410,14 @@ namespace MediaBrowser.Api.Images
{
var item = _userManager.GetUserById(request.Id);
return GetImage(request, Guid.Empty, item, false);
return GetImage(request, item, false);
}
public object Head(GetUserImage request)
{
var item = _userManager.GetUserById(request.Id);
return GetImage(request, Guid.Empty, item, true);
return GetImage(request, item, true);
}
public object Get(GetItemByNameImage request)
@ -448,9 +450,9 @@ namespace MediaBrowser.Api.Images
request.Type = Enum.Parse<ImageType>(GetPathValue(3).ToString(), true);
var item = _userManager.GetUserById(id);
var user = _userManager.GetUserById(id);
return PostImage(item, request.RequestStream, request.Type, Request.ContentType);
return PostImage(user, request.RequestStream, Request.ContentType);
}
/// <summary>
@ -477,9 +479,17 @@ namespace MediaBrowser.Api.Images
var userId = request.Id;
AssertCanUpdateUser(_authContext, _userManager, userId, true);
var item = _userManager.GetUserById(userId);
var user = _userManager.GetUserById(userId);
try
{
File.Delete(user.ProfileImage.Path);
}
catch (IOException e)
{
Logger.LogError(e, "Error deleting user profile image:");
}
item.DeleteImage(request.Type, request.Index ?? 0);
_userManager.ClearProfileImage(user);
}
/// <summary>
@ -567,14 +577,14 @@ namespace MediaBrowser.Api.Images
throw new ResourceNotFoundException(string.Format("{0} does not have an image of type {1}", item.Name, request.Type));
}
bool cropwhitespace;
bool cropWhitespace;
if (request.CropWhitespace.HasValue)
{
cropwhitespace = request.CropWhitespace.Value;
cropWhitespace = request.CropWhitespace.Value;
}
else
{
cropwhitespace = request.Type == ImageType.Logo || request.Type == ImageType.Art;
cropWhitespace = request.Type == ImageType.Logo || request.Type == ImageType.Art;
}
var outputFormats = GetOutputFormats(request);
@ -597,13 +607,90 @@ namespace MediaBrowser.Api.Images
itemId,
request,
imageInfo,
cropwhitespace,
cropWhitespace,
outputFormats,
cacheDuration,
responseHeaders,
isHeadRequest);
}
public Task<object> GetImage(ImageRequest request, User user, bool isHeadRequest)
{
var imageInfo = GetImageInfo(request, user);
TimeSpan? cacheDuration = null;
if (!string.IsNullOrEmpty(request.Tag))
{
cacheDuration = TimeSpan.FromDays(365);
}
var responseHeaders = new Dictionary<string, string>
{
{"transferMode.dlna.org", "Interactive"},
{"realTimeInfo.dlna.org", "DLNA.ORG_TLAG=*"}
};
var outputFormats = GetOutputFormats(request);
return GetImageResult(user.Id,
request,
imageInfo,
outputFormats,
cacheDuration,
responseHeaders,
isHeadRequest);
}
private async Task<object> GetImageResult(
Guid itemId,
ImageRequest request,
ItemImageInfo info,
IReadOnlyCollection<ImageFormat> supportedFormats,
TimeSpan? cacheDuration,
IDictionary<string, string> headers,
bool isHeadRequest)
{
info.Type = ImageType.Profile;
var options = new ImageProcessingOptions
{
CropWhiteSpace = true,
Height = request.Height,
ImageIndex = request.Index ?? 0,
Image = info,
Item = null, // Hack alert
ItemId = itemId,
MaxHeight = request.MaxHeight,
MaxWidth = request.MaxWidth,
Quality = request.Quality ?? 100,
Width = request.Width,
AddPlayedIndicator = request.AddPlayedIndicator,
PercentPlayed = 0,
UnplayedCount = request.UnplayedCount,
Blur = request.Blur,
BackgroundColor = request.BackgroundColor,
ForegroundLayer = request.ForegroundLayer,
SupportedOutputFormats = supportedFormats
};
var imageResult = await _imageProcessor.ProcessImage(options).ConfigureAwait(false);
headers[HeaderNames.Vary] = HeaderNames.Accept;
return await ResultFactory.GetStaticFileResult(Request, new StaticFileResultOptions
{
CacheDuration = cacheDuration,
ResponseHeaders = headers,
ContentType = imageResult.Item2,
DateLastModified = imageResult.Item3,
IsHeadRequest = isHeadRequest,
Path = imageResult.Item1,
FileShare = FileShare.Read
}).ConfigureAwait(false);
}
private async Task<object> GetImageResult(
BaseItem item,
Guid itemId,
@ -741,13 +828,35 @@ namespace MediaBrowser.Api.Images
/// <param name="request">The request.</param>
/// <param name="item">The item.</param>
/// <returns>System.String.</returns>
private ItemImageInfo GetImageInfo(ImageRequest request, BaseItem item)
private static ItemImageInfo GetImageInfo(ImageRequest request, BaseItem item)
{
var index = request.Index ?? 0;
return item.GetImageInfo(request.Type, index);
}
private static ItemImageInfo GetImageInfo(ImageRequest request, User user)
{
var info = new ItemImageInfo
{
Path = user.ProfileImage.Path,
Type = ImageType.Primary,
DateModified = user.ProfileImage.LastModified,
};
if (request.Width.HasValue)
{
info.Width = request.Width.Value;
}
if (request.Height.HasValue)
{
info.Height = request.Height.Value;
}
return info;
}
/// <summary>
/// Posts the image.
/// </summary>
@ -757,23 +866,42 @@ namespace MediaBrowser.Api.Images
/// <param name="mimeType">Type of the MIME.</param>
/// <returns>Task.</returns>
public async Task PostImage(BaseItem entity, Stream inputStream, ImageType imageType, string mimeType)
{
var memoryStream = await GetMemoryStream(inputStream);
// Handle image/png; charset=utf-8
mimeType = mimeType.Split(';').FirstOrDefault();
await _providerManager.SaveImage(entity, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
entity.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
}
private static async Task<MemoryStream> GetMemoryStream(Stream inputStream)
{
using var reader = new StreamReader(inputStream);
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
var bytes = Convert.FromBase64String(text);
var memoryStream = new MemoryStream(bytes)
return new MemoryStream(bytes)
{
Position = 0
};
}
private async Task PostImage(User user, Stream inputStream, string mimeType)
{
var memoryStream = await GetMemoryStream(inputStream);
// Handle image/png; charset=utf-8
mimeType = mimeType.Split(';').FirstOrDefault();
var userDataPath = Path.Combine(ServerConfigurationManager.ApplicationPaths.UserConfigurationDirectoryPath, user.Username);
user.ProfileImage = new Jellyfin.Data.Entities.ImageInfo(Path.Combine(userDataPath, "profile" + MimeTypes.ToExtension(mimeType)));
await _providerManager.SaveImage(entity, memoryStream, mimeType, imageType, null, CancellationToken.None).ConfigureAwait(false);
entity.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
await _providerManager
.SaveImage(user, memoryStream, mimeType, user.ProfileImage.Path)
.ConfigureAwait(false);
await _userManager.UpdateUserAsync(user);
}
}
}

@ -6,6 +6,7 @@ using System.Net;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Api.Movies;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Progress;
@ -14,7 +15,6 @@ using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Entities.TV;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Providers;
@ -27,6 +27,12 @@ using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
using Book = MediaBrowser.Controller.Entities.Book;
using Episode = MediaBrowser.Controller.Entities.TV.Episode;
using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
using Series = MediaBrowser.Controller.Entities.TV.Series;
namespace MediaBrowser.Api.Library
{
@ -350,6 +356,7 @@ namespace MediaBrowser.Api.Library
_moviesServiceLogger = moviesServiceLogger;
}
// Content Types available for each Library
private string[] GetRepresentativeItemTypes(string contentType)
{
return contentType switch
@ -359,7 +366,7 @@ namespace MediaBrowser.Api.Library
CollectionType.Movies => new[] {"Movie"},
CollectionType.TvShows => new[] {"Series", "Season", "Episode"},
CollectionType.Books => new[] {"Book"},
CollectionType.Music => new[] {"MusicAlbum", "MusicArtist", "Audio", "MusicVideo"},
CollectionType.Music => new[] {"MusicArtist", "MusicAlbum", "Audio", "MusicVideo"},
CollectionType.HomeVideos => new[] {"Video", "Photo"},
CollectionType.Photos => new[] {"Video", "Photo"},
CollectionType.MusicVideos => new[] {"MusicVideo"},
@ -425,7 +432,6 @@ namespace MediaBrowser.Api.Library
return string.Equals(name, "TheTVDB", StringComparison.OrdinalIgnoreCase)
|| string.Equals(name, "Screen Grabber", StringComparison.OrdinalIgnoreCase)
|| string.Equals(name, "TheAudioDB", StringComparison.OrdinalIgnoreCase)
|| string.Equals(name, "Emby Designs", StringComparison.OrdinalIgnoreCase)
|| string.Equals(name, "Image Extractor", StringComparison.OrdinalIgnoreCase);
}
@ -763,8 +769,8 @@ namespace MediaBrowser.Api.Library
{
try
{
_activityManager.Create(new Jellyfin.Data.Entities.ActivityLog(
string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Name, item.Name),
_activityManager.Create(new ActivityLog(
string.Format(_localization.GetLocalizedString("UserDownloadingItemWithValues"), user.Username, item.Name),
"UserDownloadingContent",
auth.UserId)
{

@ -7,6 +7,7 @@ using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Api.UserLibrary;
using MediaBrowser.Common;
using MediaBrowser.Common.Configuration;
@ -859,7 +860,7 @@ namespace MediaBrowser.Api.LiveTv
throw new SecurityException("Anonymous live tv management is not allowed.");
}
if (!user.Policy.EnableLiveTvManagement)
if (!user.HasPermission(PermissionKind.EnableLiveTvManagement))
{
throw new SecurityException("The current user does not have permission to manage live tv.");
}

@ -2,11 +2,11 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Jellyfin.Data.Entities;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Net;
@ -15,6 +15,8 @@ using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging;
using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
using Movie = MediaBrowser.Controller.Entities.Movies.Movie;
namespace MediaBrowser.Api.Movies
{
@ -251,7 +253,12 @@ namespace MediaBrowser.Api.Movies
return categories.OrderBy(i => i.RecommendationType);
}
private IEnumerable<RecommendationDto> GetWithDirector(User user, IEnumerable<string> names, int itemLimit, DtoOptions dtoOptions, RecommendationType type)
private IEnumerable<RecommendationDto> GetWithDirector(
User user,
IEnumerable<string> names,
int itemLimit,
DtoOptions dtoOptions,
RecommendationType type)
{
var itemTypes = new List<string> { typeof(Movie).Name };
if (ServerConfigurationManager.Configuration.EnableExternalContentInSuggestions)

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Linq;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;

@ -7,6 +7,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Configuration;
@ -196,7 +197,7 @@ namespace MediaBrowser.Api.Playback
if (state.VideoRequest != null && !EncodingHelper.IsCopyCodec(state.OutputVideoCodec))
{
var auth = AuthorizationContext.GetAuthorizationInfo(Request);
if (auth.User != null && !auth.User.Policy.EnableVideoPlaybackTranscoding)
if (auth.User != null && !auth.User.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding))
{
ApiEntryPoint.Instance.OnTranscodeFailedToStart(outputPath, TranscodingJobType, state);

@ -9,6 +9,7 @@ using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
@ -400,21 +401,24 @@ namespace MediaBrowser.Api.Playback
if (item is Audio)
{
Logger.LogInformation("User policy for {0}. EnableAudioPlaybackTranscoding: {1}", user.Name, user.Policy.EnableAudioPlaybackTranscoding);
Logger.LogInformation(
"User policy for {0}. EnableAudioPlaybackTranscoding: {1}",
user.Username,
user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
}
else
{
Logger.LogInformation("User policy for {0}. EnablePlaybackRemuxing: {1} EnableVideoPlaybackTranscoding: {2} EnableAudioPlaybackTranscoding: {3}",
user.Name,
user.Policy.EnablePlaybackRemuxing,
user.Policy.EnableVideoPlaybackTranscoding,
user.Policy.EnableAudioPlaybackTranscoding);
user.Username,
user.HasPermission(PermissionKind.EnablePlaybackRemuxing),
user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding),
user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding));
}
// Beginning of Playback Determination: Attempt DirectPlay first
if (mediaSource.SupportsDirectPlay)
{
if (mediaSource.IsRemote && user.Policy.ForceRemoteSourceTranscoding)
if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
{
mediaSource.SupportsDirectPlay = false;
}
@ -428,14 +432,16 @@ namespace MediaBrowser.Api.Playback
if (item is Audio)
{
if (!user.Policy.EnableAudioPlaybackTranscoding)
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
{
options.ForceDirectPlay = true;
}
}
else if (item is Video)
{
if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing)
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
&& !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
&& !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
{
options.ForceDirectPlay = true;
}
@ -463,7 +469,7 @@ namespace MediaBrowser.Api.Playback
if (mediaSource.SupportsDirectStream)
{
if (mediaSource.IsRemote && user.Policy.ForceRemoteSourceTranscoding)
if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
{
mediaSource.SupportsDirectStream = false;
}
@ -473,14 +479,16 @@ namespace MediaBrowser.Api.Playback
if (item is Audio)
{
if (!user.Policy.EnableAudioPlaybackTranscoding)
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding))
{
options.ForceDirectStream = true;
}
}
else if (item is Video)
{
if (!user.Policy.EnableAudioPlaybackTranscoding && !user.Policy.EnableVideoPlaybackTranscoding && !user.Policy.EnablePlaybackRemuxing)
if (!user.HasPermission(PermissionKind.EnableAudioPlaybackTranscoding)
&& !user.HasPermission(PermissionKind.EnableVideoPlaybackTranscoding)
&& !user.HasPermission(PermissionKind.EnablePlaybackRemuxing))
{
options.ForceDirectStream = true;
}
@ -512,7 +520,7 @@ namespace MediaBrowser.Api.Playback
? streamBuilder.BuildAudioItem(options)
: streamBuilder.BuildVideoItem(options);
if (mediaSource.IsRemote && user.Policy.ForceRemoteSourceTranscoding)
if (mediaSource.IsRemote && user.HasPermission(PermissionKind.ForceRemoteSourceTranscoding))
{
if (streamInfo != null)
{
@ -576,10 +584,10 @@ namespace MediaBrowser.Api.Playback
}
}
private long? GetMaxBitrate(long? clientMaxBitrate, User user)
private long? GetMaxBitrate(long? clientMaxBitrate, Jellyfin.Data.Entities.User user)
{
var maxBitrate = clientMaxBitrate;
var remoteClientMaxBitrate = user?.Policy.RemoteClientBitrateLimit ?? 0;
var remoteClientMaxBitrate = user?.RemoteClientBitrateLimit ?? 0;
if (remoteClientMaxBitrate <= 0)
{

@ -2,6 +2,7 @@ using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Devices;
using MediaBrowser.Controller.Library;
@ -326,12 +327,12 @@ namespace MediaBrowser.Api.Sessions
var user = _userManager.GetUserById(request.ControllableByUserId);
if (!user.Policy.EnableRemoteControlOfOtherUsers)
if (!user.HasPermission(PermissionKind.EnableRemoteControlOfOtherUsers))
{
result = result.Where(i => i.UserId.Equals(Guid.Empty) || i.ContainsUser(request.ControllableByUserId));
}
if (!user.Policy.EnableSharedDeviceControl)
if (!user.HasPermission(PermissionKind.EnableSharedDeviceControl))
{
result = result.Where(i => !i.UserId.Equals(Guid.Empty));
}

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

@ -378,7 +378,7 @@ namespace MediaBrowser.Api
{
var user = _userManager.GetUserById(request.UserId);
var series = GetSeries(request.Id, user);
var series = GetSeries(request.Id);
if (series == null)
{
@ -404,7 +404,7 @@ namespace MediaBrowser.Api
};
}
private Series GetSeries(string seriesId, User user)
private Series GetSeries(string seriesId)
{
if (!string.IsNullOrWhiteSpace(seriesId))
{
@ -433,7 +433,7 @@ namespace MediaBrowser.Api
}
else if (request.Season.HasValue)
{
var series = GetSeries(request.Id, user);
var series = GetSeries(request.Id);
if (series == null)
{
@ -446,7 +446,7 @@ namespace MediaBrowser.Api
}
else
{
var series = GetSeries(request.Id, user);
var series = GetSeries(request.Id);
if (series == null)
{

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

@ -2,10 +2,11 @@ using System;
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.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Model.Dto;
@ -14,6 +15,7 @@ using MediaBrowser.Model.Globalization;
using MediaBrowser.Model.Querying;
using MediaBrowser.Model.Services;
using Microsoft.Extensions.Logging;
using MusicAlbum = MediaBrowser.Controller.Entities.Audio.MusicAlbum;
namespace MediaBrowser.Api.UserLibrary
{
@ -86,7 +88,7 @@ namespace MediaBrowser.Api.UserLibrary
var ancestorIds = Array.Empty<Guid>();
var excludeFolderIds = user.Configuration.LatestItemsExcludes;
var excludeFolderIds = user.GetPreference(PreferenceKind.LatestItemExcludes);
if (parentIdGuid.Equals(Guid.Empty) && excludeFolderIds.Length > 0)
{
ancestorIds = _libraryManager.GetUserRootFolder().GetChildren(user, true)
@ -211,14 +213,14 @@ namespace MediaBrowser.Api.UserLibrary
request.IncludeItemTypes = "Playlist";
}
bool isInEnabledFolder = user.Policy.EnabledFolders.Any(i => new Guid(i) == item.Id)
bool isInEnabledFolder = user.GetPreference(PreferenceKind.EnabledFolders).Any(i => new Guid(i) == item.Id)
// Assume all folders inside an EnabledChannel are enabled
|| user.Policy.EnabledChannels.Any(i => new Guid(i) == item.Id);
|| user.GetPreference(PreferenceKind.EnabledChannels).Any(i => new Guid(i) == item.Id);
var collectionFolders = _libraryManager.GetCollectionFolders(item);
foreach (var collectionFolder in collectionFolders)
{
if (user.Policy.EnabledFolders.Contains(
if (user.GetPreference(PreferenceKind.EnabledFolders).Contains(
collectionFolder.Id.ToString("N", CultureInfo.InvariantCulture),
StringComparer.OrdinalIgnoreCase))
{
@ -226,9 +228,12 @@ namespace MediaBrowser.Api.UserLibrary
}
}
if (!(item is UserRootFolder) && !user.Policy.EnableAllFolders && !isInEnabledFolder && !user.Policy.EnableAllChannels)
if (!(item is UserRootFolder)
&& !isInEnabledFolder
&& !user.HasPermission(PermissionKind.EnableAllFolders)
&& !user.HasPermission(PermissionKind.EnableAllChannels))
{
Logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Name, item.Name);
Logger.LogWarning("{UserName} is not permitted to access Library {ItemName}.", user.Username, item.Name);
return new QueryResult<BaseItem>
{
Items = Array.Empty<BaseItem>(),

@ -1,8 +1,8 @@
using System;
using System.Globalization;
using System.Threading.Tasks;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Session;

@ -312,7 +312,7 @@ namespace MediaBrowser.Api.UserLibrary
if (!request.IsPlayed.HasValue)
{
if (user.Configuration.HidePlayedInLatest)
if (user.HidePlayedInLatest)
{
request.IsPlayed = false;
}

@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Authentication;
@ -300,12 +301,12 @@ namespace MediaBrowser.Api
if (request.IsDisabled.HasValue)
{
users = users.Where(i => i.Policy.IsDisabled == request.IsDisabled.Value);
users = users.Where(i => i.HasPermission(PermissionKind.IsDisabled) == request.IsDisabled.Value);
}
if (request.IsHidden.HasValue)
{
users = users.Where(i => i.Policy.IsHidden == request.IsHidden.Value);
users = users.Where(i => i.HasPermission(PermissionKind.IsHidden) == request.IsHidden.Value);
}
if (filterByDevice)
@ -322,12 +323,12 @@ namespace MediaBrowser.Api
{
if (!_networkManager.IsInLocalNetwork(Request.RemoteIp))
{
users = users.Where(i => i.Policy.EnableRemoteAccess);
users = users.Where(i => i.HasPermission(PermissionKind.EnableRemoteAccess));
}
}
var result = users
.OrderBy(u => u.Name)
.OrderBy(u => u.Username)
.Select(i => _userManager.GetUserDto(i, Request.RemoteIp))
.ToArray();
@ -397,7 +398,7 @@ namespace MediaBrowser.Api
// Password should always be null
return Post(new AuthenticateUserByName
{
Username = user.Name,
Username = user.Username,
Password = null,
Pw = request.Pw
});
@ -456,7 +457,12 @@ namespace MediaBrowser.Api
}
else
{
var success = await _userManager.AuthenticateUser(user.Name, request.CurrentPw, request.CurrentPassword, Request.RemoteIp, false).ConfigureAwait(false);
var success = await _userManager.AuthenticateUser(
user.Username,
request.CurrentPw,
request.CurrentPassword,
Request.RemoteIp,
false).ConfigureAwait(false);
if (success == null)
{
@ -506,10 +512,10 @@ namespace MediaBrowser.Api
var user = _userManager.GetUserById(id);
if (string.Equals(user.Name, dtoUser.Name, StringComparison.Ordinal))
if (string.Equals(user.Username, dtoUser.Name, StringComparison.Ordinal))
{
_userManager.UpdateUser(user);
_userManager.UpdateConfiguration(user, dtoUser.Configuration);
await _userManager.UpdateUserAsync(user);
_userManager.UpdateConfiguration(user.Id, dtoUser.Configuration);
}
else
{
@ -560,7 +566,6 @@ namespace MediaBrowser.Api
AssertCanUpdateUser(_authContext, _userManager, request.Id, false);
_userManager.UpdateConfiguration(request.Id, request);
}
public void Post(UpdateUserPolicy request)
@ -568,24 +573,24 @@ namespace MediaBrowser.Api
var user = _userManager.GetUserById(request.Id);
// If removing admin access
if (!request.IsAdministrator && user.Policy.IsAdministrator)
if (!request.IsAdministrator && user.HasPermission(PermissionKind.IsAdministrator))
{
if (_userManager.Users.Count(i => i.Policy.IsAdministrator) == 1)
if (_userManager.Users.Count(i => i.HasPermission(PermissionKind.IsAdministrator)) == 1)
{
throw new ArgumentException("There must be at least one user in the system with administrative access.");
}
}
// If disabling
if (request.IsDisabled && user.Policy.IsAdministrator)
if (request.IsDisabled && user.HasPermission(PermissionKind.IsAdministrator))
{
throw new ArgumentException("Administrators cannot be disabled.");
}
// If disabling
if (request.IsDisabled && !user.Policy.IsDisabled)
if (request.IsDisabled && !user.HasPermission(PermissionKind.IsDisabled))
{
if (_userManager.Users.Count(i => !i.Policy.IsDisabled) == 1)
if (_userManager.Users.Count(i => !i.HasPermission(PermissionKind.IsDisabled)) == 1)
{
throw new ArgumentException("There must be at least one enabled user in the system.");
}
@ -594,7 +599,7 @@ namespace MediaBrowser.Api
_sessionMananger.RevokeUserTokens(user.Id, currentToken);
}
_userManager.UpdateUserPolicy(request.Id, request);
_userManager.UpdatePolicy(request.Id, request);
}
}
}

@ -1,5 +1,5 @@
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
using Jellyfin.Data.Entities;
using MediaBrowser.Model.Users;
namespace MediaBrowser.Controller.Authentication

@ -1,6 +1,6 @@
using System;
using System.Threading.Tasks;
using MediaBrowser.Controller.Entities;
using Jellyfin.Data.Entities;
using MediaBrowser.Model.Users;
namespace MediaBrowser.Controller.Authentication

@ -3,6 +3,8 @@ using System.Globalization;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using Jellyfin.Data.Entities;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Querying;
@ -13,16 +15,18 @@ namespace MediaBrowser.Controller.Channels
{
public override bool IsVisible(User user)
{
if (user.Policy.BlockedChannels != null)
if (user.GetPreference(PreferenceKind.BlockedChannels) != null)
{
if (user.Policy.BlockedChannels.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
if (user.GetPreference(PreferenceKind.BlockedChannels).Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
{
return false;
}
}
else
{
if (!user.Policy.EnableAllChannels && !user.Policy.EnabledChannels.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
if (!user.HasPermission(PermissionKind.EnableAllChannels)
&& !user.GetPreference(PreferenceKind.EnabledChannels)
.Contains(Id.ToString("N", CultureInfo.InvariantCulture), StringComparer.OrdinalIgnoreCase))
{
return false;
}

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Jellyfin.Data.Entities;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Movies;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save