Merge remote-tracking branch 'remotes/upstream/api-migration' into api-sessionservice

# Conflicts:
#	MediaBrowser.Api/Sessions/SessionService.cs
pull/3324/head
David 4 years ago
commit 42385a14f0

@ -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;
@ -38,9 +39,9 @@ namespace Emby.Server.Implementations.HttpServer.Security
_networkManager = networkManager;
}
public void Authenticate(IRequest request, IAuthenticationAttributes authAttribtues)
public void Authenticate(IRequest request, IAuthenticationAttributes authAttributes)
{
ValidateUser(request, authAttribtues);
ValidateUser(request, authAttributes);
}
public User Authenticate(HttpRequest request, IAuthenticationAttributes authAttributes)
@ -50,17 +51,33 @@ namespace Emby.Server.Implementations.HttpServer.Security
return user;
}
private User ValidateUser(IRequest request, IAuthenticationAttributes authAttribtues)
public AuthorizationInfo Authenticate(HttpRequest request)
{
var auth = _authorizationContext.GetAuthorizationInfo(request);
if (auth?.User == null)
{
return null;
}
if (auth.User.HasPermission(PermissionKind.IsDisabled))
{
throw new SecurityException("User account has been disabled.");
}
return auth;
}
private User ValidateUser(IRequest request, IAuthenticationAttributes authAttributes)
{
// This code is executed before the service
var auth = _authorizationContext.GetAuthorizationInfo(request);
if (!IsExemptFromAuthenticationToken(authAttribtues, request))
if (!IsExemptFromAuthenticationToken(authAttributes, request))
{
ValidateSecurityToken(request, auth.Token);
}
if (authAttribtues.AllowLocalOnly && !request.IsLocal)
if (authAttributes.AllowLocalOnly && !request.IsLocal)
{
throw new SecurityException("Operation not found.");
}
@ -74,14 +91,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (user != null)
{
ValidateUserAccess(user, request, authAttribtues, auth);
ValidateUserAccess(user, request, authAttributes);
}
var info = GetTokenInfo(request);
if (!IsExemptFromRoles(auth, authAttribtues, request, info))
if (!IsExemptFromRoles(auth, authAttributes, request, info))
{
var roles = authAttribtues.GetRoles();
var roles = authAttributes.GetRoles();
ValidateRoles(roles, user);
}
@ -90,7 +107,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 +122,20 @@ namespace Emby.Server.Implementations.HttpServer.Security
private void ValidateUserAccess(
User user,
IRequest request,
IAuthenticationAttributes authAttribtues,
AuthorizationInfo auth)
IAuthenticationAttributes authAttributes)
{
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 +203,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 +211,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 +219,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.");
}
@ -228,16 +245,6 @@ namespace Emby.Server.Implementations.HttpServer.Security
{
throw new AuthenticationException("Access token is invalid or expired.");
}
//if (!string.IsNullOrEmpty(info.UserId))
//{
// var user = _userManager.GetUserById(info.UserId);
// if (user == null || user.Configuration.IsDisabled)
// {
// throw new SecurityException("User account has been disabled.");
// }
//}
}
}
}

@ -8,6 +8,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Security;
using MediaBrowser.Model.Services;
using Microsoft.AspNetCore.Http;
using Microsoft.Net.Http.Headers;
namespace Emby.Server.Implementations.HttpServer.Security
@ -38,6 +39,14 @@ namespace Emby.Server.Implementations.HttpServer.Security
return GetAuthorization(requestContext);
}
public AuthorizationInfo GetAuthorizationInfo(HttpRequest requestContext)
{
var auth = GetAuthorizationDictionary(requestContext);
var (authInfo, _) =
GetAuthorizationInfoFromDictionary(auth, requestContext.Headers, requestContext.Query);
return authInfo;
}
/// <summary>
/// Gets the authorization.
/// </summary>
@ -46,7 +55,23 @@ namespace Emby.Server.Implementations.HttpServer.Security
private AuthorizationInfo GetAuthorization(IRequest httpReq)
{
var auth = GetAuthorizationDictionary(httpReq);
var (authInfo, originalAuthInfo) =
GetAuthorizationInfoFromDictionary(auth, httpReq.Headers, httpReq.QueryString);
if (originalAuthInfo != null)
{
httpReq.Items["OriginalAuthenticationInfo"] = originalAuthInfo;
}
httpReq.Items["AuthorizationInfo"] = authInfo;
return authInfo;
}
private (AuthorizationInfo authInfo, AuthenticationInfo originalAuthenticationInfo) GetAuthorizationInfoFromDictionary(
in Dictionary<string, string> auth,
in IHeaderDictionary headers,
in IQueryCollection queryString)
{
string deviceId = null;
string device = null;
string client = null;
@ -64,19 +89,26 @@ namespace Emby.Server.Implementations.HttpServer.Security
if (string.IsNullOrEmpty(token))
{
token = httpReq.Headers["X-Emby-Token"];
token = headers["X-Emby-Token"];
}
if (string.IsNullOrEmpty(token))
{
token = httpReq.Headers["X-MediaBrowser-Token"];
token = headers["X-MediaBrowser-Token"];
}
if (string.IsNullOrEmpty(token))
{
token = queryString["ApiKey"];
}
// TODO deprecate this query parameter.
if (string.IsNullOrEmpty(token))
{
token = httpReq.QueryString["api_key"];
token = queryString["api_key"];
}
var info = new AuthorizationInfo
var authInfo = new AuthorizationInfo
{
Client = client,
Device = device,
@ -85,6 +117,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
Token = token
};
AuthenticationInfo originalAuthenticationInfo = null;
if (!string.IsNullOrWhiteSpace(token))
{
var result = _authRepo.Get(new AuthenticationInfoQuery
@ -92,81 +125,77 @@ namespace Emby.Server.Implementations.HttpServer.Security
AccessToken = token
});
var tokenInfo = result.Items.Count > 0 ? result.Items[0] : null;
originalAuthenticationInfo = result.Items.Count > 0 ? result.Items[0] : null;
if (tokenInfo != null)
if (originalAuthenticationInfo != null)
{
var updateToken = false;
// TODO: Remove these checks for IsNullOrWhiteSpace
if (string.IsNullOrWhiteSpace(info.Client))
if (string.IsNullOrWhiteSpace(authInfo.Client))
{
info.Client = tokenInfo.AppName;
authInfo.Client = originalAuthenticationInfo.AppName;
}
if (string.IsNullOrWhiteSpace(info.DeviceId))
if (string.IsNullOrWhiteSpace(authInfo.DeviceId))
{
info.DeviceId = tokenInfo.DeviceId;
authInfo.DeviceId = originalAuthenticationInfo.DeviceId;
}
// Temporary. TODO - allow clients to specify that the token has been shared with a casting device
var allowTokenInfoUpdate = info.Client == null || info.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
var allowTokenInfoUpdate = authInfo.Client == null || authInfo.Client.IndexOf("chromecast", StringComparison.OrdinalIgnoreCase) == -1;
if (string.IsNullOrWhiteSpace(info.Device))
if (string.IsNullOrWhiteSpace(authInfo.Device))
{
info.Device = tokenInfo.DeviceName;
authInfo.Device = originalAuthenticationInfo.DeviceName;
}
else if (!string.Equals(info.Device, tokenInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
else if (!string.Equals(authInfo.Device, originalAuthenticationInfo.DeviceName, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{
updateToken = true;
tokenInfo.DeviceName = info.Device;
originalAuthenticationInfo.DeviceName = authInfo.Device;
}
}
if (string.IsNullOrWhiteSpace(info.Version))
if (string.IsNullOrWhiteSpace(authInfo.Version))
{
info.Version = tokenInfo.AppVersion;
authInfo.Version = originalAuthenticationInfo.AppVersion;
}
else if (!string.Equals(info.Version, tokenInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
else if (!string.Equals(authInfo.Version, originalAuthenticationInfo.AppVersion, StringComparison.OrdinalIgnoreCase))
{
if (allowTokenInfoUpdate)
{
updateToken = true;
tokenInfo.AppVersion = info.Version;
originalAuthenticationInfo.AppVersion = authInfo.Version;
}
}
if ((DateTime.UtcNow - tokenInfo.DateLastActivity).TotalMinutes > 3)
if ((DateTime.UtcNow - originalAuthenticationInfo.DateLastActivity).TotalMinutes > 3)
{
tokenInfo.DateLastActivity = DateTime.UtcNow;
originalAuthenticationInfo.DateLastActivity = DateTime.UtcNow;
updateToken = true;
}
if (!tokenInfo.UserId.Equals(Guid.Empty))
if (!originalAuthenticationInfo.UserId.Equals(Guid.Empty))
{
info.User = _userManager.GetUserById(tokenInfo.UserId);
authInfo.User = _userManager.GetUserById(originalAuthenticationInfo.UserId);
if (info.User != null && !string.Equals(info.User.Name, tokenInfo.UserName, StringComparison.OrdinalIgnoreCase))
if (authInfo.User != null && !string.Equals(authInfo.User.Username, originalAuthenticationInfo.UserName, StringComparison.OrdinalIgnoreCase))
{
tokenInfo.UserName = info.User.Name;
originalAuthenticationInfo.UserName = authInfo.User.Username;
updateToken = true;
}
}
if (updateToken)
{
_authRepo.Update(tokenInfo);
_authRepo.Update(originalAuthenticationInfo);
}
}
httpReq.Items["OriginalAuthenticationInfo"] = tokenInfo;
}
httpReq.Items["AuthorizationInfo"] = info;
return info;
return (authInfo, originalAuthenticationInfo);
}
/// <summary>
@ -186,6 +215,23 @@ namespace Emby.Server.Implementations.HttpServer.Security
return GetAuthorization(auth);
}
/// <summary>
/// Gets the auth.
/// </summary>
/// <param name="httpReq">The HTTP req.</param>
/// <returns>Dictionary{System.StringSystem.String}.</returns>
private Dictionary<string, string> GetAuthorizationDictionary(HttpRequest httpReq)
{
var auth = httpReq.Headers["X-Emby-Authorization"];
if (string.IsNullOrEmpty(auth))
{
auth = httpReq.Headers[HeaderNames.Authorization];
}
return GetAuthorization(auth);
}
/// <summary>
/// Gets the authorization.
/// </summary>
@ -236,12 +282,7 @@ namespace Emby.Server.Implementations.HttpServer.Security
private static string NormalizeValue(string value)
{
if (string.IsNullOrEmpty(value))
{
return value;
}
return WebUtility.HtmlEncode(value);
return string.IsNullOrEmpty(value) ? value : WebUtility.HtmlEncode(value);
}
}
}

@ -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();

@ -0,0 +1,102 @@
#nullable enable
using System.Net;
using System.Security.Claims;
using Jellyfin.Api.Helpers;
using Jellyfin.Data.Enums;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
namespace Jellyfin.Api.Auth
{
/// <summary>
/// Base authorization handler.
/// </summary>
/// <typeparam name="T">Type of Authorization Requirement.</typeparam>
public abstract class BaseAuthorizationHandler<T> : AuthorizationHandler<T>
where T : IAuthorizationRequirement
{
private readonly IUserManager _userManager;
private readonly INetworkManager _networkManager;
private readonly IHttpContextAccessor _httpContextAccessor;
/// <summary>
/// Initializes a new instance of the <see cref="BaseAuthorizationHandler{T}"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
protected BaseAuthorizationHandler(
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
{
_userManager = userManager;
_networkManager = networkManager;
_httpContextAccessor = httpContextAccessor;
}
/// <summary>
/// Validate authenticated claims.
/// </summary>
/// <param name="claimsPrincipal">Request claims.</param>
/// <param name="ignoreSchedule">Whether to ignore parental control.</param>
/// <param name="localAccessOnly">Whether access is to be allowed locally only.</param>
/// <returns>Validated claim status.</returns>
protected bool ValidateClaims(
ClaimsPrincipal claimsPrincipal,
bool ignoreSchedule = false,
bool localAccessOnly = false)
{
// Ensure claim has userId.
var userId = ClaimHelpers.GetUserId(claimsPrincipal);
if (userId == null)
{
return false;
}
// Ensure userId links to a valid user.
var user = _userManager.GetUserById(userId.Value);
if (user == null)
{
return false;
}
// Ensure user is not disabled.
if (user.HasPermission(PermissionKind.IsDisabled))
{
return false;
}
var ip = NormalizeIp(_httpContextAccessor.HttpContext.Connection.RemoteIpAddress).ToString();
var isInLocalNetwork = _networkManager.IsInLocalNetwork(ip);
// User cannot access remotely and user is remote
if (!user.HasPermission(PermissionKind.EnableRemoteAccess) && !isInLocalNetwork)
{
return false;
}
if (localAccessOnly && !isInLocalNetwork)
{
return false;
}
// User attempting to access out of parental control hours.
if (!ignoreSchedule
&& !user.HasPermission(PermissionKind.IsAdministrator)
&& !user.IsParentalScheduleAllowed())
{
return false;
}
return true;
}
private static IPAddress NormalizeIp(IPAddress ip)
{
return ip.IsIPv4MappedToIPv6 ? ip.MapToIPv4() : ip;
}
}
}

@ -1,8 +1,12 @@
#nullable enable
using System.Globalization;
using System.Security.Authentication;
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;
@ -38,15 +42,10 @@ namespace Jellyfin.Api.Auth
/// <inheritdoc />
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var authenticatedAttribute = new AuthenticatedAttribute
{
IgnoreLegacyAuth = true
};
try
{
var user = _authService.Authenticate(Request, authenticatedAttribute);
if (user == null)
var authorizationInfo = _authService.Authenticate(Request);
if (authorizationInfo == null)
{
return Task.FromResult(AuthenticateResult.NoResult());
// TODO return when legacy API is removed.
@ -56,11 +55,16 @@ namespace Jellyfin.Api.Auth
var claims = new[]
{
new Claim(ClaimTypes.Name, user.Name),
new Claim(
ClaimTypes.Role,
value: user.Policy.IsAdministrator ? UserRoles.Administrator : UserRoles.User)
new Claim(ClaimTypes.Name, authorizationInfo.User.Username),
new Claim(ClaimTypes.Role, value: authorizationInfo.User.HasPermission(PermissionKind.IsAdministrator) ? UserRoles.Administrator : UserRoles.User),
new Claim(InternalClaimTypes.UserId, authorizationInfo.UserId.ToString("N", CultureInfo.InvariantCulture)),
new Claim(InternalClaimTypes.DeviceId, authorizationInfo.DeviceId),
new Claim(InternalClaimTypes.Device, authorizationInfo.Device),
new Claim(InternalClaimTypes.Client, authorizationInfo.Client),
new Claim(InternalClaimTypes.Version, authorizationInfo.Version),
new Claim(InternalClaimTypes.Token, authorizationInfo.Token),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);

@ -0,0 +1,42 @@
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
{
/// <summary>
/// Default authorization handler.
/// </summary>
public class DefaultAuthorizationHandler : BaseAuthorizationHandler<DefaultAuthorizationRequirement>
{
/// <summary>
/// Initializes a new instance of the <see cref="DefaultAuthorizationHandler"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public DefaultAuthorizationHandler(
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor)
{
}
/// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, DefaultAuthorizationRequirement requirement)
{
var validated = ValidateClaims(context.User);
if (!validated)
{
context.Fail();
return Task.CompletedTask;
}
context.Succeed(requirement);
return Task.CompletedTask;
}
}
}

@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Authorization;
namespace Jellyfin.Api.Auth.DefaultAuthorizationPolicy
{
/// <summary>
/// The default authorization requirement.
/// </summary>
public class DefaultAuthorizationRequirement : IAuthorizationRequirement
{
}
}

@ -1,22 +1,33 @@
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
{
/// <summary>
/// Authorization handler for requiring first time setup or elevated privileges.
/// </summary>
public class FirstTimeSetupOrElevatedHandler : AuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
public class FirstTimeSetupOrElevatedHandler : BaseAuthorizationHandler<FirstTimeSetupOrElevatedRequirement>
{
private readonly IConfigurationManager _configurationManager;
/// <summary>
/// Initializes a new instance of the <see cref="FirstTimeSetupOrElevatedHandler" /> class.
/// </summary>
/// <param name="configurationManager">The jellyfin configuration manager.</param>
public FirstTimeSetupOrElevatedHandler(IConfigurationManager configurationManager)
/// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public FirstTimeSetupOrElevatedHandler(
IConfigurationManager configurationManager,
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor)
{
_configurationManager = configurationManager;
}
@ -27,8 +38,11 @@ namespace Jellyfin.Api.Auth.FirstTimeSetupOrElevatedPolicy
if (!_configurationManager.CommonConfiguration.IsStartupWizardCompleted)
{
context.Succeed(firstTimeSetupOrElevatedRequirement);
return Task.CompletedTask;
}
else if (context.User.IsInRole(UserRoles.Administrator))
var validated = ValidateClaims(context.User);
if (validated && context.User.IsInRole(UserRoles.Administrator))
{
context.Succeed(firstTimeSetupOrElevatedRequirement);
}

@ -0,0 +1,42 @@
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy
{
/// <summary>
/// Escape schedule controls handler.
/// </summary>
public class IgnoreScheduleHandler : BaseAuthorizationHandler<IgnoreScheduleRequirement>
{
/// <summary>
/// Initializes a new instance of the <see cref="IgnoreScheduleHandler"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public IgnoreScheduleHandler(
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor)
{
}
/// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, IgnoreScheduleRequirement requirement)
{
var validated = ValidateClaims(context.User, ignoreSchedule: true);
if (!validated)
{
context.Fail();
return Task.CompletedTask;
}
context.Succeed(requirement);
return Task.CompletedTask;
}
}
}

@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Authorization;
namespace Jellyfin.Api.Auth.IgnoreSchedulePolicy
{
/// <summary>
/// Escape schedule controls requirement.
/// </summary>
public class IgnoreScheduleRequirement : IAuthorizationRequirement
{
}
}

@ -0,0 +1,44 @@
using System.Threading.Tasks;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
namespace Jellyfin.Api.Auth.LocalAccessPolicy
{
/// <summary>
/// Local access handler.
/// </summary>
public class LocalAccessHandler : BaseAuthorizationHandler<LocalAccessRequirement>
{
/// <summary>
/// Initializes a new instance of the <see cref="LocalAccessHandler"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public LocalAccessHandler(
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor)
{
}
/// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LocalAccessRequirement requirement)
{
var validated = ValidateClaims(context.User, localAccessOnly: true);
if (!validated)
{
context.Fail();
}
else
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
}

@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Authorization;
namespace Jellyfin.Api.Auth.LocalAccessPolicy
{
/// <summary>
/// The local access authorization requirement.
/// </summary>
public class LocalAccessRequirement : IAuthorizationRequirement
{
}
}

@ -1,21 +1,43 @@
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller.Library;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
namespace Jellyfin.Api.Auth.RequiresElevationPolicy
{
/// <summary>
/// Authorization handler for requiring elevated privileges.
/// </summary>
public class RequiresElevationHandler : AuthorizationHandler<RequiresElevationRequirement>
public class RequiresElevationHandler : BaseAuthorizationHandler<RequiresElevationRequirement>
{
/// <summary>
/// Initializes a new instance of the <see cref="RequiresElevationHandler"/> class.
/// </summary>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
/// <param name="networkManager">Instance of the <see cref="INetworkManager"/> interface.</param>
/// <param name="httpContextAccessor">Instance of the <see cref="IHttpContextAccessor"/> interface.</param>
public RequiresElevationHandler(
IUserManager userManager,
INetworkManager networkManager,
IHttpContextAccessor httpContextAccessor)
: base(userManager, networkManager, httpContextAccessor)
{
}
/// <inheritdoc />
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequiresElevationRequirement requirement)
{
if (context.User.IsInRole(UserRoles.Administrator))
var validated = ValidateClaims(context.User);
if (validated && context.User.IsInRole(UserRoles.Administrator))
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
return Task.CompletedTask;
}

@ -0,0 +1,38 @@
namespace Jellyfin.Api.Constants
{
/// <summary>
/// Internal claim types for authorization.
/// </summary>
public static class InternalClaimTypes
{
/// <summary>
/// User Id.
/// </summary>
public const string UserId = "Jellyfin-UserId";
/// <summary>
/// Device Id.
/// </summary>
public const string DeviceId = "Jellyfin-DeviceId";
/// <summary>
/// Device.
/// </summary>
public const string Device = "Jellyfin-Device";
/// <summary>
/// Client.
/// </summary>
public const string Client = "Jellyfin-Client";
/// <summary>
/// Version.
/// </summary>
public const string Version = "Jellyfin-Version";
/// <summary>
/// Token.
/// </summary>
public const string Token = "Jellyfin-Token";
}
}

@ -5,6 +5,11 @@ namespace Jellyfin.Api.Constants
/// </summary>
public static class Policies
{
/// <summary>
/// Policy name for default authorization.
/// </summary>
public const string DefaultAuthorization = "DefaultAuthorization";
/// <summary>
/// Policy name for requiring first time setup or elevated privileges.
/// </summary>
@ -14,5 +19,15 @@ namespace Jellyfin.Api.Constants
/// Policy name for requiring elevated privileges.
/// </summary>
public const string RequiresElevation = "RequiresElevation";
/// <summary>
/// Policy name for allowing local access only.
/// </summary>
public const string LocalAccessOnly = "LocalAccessOnly";
/// <summary>
/// Policy name for escaping schedule controls.
/// </summary>
public const string IgnoreSchedule = "IgnoreSchedule";
}
}

@ -1,4 +1,3 @@
#nullable enable
#pragma warning disable CA1801
using System;

@ -1,5 +1,3 @@
#nullable enable
using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
@ -18,7 +16,7 @@ namespace Jellyfin.Api.Controllers
/// Configuration Controller.
/// </summary>
[Route("System")]
[Authorize]
[Authorize(Policy = Policies.DefaultAuthorization)]
public class ConfigurationController : BaseJellyfinApiController
{
private readonly IServerConfigurationManager _configurationManager;
@ -53,15 +51,15 @@ namespace Jellyfin.Api.Controllers
/// Updates application configuration.
/// </summary>
/// <param name="configuration">Configuration.</param>
/// <response code="200">Configuration updated.</response>
/// <response code="204">Configuration updated.</response>
/// <returns>Update status.</returns>
[HttpPost("Configuration")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdateConfiguration([FromBody, BindRequired] ServerConfiguration configuration)
{
_configurationManager.ReplaceConfiguration(configuration);
return Ok();
return NoContent();
}
/// <summary>
@ -81,17 +79,17 @@ namespace Jellyfin.Api.Controllers
/// Updates named configuration.
/// </summary>
/// <param name="key">Configuration key.</param>
/// <response code="200">Named configuration updated.</response>
/// <response code="204">Named configuration updated.</response>
/// <returns>Update status.</returns>
[HttpPost("Configuration/{Key}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> UpdateNamedConfiguration([FromRoute] string key)
{
var configurationType = _configurationManager.GetConfigurationType(key);
var configuration = await JsonSerializer.DeserializeAsync(Request.Body, configurationType).ConfigureAwait(false);
_configurationManager.SaveConfiguration(key, configuration);
return Ok();
return NoContent();
}
/// <summary>
@ -111,15 +109,15 @@ namespace Jellyfin.Api.Controllers
/// Updates the path to the media encoder.
/// </summary>
/// <param name="mediaEncoderPath">Media encoder path form body.</param>
/// <response code="200">Media encoder path updated.</response>
/// <response code="204">Media encoder path updated.</response>
/// <returns>Status.</returns>
[HttpPost("MediaEncoder/Path")]
[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdateMediaEncoderPath([FromForm, BindRequired] MediaEncoderPathDto mediaEncoderPath)
{
_mediaEncoder.UpdateEncoderPath(mediaEncoderPath.Path, mediaEncoderPath.PathType);
return Ok();
return NoContent();
}
}
}

@ -1,5 +1,3 @@
#nullable enable
using System;
using Jellyfin.Api.Constants;
using MediaBrowser.Controller.Devices;
@ -17,7 +15,7 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Devices Controller.
/// </summary>
[Authorize]
[Authorize(Policy = Policies.DefaultAuthorization)]
public class DevicesController : BaseJellyfinApiController
{
private readonly IDeviceManager _deviceManager;
@ -105,12 +103,12 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="id">Device Id.</param>
/// <param name="deviceOptions">Device Options.</param>
/// <response code="200">Device options updated.</response>
/// <response code="204">Device options updated.</response>
/// <response code="404">Device not found.</response>
/// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
[HttpPost("Options")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult UpdateDeviceOptions(
[FromQuery, BindRequired] string id,
@ -123,18 +121,18 @@ namespace Jellyfin.Api.Controllers
}
_deviceManager.UpdateDeviceOptions(id, deviceOptions);
return Ok();
return NoContent();
}
/// <summary>
/// Deletes a device.
/// </summary>
/// <param name="id">Device Id.</param>
/// <response code="200">Device deleted.</response>
/// <response code="204">Device deleted.</response>
/// <response code="404">Device not found.</response>
/// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the device could not be found.</returns>
[HttpDelete]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult DeleteDevice([FromQuery, BindRequired] string id)
{
var existingDevice = _deviceManager.GetDevice(id);
@ -150,7 +148,7 @@ namespace Jellyfin.Api.Controllers
_sessionManager.Logout(session);
}
return Ok();
return NoContent();
}
}
}

@ -0,0 +1,220 @@
#pragma warning disable CA1801
using System;
using System.Linq;
using MediaBrowser.Controller.Dto;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Playlists;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Querying;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// Filters controller.
/// </summary>
[Authorize]
public class FilterController : BaseJellyfinApiController
{
private readonly ILibraryManager _libraryManager;
private readonly IUserManager _userManager;
/// <summary>
/// Initializes a new instance of the <see cref="FilterController"/> class.
/// </summary>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
/// <param name="userManager">Instance of the <see cref="IUserManager"/> interface.</param>
public FilterController(ILibraryManager libraryManager, IUserManager userManager)
{
_libraryManager = libraryManager;
_userManager = userManager;
}
/// <summary>
/// Gets legacy query filters.
/// </summary>
/// <param name="userId">Optional. User id.</param>
/// <param name="parentId">Optional. Parent id.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
/// <param name="mediaTypes">Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
/// <response code="200">Legacy filters retrieved.</response>
/// <returns>Legacy query filters.</returns>
[HttpGet("/Items/Filters")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryFiltersLegacy> GetQueryFiltersLegacy(
[FromQuery] Guid? userId,
[FromQuery] string? parentId,
[FromQuery] string? includeItemTypes,
[FromQuery] string? mediaTypes)
{
var parentItem = string.IsNullOrEmpty(parentId)
? null
: _libraryManager.GetItemById(parentId);
var user = userId == null || userId == Guid.Empty
? null
: _userManager.GetUserById(userId.Value);
if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
{
parentItem = null;
}
var item = string.IsNullOrEmpty(parentId)
? user == null
? _libraryManager.RootFolder
: _libraryManager.GetUserRootFolder()
: parentItem;
var query = new InternalItemsQuery
{
User = user,
MediaTypes = (mediaTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
IncludeItemTypes = (includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
Recursive = true,
EnableTotalRecordCount = false,
DtoOptions = new DtoOptions
{
Fields = new[] { ItemFields.Genres, ItemFields.Tags },
EnableImages = false,
EnableUserData = false
}
};
var itemList = ((Folder)item!).GetItemList(query);
return new QueryFiltersLegacy
{
Years = itemList.Select(i => i.ProductionYear ?? -1)
.Where(i => i > 0)
.Distinct()
.OrderBy(i => i)
.ToArray(),
Genres = itemList.SelectMany(i => i.Genres)
.DistinctNames()
.OrderBy(i => i)
.ToArray(),
Tags = itemList
.SelectMany(i => i.Tags)
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(i => i)
.ToArray(),
OfficialRatings = itemList
.Select(i => i.OfficialRating)
.Where(i => !string.IsNullOrWhiteSpace(i))
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(i => i)
.ToArray()
};
}
/// <summary>
/// Gets query filters.
/// </summary>
/// <param name="userId">Optional. User id.</param>
/// <param name="parentId">Optional. Specify this to localize the search to a specific item or folder. Omit to use the root.</param>
/// <param name="includeItemTypes">Optional. If specified, results will be filtered based on item type. This allows multiple, comma delimited.</param>
/// <param name="mediaTypes">[Unused] Optional. Filter by MediaType. Allows multiple, comma delimited.</param>
/// <param name="isAiring">Optional. Is item airing.</param>
/// <param name="isMovie">Optional. Is item movie.</param>
/// <param name="isSports">Optional. Is item sports.</param>
/// <param name="isKids">Optional. Is item kids.</param>
/// <param name="isNews">Optional. Is item news.</param>
/// <param name="isSeries">Optional. Is item series.</param>
/// <param name="recursive">Optional. Search recursive.</param>
/// <response code="200">Filters retrieved.</response>
/// <returns>Query filters.</returns>
[HttpGet("/Items/Filters2")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<QueryFilters> GetQueryFilters(
[FromQuery] Guid? userId,
[FromQuery] string? parentId,
[FromQuery] string? includeItemTypes,
[FromQuery] string? mediaTypes,
[FromQuery] bool? isAiring,
[FromQuery] bool? isMovie,
[FromQuery] bool? isSports,
[FromQuery] bool? isKids,
[FromQuery] bool? isNews,
[FromQuery] bool? isSeries,
[FromQuery] bool? recursive)
{
var parentItem = string.IsNullOrEmpty(parentId)
? null
: _libraryManager.GetItemById(parentId);
var user = userId == null || userId == Guid.Empty
? null
: _userManager.GetUserById(userId.Value);
if (string.Equals(includeItemTypes, nameof(BoxSet), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, nameof(Playlist), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, nameof(Trailer), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, "Program", StringComparison.OrdinalIgnoreCase))
{
parentItem = null;
}
var filters = new QueryFilters();
var genreQuery = new InternalItemsQuery(user)
{
IncludeItemTypes =
(includeItemTypes ?? string.Empty).Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries),
DtoOptions = new DtoOptions
{
Fields = Array.Empty<ItemFields>(),
EnableImages = false,
EnableUserData = false
},
IsAiring = isAiring,
IsMovie = isMovie,
IsSports = isSports,
IsKids = isKids,
IsNews = isNews,
IsSeries = isSeries
};
if ((recursive ?? true) || parentItem is UserView || parentItem is ICollectionFolder)
{
genreQuery.AncestorIds = parentItem == null ? Array.Empty<Guid>() : new[] { parentItem.Id };
}
else
{
genreQuery.Parent = parentItem;
}
if (string.Equals(includeItemTypes, nameof(MusicAlbum), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, nameof(MusicVideo), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, nameof(MusicArtist), StringComparison.OrdinalIgnoreCase)
|| string.Equals(includeItemTypes, nameof(Audio), StringComparison.OrdinalIgnoreCase))
{
filters.Genres = _libraryManager.GetMusicGenres(genreQuery).Items.Select(i => new NameGuidPair
{
Name = i.Item1.Name,
Id = i.Item1.Id
}).ToArray();
}
else
{
filters.Genres = _libraryManager.GetGenres(genreQuery).Items.Select(i => new NameGuidPair
{
Name = i.Item1.Name,
Id = i.Item1.Id
}).ToArray();
}
return filters;
}
}
}

@ -0,0 +1,229 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Mime;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Dto;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// Images By Name Controller.
/// </summary>
[Route("Images")]
public class ImageByNameController : BaseJellyfinApiController
{
private readonly IServerApplicationPaths _applicationPaths;
private readonly IFileSystem _fileSystem;
/// <summary>
/// Initializes a new instance of the <see cref="ImageByNameController" /> class.
/// </summary>
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager" /> interface.</param>
/// <param name="fileSystem">Instance of the <see cref="IFileSystem" /> interface.</param>
public ImageByNameController(
IServerConfigurationManager serverConfigurationManager,
IFileSystem fileSystem)
{
_applicationPaths = serverConfigurationManager.ApplicationPaths;
_fileSystem = fileSystem;
}
/// <summary>
/// Get all general images.
/// </summary>
/// <response code="200">Retrieved list of images.</response>
/// <returns>An <see cref="OkResult"/> containing the list of images.</returns>
[HttpGet("General")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<ImageByNameInfo>> GetGeneralImages()
{
return GetImageList(_applicationPaths.GeneralPath, false);
}
/// <summary>
/// Get General Image.
/// </summary>
/// <param name="name">The name of the image.</param>
/// <param name="type">Image Type (primary, backdrop, logo, etc).</param>
/// <response code="200">Image stream retrieved.</response>
/// <response code="404">Image not found.</response>
/// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
[HttpGet("General/{Name}/{Type}")]
[AllowAnonymous]
[Produces(MediaTypeNames.Application.Octet)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<FileStreamResult> GetGeneralImage([FromRoute] string name, [FromRoute] string type)
{
var filename = string.Equals(type, "primary", StringComparison.OrdinalIgnoreCase)
? "folder"
: type;
var path = BaseItem.SupportedImageExtensions
.Select(i => Path.Combine(_applicationPaths.GeneralPath, name, filename + i))
.FirstOrDefault(System.IO.File.Exists);
if (path == null)
{
return NotFound();
}
var contentType = MimeTypes.GetMimeType(path);
return File(System.IO.File.OpenRead(path), contentType);
}
/// <summary>
/// Get all general images.
/// </summary>
/// <response code="200">Retrieved list of images.</response>
/// <returns>An <see cref="OkResult"/> containing the list of images.</returns>
[HttpGet("Ratings")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<ImageByNameInfo>> GetRatingImages()
{
return GetImageList(_applicationPaths.RatingsPath, false);
}
/// <summary>
/// Get rating image.
/// </summary>
/// <param name="theme">The theme to get the image from.</param>
/// <param name="name">The name of the image.</param>
/// <response code="200">Image stream retrieved.</response>
/// <response code="404">Image not found.</response>
/// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
[HttpGet("Ratings/{Theme}/{Name}")]
[AllowAnonymous]
[Produces(MediaTypeNames.Application.Octet)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<FileStreamResult> GetRatingImage(
[FromRoute] string theme,
[FromRoute] string name)
{
return GetImageFile(_applicationPaths.RatingsPath, theme, name);
}
/// <summary>
/// Get all media info images.
/// </summary>
/// <response code="200">Image list retrieved.</response>
/// <returns>An <see cref="OkResult"/> containing the list of images.</returns>
[HttpGet("MediaInfo")]
[Authorize]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<ImageByNameInfo>> GetMediaInfoImages()
{
return GetImageList(_applicationPaths.MediaInfoImagesPath, false);
}
/// <summary>
/// Get media info image.
/// </summary>
/// <param name="theme">The theme to get the image from.</param>
/// <param name="name">The name of the image.</param>
/// <response code="200">Image stream retrieved.</response>
/// <response code="404">Image not found.</response>
/// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
[HttpGet("MediaInfo/{Theme}/{Name}")]
[AllowAnonymous]
[Produces(MediaTypeNames.Application.Octet)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<FileStreamResult> GetMediaInfoImage(
[FromRoute] string theme,
[FromRoute] string name)
{
return GetImageFile(_applicationPaths.MediaInfoImagesPath, theme, name);
}
/// <summary>
/// Internal FileHelper.
/// </summary>
/// <param name="basePath">Path to begin search.</param>
/// <param name="theme">Theme to search.</param>
/// <param name="name">File name to search for.</param>
/// <returns>A <see cref="FileStreamResult"/> containing the image contents on success, or a <see cref="NotFoundResult"/> if the image could not be found.</returns>
private ActionResult<FileStreamResult> GetImageFile(string basePath, string theme, string name)
{
var themeFolder = Path.Combine(basePath, theme);
if (Directory.Exists(themeFolder))
{
var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(themeFolder, name + i))
.FirstOrDefault(System.IO.File.Exists);
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
{
var contentType = MimeTypes.GetMimeType(path);
return File(System.IO.File.OpenRead(path), contentType);
}
}
var allFolder = Path.Combine(basePath, "all");
if (Directory.Exists(allFolder))
{
var path = BaseItem.SupportedImageExtensions.Select(i => Path.Combine(allFolder, name + i))
.FirstOrDefault(System.IO.File.Exists);
if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path))
{
var contentType = MimeTypes.GetMimeType(path);
return File(System.IO.File.OpenRead(path), contentType);
}
}
return NotFound();
}
private List<ImageByNameInfo> GetImageList(string path, bool supportsThemes)
{
try
{
return _fileSystem.GetFiles(path, BaseItem.SupportedImageExtensions, false, true)
.Select(i => new ImageByNameInfo
{
Name = _fileSystem.GetFileNameWithoutExtension(i),
FileLength = i.Length,
// For themeable images, use the Theme property
// For general images, the same object structure is fine,
// but it's not owned by a theme, so call it Context
Theme = supportsThemes ? GetThemeName(i.FullName, path) : null,
Context = supportsThemes ? null : GetThemeName(i.FullName, path),
Format = i.Extension.ToLowerInvariant().TrimStart('.')
})
.OrderBy(i => i.Name)
.ToList();
}
catch (IOException)
{
return new List<ImageByNameInfo>();
}
}
private string? GetThemeName(string path, string rootImagePath)
{
var parentName = Path.GetDirectoryName(path);
if (string.Equals(parentName, rootImagePath, StringComparison.OrdinalIgnoreCase))
{
return null;
}
parentName = Path.GetFileName(parentName);
return string.Equals(parentName, "all", StringComparison.OrdinalIgnoreCase) ? null : parentName;
}
}
}

@ -0,0 +1,88 @@
#pragma warning disable CA1801
using System.ComponentModel;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.IO;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// Item Refresh Controller.
/// </summary>
/// [Authenticated]
[Route("/Items")]
[Authorize]
public class ItemRefreshController : BaseJellyfinApiController
{
private readonly ILibraryManager _libraryManager;
private readonly IProviderManager _providerManager;
private readonly IFileSystem _fileSystem;
/// <summary>
/// Initializes a new instance of the <see cref="ItemRefreshController"/> class.
/// </summary>
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
/// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param>
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
public ItemRefreshController(
ILibraryManager libraryManager,
IProviderManager providerManager,
IFileSystem fileSystem)
{
_libraryManager = libraryManager;
_providerManager = providerManager;
_fileSystem = fileSystem;
}
/// <summary>
/// Refreshes metadata for an item.
/// </summary>
/// <param name="id">Item id.</param>
/// <param name="metadataRefreshMode">(Optional) Specifies the metadata refresh mode.</param>
/// <param name="imageRefreshMode">(Optional) Specifies the image refresh mode.</param>
/// <param name="replaceAllMetadata">(Optional) Determines if metadata should be replaced. Only applicable if mode is FullRefresh.</param>
/// <param name="replaceAllImages">(Optional) Determines if images should be replaced. Only applicable if mode is FullRefresh.</param>
/// <param name="recursive">(Unused) Indicates if the refresh should occur recursively.</param>
/// <response code="200">Item metadata refresh queued.</response>
/// <response code="404">Item to refresh not found.</response>
/// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the item could not be found.</returns>
[HttpPost("{Id}/Refresh")]
[Description("Refreshes metadata for an item.")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult Post(
[FromRoute] string id,
[FromQuery] MetadataRefreshMode metadataRefreshMode = MetadataRefreshMode.None,
[FromQuery] MetadataRefreshMode imageRefreshMode = MetadataRefreshMode.None,
[FromQuery] bool replaceAllMetadata = false,
[FromQuery] bool replaceAllImages = false,
[FromQuery] bool recursive = false)
{
var item = _libraryManager.GetItemById(id);
if (item == null)
{
return NotFound();
}
var refreshOptions = new MetadataRefreshOptions(new DirectoryService(_fileSystem))
{
MetadataRefreshMode = metadataRefreshMode,
ImageRefreshMode = imageRefreshMode,
ReplaceAllImages = replaceAllImages,
ReplaceAllMetadata = replaceAllMetadata,
ForceSave = metadataRefreshMode == MetadataRefreshMode.FullRefresh
|| imageRefreshMode == MetadataRefreshMode.FullRefresh
|| replaceAllImages
|| replaceAllMetadata,
IsAutomated = false
};
_providerManager.QueueRefresh(item.Id, refreshOptions, RefreshPriority.High);
return Ok();
}
}
}

@ -0,0 +1,341 @@
#pragma warning disable CA1801
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Progress;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// The library structure controller.
/// </summary>
[Route("/Library/VirtualFolders")]
[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
public class LibraryStructureController : BaseJellyfinApiController
{
private readonly IServerApplicationPaths _appPaths;
private readonly ILibraryManager _libraryManager;
private readonly ILibraryMonitor _libraryMonitor;
/// <summary>
/// Initializes a new instance of the <see cref="LibraryStructureController"/> class.
/// </summary>
/// <param name="serverConfigurationManager">Instance of <see cref="IServerConfigurationManager"/> interface.</param>
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
/// <param name="libraryMonitor">Instance of <see cref="ILibraryMonitor"/> interface.</param>
public LibraryStructureController(
IServerConfigurationManager serverConfigurationManager,
ILibraryManager libraryManager,
ILibraryMonitor libraryMonitor)
{
_appPaths = serverConfigurationManager.ApplicationPaths;
_libraryManager = libraryManager;
_libraryMonitor = libraryMonitor;
}
/// <summary>
/// Gets all virtual folders.
/// </summary>
/// <param name="userId">The user id.</param>
/// <response code="200">Virtual folders retrieved.</response>
/// <returns>An <see cref="IEnumerable{VirtualFolderInfo}"/> with the virtual folders.</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<VirtualFolderInfo>> GetVirtualFolders([FromQuery] string userId)
{
return _libraryManager.GetVirtualFolders(true);
}
/// <summary>
/// Adds a virtual folder.
/// </summary>
/// <param name="name">The name of the virtual folder.</param>
/// <param name="collectionType">The type of the collection.</param>
/// <param name="refreshLibrary">Whether to refresh the library.</param>
/// <param name="paths">The paths of the virtual folder.</param>
/// <param name="libraryOptions">The library options.</param>
/// <response code="204">Folder added.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> AddVirtualFolder(
[FromQuery] string name,
[FromQuery] string collectionType,
[FromQuery] bool refreshLibrary,
[FromQuery] string[] paths,
[FromQuery] LibraryOptions libraryOptions)
{
libraryOptions ??= new LibraryOptions();
if (paths != null && paths.Length > 0)
{
libraryOptions.PathInfos = paths.Select(i => new MediaPathInfo { Path = i }).ToArray();
}
await _libraryManager.AddVirtualFolder(name, collectionType, libraryOptions, refreshLibrary).ConfigureAwait(false);
return NoContent();
}
/// <summary>
/// Removes a virtual folder.
/// </summary>
/// <param name="name">The name of the folder.</param>
/// <param name="refreshLibrary">Whether to refresh the library.</param>
/// <response code="204">Folder removed.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> RemoveVirtualFolder(
[FromQuery] string name,
[FromQuery] bool refreshLibrary)
{
await _libraryManager.RemoveVirtualFolder(name, refreshLibrary).ConfigureAwait(false);
return NoContent();
}
/// <summary>
/// Renames a virtual folder.
/// </summary>
/// <param name="name">The name of the virtual folder.</param>
/// <param name="newName">The new name.</param>
/// <param name="refreshLibrary">Whether to refresh the library.</param>
/// <response code="204">Folder renamed.</response>
/// <response code="404">Library doesn't exist.</response>
/// <response code="409">Library already exists.</response>
/// <returns>A <see cref="NoContentResult"/> on success, a <see cref="NotFoundResult"/> if the library doesn't exist, a <see cref="ConflictResult"/> if the new name is already taken.</returns>
/// <exception cref="ArgumentNullException">The new name may not be null.</exception>
[HttpPost("Name")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status409Conflict)]
public ActionResult RenameVirtualFolder(
[FromQuery] string name,
[FromQuery] string newName,
[FromQuery] bool refreshLibrary)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentNullException(nameof(name));
}
if (string.IsNullOrWhiteSpace(newName))
{
throw new ArgumentNullException(nameof(newName));
}
var rootFolderPath = _appPaths.DefaultUserViewsPath;
var currentPath = Path.Combine(rootFolderPath, name);
var newPath = Path.Combine(rootFolderPath, newName);
if (!Directory.Exists(currentPath))
{
return NotFound("The media collection does not exist.");
}
if (!string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase) && Directory.Exists(newPath))
{
return Conflict($"The media library already exists at {newPath}.");
}
_libraryMonitor.Stop();
try
{
// Changing capitalization. Handle windows case insensitivity
if (string.Equals(currentPath, newPath, StringComparison.OrdinalIgnoreCase))
{
var tempPath = Path.Combine(
rootFolderPath,
Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture));
Directory.Move(currentPath, tempPath);
currentPath = tempPath;
}
Directory.Move(currentPath, newPath);
}
finally
{
CollectionFolder.OnCollectionFolderChange();
Task.Run(async () =>
{
// No need to start if scanning the library because it will handle it
if (refreshLibrary)
{
await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
}
else
{
// Need to add a delay here or directory watchers may still pick up the changes
// Have to block here to allow exceptions to bubble
await Task.Delay(1000).ConfigureAwait(false);
_libraryMonitor.Start();
}
});
}
return NoContent();
}
/// <summary>
/// Add a media path to a library.
/// </summary>
/// <param name="name">The name of the library.</param>
/// <param name="path">The path to add.</param>
/// <param name="pathInfo">The path info.</param>
/// <param name="refreshLibrary">Whether to refresh the library.</param>
/// <returns>A <see cref="NoContentResult"/>.</returns>
/// <response code="204">Media path added.</response>
/// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
[HttpPost("Paths")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult AddMediaPath(
[FromQuery] string name,
[FromQuery] string path,
[FromQuery] MediaPathInfo pathInfo,
[FromQuery] bool refreshLibrary)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentNullException(nameof(name));
}
_libraryMonitor.Stop();
try
{
var mediaPath = pathInfo ?? new MediaPathInfo { Path = path };
_libraryManager.AddMediaPath(name, mediaPath);
}
finally
{
Task.Run(async () =>
{
// No need to start if scanning the library because it will handle it
if (refreshLibrary)
{
await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
}
else
{
// Need to add a delay here or directory watchers may still pick up the changes
// Have to block here to allow exceptions to bubble
await Task.Delay(1000).ConfigureAwait(false);
_libraryMonitor.Start();
}
});
}
return NoContent();
}
/// <summary>
/// Updates a media path.
/// </summary>
/// <param name="name">The name of the library.</param>
/// <param name="pathInfo">The path info.</param>
/// <returns>A <see cref="NoContentResult"/>.</returns>
/// <response code="204">Media path updated.</response>
/// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
[HttpPost("Paths/Update")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdateMediaPath(
[FromQuery] string name,
[FromQuery] MediaPathInfo pathInfo)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentNullException(nameof(name));
}
_libraryManager.UpdateMediaPath(name, pathInfo);
return NoContent();
}
/// <summary>
/// Remove a media path.
/// </summary>
/// <param name="name">The name of the library.</param>
/// <param name="path">The path to remove.</param>
/// <param name="refreshLibrary">Whether to refresh the library.</param>
/// <returns>A <see cref="NoContentResult"/>.</returns>
/// <response code="204">Media path removed.</response>
/// <exception cref="ArgumentNullException">The name of the library may not be empty.</exception>
[HttpDelete("Paths")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult RemoveMediaPath(
[FromQuery] string name,
[FromQuery] string path,
[FromQuery] bool refreshLibrary)
{
if (string.IsNullOrWhiteSpace(name))
{
throw new ArgumentNullException(nameof(name));
}
_libraryMonitor.Stop();
try
{
_libraryManager.RemoveMediaPath(name, path);
}
finally
{
Task.Run(async () =>
{
// No need to start if scanning the library because it will handle it
if (refreshLibrary)
{
await _libraryManager.ValidateMediaLibrary(new SimpleProgress<double>(), CancellationToken.None).ConfigureAwait(false);
}
else
{
// Need to add a delay here or directory watchers may still pick up the changes
// Have to block here to allow exceptions to bubble
await Task.Delay(1000).ConfigureAwait(false);
_libraryMonitor.Start();
}
});
}
return NoContent();
}
/// <summary>
/// Update library options.
/// </summary>
/// <param name="id">The library name.</param>
/// <param name="libraryOptions">The library options.</param>
/// <response code="204">Library updated.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("LibraryOptions")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdateLibraryOptions(
[FromQuery] string id,
[FromQuery] LibraryOptions libraryOptions)
{
var collectionFolder = (CollectionFolder)_libraryManager.GetItemById(id);
collectionFolder.UpdateLibraryOptions(libraryOptions);
return NoContent();
}
}
}

@ -0,0 +1,76 @@
using System.Collections.Generic;
using Jellyfin.Api.Constants;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.Globalization;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// Localization controller.
/// </summary>
[Authorize(Policy = Policies.FirstTimeSetupOrElevated)]
public class LocalizationController : BaseJellyfinApiController
{
private readonly ILocalizationManager _localization;
/// <summary>
/// Initializes a new instance of the <see cref="LocalizationController"/> class.
/// </summary>
/// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param>
public LocalizationController(ILocalizationManager localization)
{
_localization = localization;
}
/// <summary>
/// Gets known cultures.
/// </summary>
/// <response code="200">Known cultures returned.</response>
/// <returns>An <see cref="OkResult"/> containing the list of cultures.</returns>
[HttpGet("Cultures")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<CultureDto>> GetCultures()
{
return Ok(_localization.GetCultures());
}
/// <summary>
/// Gets known countries.
/// </summary>
/// <response code="200">Known countries returned.</response>
/// <returns>An <see cref="OkResult"/> containing the list of countries.</returns>
[HttpGet("Countries")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<CountryInfo>> GetCountries()
{
return Ok(_localization.GetCountries());
}
/// <summary>
/// Gets known parental ratings.
/// </summary>
/// <response code="200">Known parental ratings returned.</response>
/// <returns>An <see cref="OkResult"/> containing the list of parental ratings.</returns>
[HttpGet("ParentalRatings")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<ParentalRating>> GetParentalRatings()
{
return Ok(_localization.GetParentalRatings());
}
/// <summary>
/// Gets localization options.
/// </summary>
/// <response code="200">Localization options returned.</response>
/// <returns>An <see cref="OkResult"/> containing the list of localization options.</returns>
[HttpGet("Options")]
[ProducesResponseType(StatusCodes.Status200OK)]
public ActionResult<IEnumerable<LocalizationOption>> GetLocalizationOptions()
{
return Ok(_localization.GetLocalizationOptions());
}
}
}

@ -1,4 +1,3 @@
#nullable enable
#pragma warning disable CA1801
using System;
@ -6,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Jellyfin.Api.Models.NotificationDtos;
using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Notifications;
using MediaBrowser.Model.Dto;
@ -99,10 +99,10 @@ namespace Jellyfin.Api.Controllers
/// <param name="description">The description of the notification.</param>
/// <param name="url">The URL of the notification.</param>
/// <param name="level">The level of the notification.</param>
/// <response code="200">Notification sent.</response>
/// <returns>An <cref see="OkResult"/>.</returns>
/// <response code="204">Notification sent.</response>
/// <returns>A <cref see="NoContentResult"/>.</returns>
[HttpPost("Admin")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult CreateAdminNotification(
[FromQuery] string name,
[FromQuery] string description,
@ -115,13 +115,16 @@ namespace Jellyfin.Api.Controllers
Description = description,
Url = url,
Level = level ?? NotificationLevel.Normal,
UserIds = _userManager.Users.Where(i => i.Policy.IsAdministrator).Select(i => i.Id).ToArray(),
UserIds = _userManager.Users
.Where(user => user.HasPermission(PermissionKind.IsAdministrator))
.Select(user => user.Id)
.ToArray(),
Date = DateTime.UtcNow,
};
_notificationManager.SendNotification(notification, CancellationToken.None);
return Ok();
return NoContent();
}
/// <summary>
@ -129,15 +132,15 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="userId">The userID.</param>
/// <param name="ids">A comma-separated list of the IDs of notifications which should be set as read.</param>
/// <response code="200">Notifications set as read.</response>
/// <returns>An <cref see="OkResult"/>.</returns>
/// <response code="204">Notifications set as read.</response>
/// <returns>A <cref see="NoContentResult"/>.</returns>
[HttpPost("{UserID}/Read")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SetRead(
[FromRoute] string userId,
[FromQuery] string ids)
{
return Ok();
return NoContent();
}
/// <summary>
@ -145,15 +148,15 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="userId">The userID.</param>
/// <param name="ids">A comma-separated list of the IDs of notifications which should be set as unread.</param>
/// <response code="200">Notifications set as unread.</response>
/// <returns>An <cref see="OkResult"/>.</returns>
/// <response code="204">Notifications set as unread.</response>
/// <returns>A <cref see="NoContentResult"/>.</returns>
[HttpPost("{UserID}/Unread")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SetUnread(
[FromRoute] string userId,
[FromQuery] string ids)
{
return Ok();
return NoContent();
}
}
}

@ -1,5 +1,3 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
@ -18,7 +16,7 @@ namespace Jellyfin.Api.Controllers
/// Package Controller.
/// </summary>
[Route("Packages")]
[Authorize]
[Authorize(Policy = Policies.DefaultAuthorization)]
public class PackageController : BaseJellyfinApiController
{
private readonly IInstallationManager _installationManager;
@ -72,11 +70,11 @@ namespace Jellyfin.Api.Controllers
/// <param name="name">Package name.</param>
/// <param name="assemblyGuid">GUID of the associated assembly.</param>
/// <param name="version">Optional version. Defaults to latest version.</param>
/// <response code="200">Package found.</response>
/// <response code="204">Package found.</response>
/// <response code="404">Package not found.</response>
/// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the package could not be found.</returns>
/// <returns>A <see cref="NoContentResult"/> on success, or a <see cref="NotFoundResult"/> if the package could not be found.</returns>
[HttpPost("/Installed/{Name}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[Authorize(Policy = Policies.RequiresElevation)]
public async Task<ActionResult> InstallPackage(
@ -98,23 +96,24 @@ namespace Jellyfin.Api.Controllers
await _installationManager.InstallPackage(package).ConfigureAwait(false);
return Ok();
return NoContent();
}
/// <summary>
/// Cancels a package installation.
/// </summary>
/// <param name="id">Installation Id.</param>
/// <response code="200">Installation cancelled.</response>
/// <returns>An <see cref="OkResult"/> on successfully cancelling a package installation.</returns>
/// <response code="204">Installation cancelled.</response>
/// <returns>A <see cref="NoContentResult"/> on successfully cancelling a package installation.</returns>
[HttpDelete("/Installing/{id}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public IActionResult CancelPackageInstallation(
[FromRoute] [Required] string id)
{
_installationManager.CancelInstallation(new Guid(id));
return Ok();
return NoContent();
}
}
}

@ -0,0 +1,188 @@
#pragma warning disable CA1801
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Models.PluginDtos;
using MediaBrowser.Common;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Common.Updates;
using MediaBrowser.Model.Plugins;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// Plugins controller.
/// </summary>
[Authorize]
public class PluginsController : BaseJellyfinApiController
{
private readonly IApplicationHost _appHost;
private readonly IInstallationManager _installationManager;
/// <summary>
/// Initializes a new instance of the <see cref="PluginsController"/> class.
/// </summary>
/// <param name="appHost">Instance of the <see cref="IApplicationHost"/> interface.</param>
/// <param name="installationManager">Instance of the <see cref="IInstallationManager"/> interface.</param>
public PluginsController(
IApplicationHost appHost,
IInstallationManager installationManager)
{
_appHost = appHost;
_installationManager = installationManager;
}
/// <summary>
/// Gets a list of currently installed plugins.
/// </summary>
/// <param name="isAppStoreEnabled">Optional. Unused.</param>
/// <response code="200">Installed plugins returned.</response>
/// <returns>List of currently installed plugins.</returns>
[HttpGet]
public ActionResult<IEnumerable<PluginInfo>> GetPlugins([FromRoute] bool? isAppStoreEnabled)
{
return Ok(_appHost.Plugins.OrderBy(p => p.Name).Select(p => p.GetPluginInfo()));
}
/// <summary>
/// Uninstalls a plugin.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <response code="200">Plugin uninstalled.</response>
/// <response code="404">Plugin not found.</response>
/// <returns>An <see cref="OkResult"/> on success, or a <see cref="NotFoundResult"/> if the file could not be found.</returns>
[HttpDelete("{pluginId}")]
[Authorize(Policy = Policies.RequiresElevation)]
public ActionResult UninstallPlugin([FromRoute] Guid pluginId)
{
var plugin = _appHost.Plugins.FirstOrDefault(p => p.Id == pluginId);
if (plugin == null)
{
return NotFound();
}
_installationManager.UninstallPlugin(plugin);
return Ok();
}
/// <summary>
/// Gets plugin configuration.
/// </summary>
/// <param name="pluginId">Plugin id.</param>
/// <response code="200">Plugin configuration returned.</response>
/// <response code="404">Plugin not found or plugin configuration not found.</response>
/// <returns>Plugin configuration.</returns>
[HttpGet("{pluginId}/Configuration")]
public ActionResult<BasePluginConfiguration> GetPluginConfiguration([FromRoute] Guid pluginId)
{
if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin))
{
return NotFound();
}
return plugin.Configuration;
}
/// <summary>
/// Updates plugin configuration.
/// </summary>
/// <remarks>
/// Accepts plugin configuration as JSON body.
/// </remarks>
/// <param name="pluginId">Plugin id.</param>
/// <response code="200">Plugin configuration updated.</response>
/// <response code="200">Plugin not found or plugin does not have configuration.</response>
/// <returns>
/// A <see cref="Task" /> that represents the asynchronous operation to update plugin configuration.
/// The task result contains an <see cref="OkResult"/> indicating success, or <see cref="NotFoundResult"/>
/// when plugin not found or plugin doesn't have configuration.
/// </returns>
[HttpPost("{pluginId}/Configuration")]
public async Task<ActionResult> UpdatePluginConfiguration([FromRoute] Guid pluginId)
{
if (!(_appHost.Plugins.FirstOrDefault(p => p.Id == pluginId) is IHasPluginConfiguration plugin))
{
return NotFound();
}
var configuration = (BasePluginConfiguration)await JsonSerializer.DeserializeAsync(Request.Body, plugin.ConfigurationType)
.ConfigureAwait(false);
plugin.UpdateConfiguration(configuration);
return Ok();
}
/// <summary>
/// Get plugin security info.
/// </summary>
/// <response code="200">Plugin security info returned.</response>
/// <returns>Plugin security info.</returns>
[Obsolete("This endpoint should not be used.")]
[HttpGet("SecurityInfo")]
public ActionResult<PluginSecurityInfo> GetPluginSecurityInfo()
{
return new PluginSecurityInfo
{
IsMbSupporter = true,
SupporterKey = "IAmTotallyLegit"
};
}
/// <summary>
/// Updates plugin security info.
/// </summary>
/// <param name="pluginSecurityInfo">Plugin security info.</param>
/// <response code="200">Plugin security info updated.</response>
/// <returns>An <see cref="OkResult"/>.</returns>
[Obsolete("This endpoint should not be used.")]
[HttpPost("SecurityInfo")]
[Authorize(Policy = Policies.RequiresElevation)]
public ActionResult UpdatePluginSecurityInfo([FromBody, BindRequired] PluginSecurityInfo pluginSecurityInfo)
{
return Ok();
}
/// <summary>
/// Gets registration status for a feature.
/// </summary>
/// <param name="name">Feature name.</param>
/// <response code="200">Registration status returned.</response>
/// <returns>Mb registration record.</returns>
[Obsolete("This endpoint should not be used.")]
[HttpPost("RegistrationRecords/{name}")]
public ActionResult<MBRegistrationRecord> GetRegistrationStatus([FromRoute] string name)
{
return new MBRegistrationRecord
{
IsRegistered = true,
RegChecked = true,
TrialVersion = false,
IsValid = true,
RegError = false
};
}
/// <summary>
/// Gets registration status for a feature.
/// </summary>
/// <param name="name">Feature name.</param>
/// <response code="501">Not implemented.</response>
/// <returns>Not Implemented.</returns>
/// <exception cref="NotImplementedException">This endpoint is not implemented.</exception>
[Obsolete("Paid plugins are not supported")]
[HttpGet("/Registrations/{name}")]
public ActionResult GetRegistration([FromRoute] string name)
{
// TODO Once we have proper apps and plugins and decide to break compatibility with paid plugins,
// delete all these registration endpoints. They are only kept for compatibility.
throw new NotImplementedException();
}
}
}

@ -0,0 +1,265 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Common.Net;
using MediaBrowser.Controller;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// Remote Images Controller.
/// </summary>
[Route("Images")]
[Authorize]
public class RemoteImageController : BaseJellyfinApiController
{
private readonly IProviderManager _providerManager;
private readonly IServerApplicationPaths _applicationPaths;
private readonly IHttpClient _httpClient;
private readonly ILibraryManager _libraryManager;
/// <summary>
/// Initializes a new instance of the <see cref="RemoteImageController"/> class.
/// </summary>
/// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param>
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
/// <param name="httpClient">Instance of the <see cref="IHttpClient"/> interface.</param>
/// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
public RemoteImageController(
IProviderManager providerManager,
IServerApplicationPaths applicationPaths,
IHttpClient httpClient,
ILibraryManager libraryManager)
{
_providerManager = providerManager;
_applicationPaths = applicationPaths;
_httpClient = httpClient;
_libraryManager = libraryManager;
}
/// <summary>
/// Gets available remote images for an item.
/// </summary>
/// <param name="id">Item Id.</param>
/// <param name="type">The image type.</param>
/// <param name="startIndex">Optional. The record index to start at. All items with a lower index will be dropped from the results.</param>
/// <param name="limit">Optional. The maximum number of records to return.</param>
/// <param name="providerName">Optional. The image provider to use.</param>
/// <param name="includeAllLanguages">Optional. Include all languages.</param>
/// <response code="200">Remote Images returned.</response>
/// <response code="404">Item not found.</response>
/// <returns>Remote Image Result.</returns>
[HttpGet("{Id}/RemoteImages")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<RemoteImageResult>> GetRemoteImages(
[FromRoute] string id,
[FromQuery] ImageType? type,
[FromQuery] int? startIndex,
[FromQuery] int? limit,
[FromQuery] string providerName,
[FromQuery] bool includeAllLanguages)
{
var item = _libraryManager.GetItemById(id);
if (item == null)
{
return NotFound();
}
var images = await _providerManager.GetAvailableRemoteImages(
item,
new RemoteImageQuery(providerName)
{
IncludeAllLanguages = includeAllLanguages,
IncludeDisabledProviders = true,
ImageType = type
}, CancellationToken.None)
.ConfigureAwait(false);
var imageArray = images.ToArray();
var allProviders = _providerManager.GetRemoteImageProviderInfo(item);
if (type.HasValue)
{
allProviders = allProviders.Where(o => o.SupportedImages.Contains(type.Value));
}
var result = new RemoteImageResult
{
TotalRecordCount = imageArray.Length,
Providers = allProviders.Select(o => o.Name)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray()
};
if (startIndex.HasValue)
{
imageArray = imageArray.Skip(startIndex.Value).ToArray();
}
if (limit.HasValue)
{
imageArray = imageArray.Take(limit.Value).ToArray();
}
result.Images = imageArray;
return result;
}
/// <summary>
/// Gets available remote image providers for an item.
/// </summary>
/// <param name="id">Item Id.</param>
/// <response code="200">Returned remote image providers.</response>
/// <response code="404">Item not found.</response>
/// <returns>List of remote image providers.</returns>
[HttpGet("{Id}/RemoteImages/Providers")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<IEnumerable<ImageProviderInfo>> GetRemoteImageProviders([FromRoute] string id)
{
var item = _libraryManager.GetItemById(id);
if (item == null)
{
return NotFound();
}
return Ok(_providerManager.GetRemoteImageProviderInfo(item));
}
/// <summary>
/// Gets a remote image.
/// </summary>
/// <param name="imageUrl">The image url.</param>
/// <response code="200">Remote image returned.</response>
/// <response code="404">Remote image not found.</response>
/// <returns>Image Stream.</returns>
[HttpGet("Remote")]
[Produces(MediaTypeNames.Application.Octet)]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<FileStreamResult>> GetRemoteImage([FromQuery, BindRequired] string imageUrl)
{
var urlHash = imageUrl.GetMD5();
var pointerCachePath = GetFullCachePath(urlHash.ToString());
string? contentPath = null;
var hasFile = false;
try
{
contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
if (System.IO.File.Exists(contentPath))
{
hasFile = true;
}
}
catch (FileNotFoundException)
{
// The file isn't cached yet
}
catch (IOException)
{
// The file isn't cached yet
}
if (!hasFile)
{
await DownloadImage(imageUrl, urlHash, pointerCachePath).ConfigureAwait(false);
contentPath = await System.IO.File.ReadAllTextAsync(pointerCachePath).ConfigureAwait(false);
}
if (string.IsNullOrEmpty(contentPath))
{
return NotFound();
}
var contentType = MimeTypes.GetMimeType(contentPath);
return File(System.IO.File.OpenRead(contentPath), contentType);
}
/// <summary>
/// Downloads a remote image for an item.
/// </summary>
/// <param name="id">Item Id.</param>
/// <param name="type">The image type.</param>
/// <param name="imageUrl">The image url.</param>
/// <response code="200">Remote image downloaded.</response>
/// <response code="404">Remote image not found.</response>
/// <returns>Download status.</returns>
[HttpPost("{Id}/RemoteImages/Download")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult> DownloadRemoteImage(
[FromRoute] string id,
[FromQuery, BindRequired] ImageType type,
[FromQuery] string imageUrl)
{
var item = _libraryManager.GetItemById(id);
if (item == null)
{
return NotFound();
}
await _providerManager.SaveImage(item, imageUrl, type, null, CancellationToken.None)
.ConfigureAwait(false);
item.UpdateToRepository(ItemUpdateType.ImageUpdate, CancellationToken.None);
return Ok();
}
/// <summary>
/// Gets the full cache path.
/// </summary>
/// <param name="filename">The filename.</param>
/// <returns>System.String.</returns>
private string GetFullCachePath(string filename)
{
return Path.Combine(_applicationPaths.CachePath, "remote-images", filename.Substring(0, 1), filename);
}
/// <summary>
/// Downloads the image.
/// </summary>
/// <param name="url">The URL.</param>
/// <param name="urlHash">The URL hash.</param>
/// <param name="pointerCachePath">The pointer cache path.</param>
/// <returns>Task.</returns>
private async Task DownloadImage(string url, Guid urlHash, string pointerCachePath)
{
using var result = await _httpClient.GetResponse(new HttpRequestOptions
{
Url = url,
BufferContent = false
}).ConfigureAwait(false);
var ext = result.ContentType.Split('/').Last();
var fullCachePath = GetFullCachePath(urlHash + "." + ext);
Directory.CreateDirectory(Path.GetDirectoryName(fullCachePath));
await using (var stream = result.Content)
{
await using var fileStream = new FileStream(fullCachePath, FileMode.Create, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, true);
await stream.CopyToAsync(fileStream).ConfigureAwait(false);
}
Directory.CreateDirectory(Path.GetDirectoryName(pointerCachePath));
await System.IO.File.WriteAllTextAsync(pointerCachePath, fullCachePath, CancellationToken.None)
.ConfigureAwait(false);
}
}
}

@ -3,6 +3,7 @@ using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Linq;
using Jellyfin.Api.Constants;
using Jellyfin.Api.Helpers;
using MediaBrowser.Controller.Drawing;
using MediaBrowser.Controller.Dto;
@ -23,7 +24,7 @@ namespace Jellyfin.Api.Controllers
/// Search controller.
/// </summary>
[Route("/Search/Hints")]
[Authorize]
[Authorize(Policy = Policies.DefaultAuthorization)]
public class SearchController : BaseJellyfinApiController
{
private readonly ISearchEngine _searchEngine;

@ -33,16 +33,16 @@ namespace Jellyfin.Api.Controllers
/// <summary>
/// Completes the startup wizard.
/// </summary>
/// <response code="200">Startup wizard completed.</response>
/// <returns>An <see cref="OkResult"/> indicating success.</returns>
/// <response code="204">Startup wizard completed.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Complete")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult CompleteWizard()
{
_config.Configuration.IsStartupWizardCompleted = true;
_config.SetOptimalValues();
_config.SaveConfiguration();
return Ok();
return NoContent();
}
/// <summary>
@ -70,10 +70,10 @@ namespace Jellyfin.Api.Controllers
/// <param name="uiCulture">The UI language culture.</param>
/// <param name="metadataCountryCode">The metadata country code.</param>
/// <param name="preferredMetadataLanguage">The preferred language for metadata.</param>
/// <response code="200">Configuration saved.</response>
/// <returns>An <see cref="OkResult"/> indicating success.</returns>
/// <response code="204">Configuration saved.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("Configuration")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult UpdateInitialConfiguration(
[FromForm] string uiCulture,
[FromForm] string metadataCountryCode,
@ -83,7 +83,7 @@ namespace Jellyfin.Api.Controllers
_config.Configuration.MetadataCountryCode = metadataCountryCode;
_config.Configuration.PreferredMetadataLanguage = preferredMetadataLanguage;
_config.SaveConfiguration();
return Ok();
return NoContent();
}
/// <summary>
@ -91,16 +91,16 @@ namespace Jellyfin.Api.Controllers
/// </summary>
/// <param name="enableRemoteAccess">Enable remote access.</param>
/// <param name="enableAutomaticPortMapping">Enable UPnP.</param>
/// <response code="200">Configuration saved.</response>
/// <returns>An <see cref="OkResult"/> indicating success.</returns>
/// <response code="204">Configuration saved.</response>
/// <returns>A <see cref="NoContentResult"/> indicating success.</returns>
[HttpPost("RemoteAccess")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public ActionResult SetRemoteAccess([FromForm] bool enableRemoteAccess, [FromForm] bool enableAutomaticPortMapping)
{
_config.Configuration.EnableRemoteAccess = enableRemoteAccess;
_config.Configuration.EnableUPnP = enableAutomaticPortMapping;
_config.SaveConfiguration();
return Ok();
return NoContent();
}
/// <summary>
@ -113,35 +113,41 @@ 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>
/// Sets the user name and password.
/// </summary>
/// <param name="startupUserDto">The DTO containing username and password.</param>
/// <response code="200">Updated user name and password.</response>
/// <response code="204">Updated user name and password.</response>
/// <returns>
/// A <see cref="Task" /> that represents the asynchronous update operation.
/// The task result contains an <see cref="OkResult"/> indicating success.
/// The task result contains a <see cref="NoContentResult"/> indicating success.
/// </returns>
[HttpPost("User")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> UpdateUser([FromForm] StartupUserDto startupUserDto)
{
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))
{
await _userManager.ChangePassword(user, startupUserDto.Password).ConfigureAwait(false);
}
return Ok();
return NoContent();
}
}
}

@ -0,0 +1,348 @@
#pragma warning disable CA1801
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Mime;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
using MediaBrowser.Controller.Net;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Controller.Subtitles;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.Net;
using MediaBrowser.Model.Providers;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
namespace Jellyfin.Api.Controllers
{
/// <summary>
/// Subtitle controller.
/// </summary>
public class SubtitleController : BaseJellyfinApiController
{
private readonly ILibraryManager _libraryManager;
private readonly ISubtitleManager _subtitleManager;
private readonly ISubtitleEncoder _subtitleEncoder;
private readonly IMediaSourceManager _mediaSourceManager;
private readonly IProviderManager _providerManager;
private readonly IFileSystem _fileSystem;
private readonly IAuthorizationContext _authContext;
private readonly ILogger<SubtitleController> _logger;
/// <summary>
/// Initializes a new instance of the <see cref="SubtitleController"/> class.
/// </summary>
/// <param name="libraryManager">Instance of <see cref="ILibraryManager"/> interface.</param>
/// <param name="subtitleManager">Instance of <see cref="ISubtitleManager"/> interface.</param>
/// <param name="subtitleEncoder">Instance of <see cref="ISubtitleEncoder"/> interface.</param>
/// <param name="mediaSourceManager">Instance of <see cref="IMediaSourceManager"/> interface.</param>
/// <param name="providerManager">Instance of <see cref="IProviderManager"/> interface.</param>
/// <param name="fileSystem">Instance of <see cref="IFileSystem"/> interface.</param>
/// <param name="authContext">Instance of <see cref="IAuthorizationContext"/> interface.</param>
/// <param name="logger">Instance of <see cref="ILogger{SubtitleController}"/> interface.</param>
public SubtitleController(
ILibraryManager libraryManager,
ISubtitleManager subtitleManager,
ISubtitleEncoder subtitleEncoder,
IMediaSourceManager mediaSourceManager,
IProviderManager providerManager,
IFileSystem fileSystem,
IAuthorizationContext authContext,
ILogger<SubtitleController> logger)
{
_libraryManager = libraryManager;
_subtitleManager = subtitleManager;
_subtitleEncoder = subtitleEncoder;
_mediaSourceManager = mediaSourceManager;
_providerManager = providerManager;
_fileSystem = fileSystem;
_authContext = authContext;
_logger = logger;
}
/// <summary>
/// Deletes an external subtitle file.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="index">The index of the subtitle file.</param>
/// <response code="204">Subtitle deleted.</response>
/// <response code="404">Item not found.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpDelete("/Videos/{id}/Subtitles/{index}")]
[Authorize(Policy = Policies.RequiresElevation)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public ActionResult<Task> DeleteSubtitle(
[FromRoute] Guid id,
[FromRoute] int index)
{
var item = _libraryManager.GetItemById(id);
if (item == null)
{
return NotFound();
}
_subtitleManager.DeleteSubtitles(item, index);
return NoContent();
}
/// <summary>
/// Search remote subtitles.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="language">The language of the subtitles.</param>
/// <param name="isPerfectMatch">Optional. Only show subtitles which are a perfect match.</param>
/// <response code="200">Subtitles retrieved.</response>
/// <returns>An array of <see cref="RemoteSubtitleInfo"/>.</returns>
[HttpGet("/Items/{id}/RemoteSearch/Subtitles/{language}")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<RemoteSubtitleInfo>>> SearchRemoteSubtitles(
[FromRoute] Guid id,
[FromRoute] string language,
[FromQuery] bool? isPerfectMatch)
{
var video = (Video)_libraryManager.GetItemById(id);
return await _subtitleManager.SearchSubtitles(video, language, isPerfectMatch, CancellationToken.None).ConfigureAwait(false);
}
/// <summary>
/// Downloads a remote subtitle.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="subtitleId">The subtitle id.</param>
/// <response code="204">Subtitle downloaded.</response>
/// <returns>A <see cref="NoContentResult"/>.</returns>
[HttpPost("/Items/{id}/RemoteSearch/Subtitles/{subtitleId}")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> DownloadRemoteSubtitles(
[FromRoute] Guid id,
[FromRoute] string subtitleId)
{
var video = (Video)_libraryManager.GetItemById(id);
try
{
await _subtitleManager.DownloadSubtitles(video, subtitleId, CancellationToken.None)
.ConfigureAwait(false);
_providerManager.QueueRefresh(video.Id, new MetadataRefreshOptions(new DirectoryService(_fileSystem)), RefreshPriority.High);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error downloading subtitles");
}
return NoContent();
}
/// <summary>
/// Gets the remote subtitles.
/// </summary>
/// <param name="id">The item id.</param>
/// <response code="200">File returned.</response>
/// <returns>A <see cref="FileStreamResult"/> with the subtitle file.</returns>
[HttpGet("/Providers/Subtitles/Subtitles/{id}")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
[Produces(MediaTypeNames.Application.Octet)]
public async Task<ActionResult> GetRemoteSubtitles([FromRoute] string id)
{
var result = await _subtitleManager.GetRemoteSubtitles(id, CancellationToken.None).ConfigureAwait(false);
return File(result.Stream, MimeTypes.GetMimeType("file." + result.Format));
}
/// <summary>
/// Gets subtitles in a specified format.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="mediaSourceId">The media source id.</param>
/// <param name="index">The subtitle stream index.</param>
/// <param name="format">The format of the returned subtitle.</param>
/// <param name="startPositionTicks">Optional. The start position of the subtitle in ticks.</param>
/// <param name="endPositionTicks">Optional. The end position of the subtitle in ticks.</param>
/// <param name="copyTimestamps">Optional. Whether to copy the timestamps.</param>
/// <param name="addVttTimeMap">Optional. Whether to add a VTT time map.</param>
/// <response code="200">File returned.</response>
/// <returns>A <see cref="FileContentResult"/> with the subtitle file.</returns>
[HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/Stream.{format}")]
[HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/{startPositionTicks}/Stream.{format}")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> GetSubtitle(
[FromRoute, Required] Guid id,
[FromRoute, Required] string mediaSourceId,
[FromRoute, Required] int index,
[FromRoute, Required] string format,
[FromRoute] long startPositionTicks,
[FromQuery] long? endPositionTicks,
[FromQuery] bool copyTimestamps,
[FromQuery] bool addVttTimeMap)
{
if (string.Equals(format, "js", StringComparison.OrdinalIgnoreCase))
{
format = "json";
}
if (string.IsNullOrEmpty(format))
{
var item = (Video)_libraryManager.GetItemById(id);
var idString = id.ToString("N", CultureInfo.InvariantCulture);
var mediaSource = _mediaSourceManager.GetStaticMediaSources(item, false)
.First(i => string.Equals(i.Id, mediaSourceId ?? idString, StringComparison.Ordinal));
var subtitleStream = mediaSource.MediaStreams
.First(i => i.Type == MediaStreamType.Subtitle && i.Index == index);
FileStream stream = new FileStream(subtitleStream.Path, FileMode.Open, FileAccess.Read);
return File(stream, MimeTypes.GetMimeType(subtitleStream.Path));
}
if (string.Equals(format, "vtt", StringComparison.OrdinalIgnoreCase) && addVttTimeMap)
{
await using Stream stream = await EncodeSubtitles(id, mediaSourceId, index, format, startPositionTicks, endPositionTicks, copyTimestamps).ConfigureAwait(false);
using var reader = new StreamReader(stream);
var text = await reader.ReadToEndAsync().ConfigureAwait(false);
text = text.Replace("WEBVTT", "WEBVTT\nX-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000", StringComparison.Ordinal);
return File(Encoding.UTF8.GetBytes(text), MimeTypes.GetMimeType("file." + format));
}
return File(
await EncodeSubtitles(
id,
mediaSourceId,
index,
format,
startPositionTicks,
endPositionTicks,
copyTimestamps).ConfigureAwait(false),
MimeTypes.GetMimeType("file." + format));
}
/// <summary>
/// Gets an HLS subtitle playlist.
/// </summary>
/// <param name="id">The item id.</param>
/// <param name="index">The subtitle stream index.</param>
/// <param name="mediaSourceId">The media source id.</param>
/// <param name="segmentLength">The subtitle segment length.</param>
/// <response code="200">Subtitle playlist retrieved.</response>
/// <returns>A <see cref="FileContentResult"/> with the HLS subtitle playlist.</returns>
[HttpGet("/Videos/{id}/{mediaSourceId}/Subtitles/{index}/subtitles.m3u8")]
[Authorize(Policy = Policies.DefaultAuthorization)]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult> GetSubtitlePlaylist(
[FromRoute] Guid id,
// TODO: 'int index' is never used: CA1801 is disabled
[FromRoute] int index,
[FromRoute] string mediaSourceId,
[FromQuery, Required] int segmentLength)
{
var item = (Video)_libraryManager.GetItemById(id);
var mediaSource = await _mediaSourceManager.GetMediaSource(item, mediaSourceId, null, false, CancellationToken.None).ConfigureAwait(false);
var builder = new StringBuilder();
var runtime = mediaSource.RunTimeTicks ?? -1;
if (runtime <= 0)
{
throw new ArgumentException("HLS Subtitles are not supported for this media.");
}
var segmentLengthTicks = TimeSpan.FromSeconds(segmentLength).Ticks;
if (segmentLengthTicks <= 0)
{
throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)");
}
builder.AppendLine("#EXTM3U");
builder.AppendLine("#EXT-X-TARGETDURATION:" + segmentLength.ToString(CultureInfo.InvariantCulture));
builder.AppendLine("#EXT-X-VERSION:3");
builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0");
builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD");
long positionTicks = 0;
var accessToken = _authContext.GetAuthorizationInfo(Request).Token;
while (positionTicks < runtime)
{
var remaining = runtime - positionTicks;
var lengthTicks = Math.Min(remaining, segmentLengthTicks);
builder.AppendLine("#EXTINF:" + TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture) + ",");
var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks);
var url = string.Format(
CultureInfo.CurrentCulture,
"stream.vtt?CopyTimestamps=true&AddVttTimeMap=true&StartPositionTicks={0}&EndPositionTicks={1}&api_key={2}",
positionTicks.ToString(CultureInfo.InvariantCulture),
endPositionTicks.ToString(CultureInfo.InvariantCulture),
accessToken);
builder.AppendLine(url);
positionTicks += segmentLengthTicks;
}
builder.AppendLine("#EXT-X-ENDLIST");
return File(Encoding.UTF8.GetBytes(builder.ToString()), MimeTypes.GetMimeType("playlist.m3u8"));
}
/// <summary>
/// Encodes a subtitle in the specified format.
/// </summary>
/// <param name="id">The media id.</param>
/// <param name="mediaSourceId">The source media id.</param>
/// <param name="index">The subtitle index.</param>
/// <param name="format">The format to convert to.</param>
/// <param name="startPositionTicks">The start position in ticks.</param>
/// <param name="endPositionTicks">The end position in ticks.</param>
/// <param name="copyTimestamps">Whether to copy the timestamps.</param>
/// <returns>A <see cref="Task{Stream}"/> with the new subtitle file.</returns>
private Task<Stream> EncodeSubtitles(
Guid id,
string mediaSourceId,
int index,
string format,
long startPositionTicks,
long? endPositionTicks,
bool copyTimestamps)
{
var item = _libraryManager.GetItemById(id);
return _subtitleEncoder.GetSubtitles(
item,
mediaSourceId,
index,
format,
startPositionTicks,
endPositionTicks ?? 0,
copyTimestamps,
CancellationToken.None);
}
}
}

@ -1,9 +1,8 @@
#nullable enable
using System;
using System.Net.Mime;
using System.Threading;
using System.Threading.Tasks;
using Jellyfin.Api.Constants;
using MediaBrowser.Common.Extensions;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.MediaEncoding;
@ -17,7 +16,7 @@ namespace Jellyfin.Api.Controllers
/// Attachments controller.
/// </summary>
[Route("Videos")]
[Authorize]
[Authorize(Policy = Policies.DefaultAuthorization)]
public class VideoAttachmentsController : BaseJellyfinApiController
{
private readonly ILibraryManager _libraryManager;

@ -0,0 +1,77 @@
#nullable enable
using System;
using System.Linq;
using System.Security.Claims;
using Jellyfin.Api.Constants;
namespace Jellyfin.Api.Helpers
{
/// <summary>
/// Claim Helpers.
/// </summary>
public static class ClaimHelpers
{
/// <summary>
/// Get user id from claims.
/// </summary>
/// <param name="user">Current claims principal.</param>
/// <returns>User id.</returns>
public static Guid? GetUserId(in ClaimsPrincipal user)
{
var value = GetClaimValue(user, InternalClaimTypes.UserId);
return string.IsNullOrEmpty(value)
? null
: (Guid?)Guid.Parse(value);
}
/// <summary>
/// Get device id from claims.
/// </summary>
/// <param name="user">Current claims principal.</param>
/// <returns>Device id.</returns>
public static string? GetDeviceId(in ClaimsPrincipal user)
=> GetClaimValue(user, InternalClaimTypes.DeviceId);
/// <summary>
/// Get device from claims.
/// </summary>
/// <param name="user">Current claims principal.</param>
/// <returns>Device.</returns>
public static string? GetDevice(in ClaimsPrincipal user)
=> GetClaimValue(user, InternalClaimTypes.Device);
/// <summary>
/// Get client from claims.
/// </summary>
/// <param name="user">Current claims principal.</param>
/// <returns>Client.</returns>
public static string? GetClient(in ClaimsPrincipal user)
=> GetClaimValue(user, InternalClaimTypes.Client);
/// <summary>
/// Get version from claims.
/// </summary>
/// <param name="user">Current claims principal.</param>
/// <returns>Version.</returns>
public static string? GetVersion(in ClaimsPrincipal user)
=> GetClaimValue(user, InternalClaimTypes.Version);
/// <summary>
/// Get token from claims.
/// </summary>
/// <param name="user">Current claims principal.</param>
/// <returns>Token.</returns>
public static string? GetToken(in ClaimsPrincipal user)
=> GetClaimValue(user, InternalClaimTypes.Token);
private static string? GetClaimValue(in ClaimsPrincipal user, string name)
{
return user?.Identities
.SelectMany(c => c.Claims)
.Where(claim => claim.Type.Equals(name, StringComparison.OrdinalIgnoreCase))
.Select(claim => claim.Value)
.FirstOrDefault();
}
}
}

@ -1,5 +1,3 @@
#nullable enable
namespace Jellyfin.Api.Models.ConfigurationDtos
{
/// <summary>

@ -1,5 +1,3 @@
#nullable enable
using System;
using MediaBrowser.Model.Notifications;

@ -1,5 +1,3 @@
#nullable enable
using System;
using System.Collections.Generic;

@ -1,5 +1,3 @@
#nullable enable
using MediaBrowser.Model.Notifications;
namespace Jellyfin.Api.Models.NotificationDtos

@ -0,0 +1,40 @@
using System;
namespace Jellyfin.Api.Models.PluginDtos
{
/// <summary>
/// MB Registration Record.
/// </summary>
public class MBRegistrationRecord
{
/// <summary>
/// Gets or sets expiration date.
/// </summary>
public DateTime ExpirationDate { get; set; }
/// <summary>
/// Gets or sets a value indicating whether is registered.
/// </summary>
public bool IsRegistered { get; set; }
/// <summary>
/// Gets or sets a value indicating whether reg checked.
/// </summary>
public bool RegChecked { get; set; }
/// <summary>
/// Gets or sets a value indicating whether reg error.
/// </summary>
public bool RegError { get; set; }
/// <summary>
/// Gets or sets a value indicating whether trial version.
/// </summary>
public bool TrialVersion { get; set; }
/// <summary>
/// Gets or sets a value indicating whether is valid.
/// </summary>
public bool IsValid { get; set; }
}
}

@ -0,0 +1,18 @@
namespace Jellyfin.Api.Models.PluginDtos
{
/// <summary>
/// Plugin security info.
/// </summary>
public class PluginSecurityInfo
{
/// <summary>
/// Gets or sets the supporter key.
/// </summary>
public string? SupporterKey { get; set; }
/// <summary>
/// Gets or sets a value indicating whether is mb supporter.
/// </summary>
public bool IsMbSupporter { get; set; }
}
}

@ -1,5 +1,3 @@
#nullable disable
namespace Jellyfin.Api.Models.StartupDtos
{
/// <summary>
@ -10,16 +8,16 @@ namespace Jellyfin.Api.Models.StartupDtos
/// <summary>
/// Gets or sets UI language culture.
/// </summary>
public string UICulture { get; set; }
public string? UICulture { get; set; }
/// <summary>
/// Gets or sets the metadata country code.
/// </summary>
public string MetadataCountryCode { get; set; }
public string? MetadataCountryCode { get; set; }
/// <summary>
/// Gets or sets the preferred language for the metadata.
/// </summary>
public string PreferredMetadataLanguage { get; set; }
public string? PreferredMetadataLanguage { get; set; }
}
}

@ -1,5 +1,3 @@
#nullable disable
namespace Jellyfin.Api.Models.StartupDtos
{
/// <summary>
@ -10,11 +8,11 @@ namespace Jellyfin.Api.Models.StartupDtos
/// <summary>
/// Gets or sets the username.
/// </summary>
public string Name { get; set; }
public string? Name { get; set; }
/// <summary>
/// Gets or sets the user's password.
/// </summary>
public string Password { get; set; }
public string? Password { get; set; }
}
}

@ -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>

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

Loading…
Cancel
Save