repo creation with initial code after cloning public repo
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Controller.Authentication
|
||||
{
|
||||
/// <summary>
|
||||
/// The exception that is thrown when an attempt to authenticate fails.
|
||||
/// </summary>
|
||||
public class AuthenticationException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthenticationException"/> class.
|
||||
/// </summary>
|
||||
public AuthenticationException() : base()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthenticationException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
public AuthenticationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthenticationException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The message that describes the error.</param>
|
||||
/// <param name="innerException">The exception that is the cause of the current exception, or a null reference if no inner exception is specified.</param>
|
||||
public AuthenticationException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#nullable disable
|
||||
|
||||
using MediaBrowser.Model.Dto;
|
||||
|
||||
namespace MediaBrowser.Controller.Authentication;
|
||||
|
||||
/// <summary>
|
||||
/// A class representing an authentication result.
|
||||
/// </summary>
|
||||
public class AuthenticationResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the user.
|
||||
/// </summary>
|
||||
public UserDto User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the session info.
|
||||
/// </summary>
|
||||
public SessionInfoDto SessionInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the access token.
|
||||
/// </summary>
|
||||
public string AccessToken { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server id.
|
||||
/// </summary>
|
||||
public string ServerId { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using MediaBrowser.Model.Users;
|
||||
|
||||
namespace MediaBrowser.Controller.Authentication
|
||||
{
|
||||
public interface IAuthenticationProvider
|
||||
{
|
||||
string Name { get; }
|
||||
|
||||
bool IsEnabled { get; }
|
||||
|
||||
Task<ProviderAuthenticationResult> Authenticate(string username, string password);
|
||||
|
||||
Task ChangePassword(User user, string newPassword);
|
||||
}
|
||||
|
||||
public interface IRequiresResolvedUser
|
||||
{
|
||||
Task<ProviderAuthenticationResult> Authenticate(string username, string password, User? resolvedUser);
|
||||
}
|
||||
|
||||
public interface IHasNewUserPolicy
|
||||
{
|
||||
UserPolicy GetNewUserPolicy();
|
||||
}
|
||||
|
||||
public class ProviderAuthenticationResult
|
||||
{
|
||||
public required string Username { get; set; }
|
||||
|
||||
public string? DisplayName { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using MediaBrowser.Model.Users;
|
||||
|
||||
namespace MediaBrowser.Controller.Authentication
|
||||
{
|
||||
public interface IPasswordResetProvider
|
||||
{
|
||||
string Name { get; }
|
||||
|
||||
bool IsEnabled { get; }
|
||||
|
||||
Task<ForgotPasswordResult> StartForgotPasswordProcess(User? user, string enteredUsername, bool isInNetwork);
|
||||
|
||||
Task<PinRedeemResult> RedeemPasswordResetPin(string pin);
|
||||
}
|
||||
|
||||
#nullable disable
|
||||
public class PasswordPinCreationResult
|
||||
{
|
||||
public string PinFile { get; set; }
|
||||
|
||||
public DateTime ExpirationDate { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using System;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Channels;
|
||||
using MediaBrowser.Controller.Configuration;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
|
||||
namespace MediaBrowser.Controller.BaseItemManager
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class BaseItemManager : IBaseItemManager
|
||||
{
|
||||
private readonly IServerConfigurationManager _serverConfigurationManager;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BaseItemManager"/> class.
|
||||
/// </summary>
|
||||
/// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param>
|
||||
public BaseItemManager(IServerConfigurationManager serverConfigurationManager)
|
||||
{
|
||||
_serverConfigurationManager = serverConfigurationManager;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsMetadataFetcherEnabled(BaseItem baseItem, TypeOptions? libraryTypeOptions, string name)
|
||||
{
|
||||
if (baseItem is Channel)
|
||||
{
|
||||
// Hack alert.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (baseItem.SourceType == SourceType.Channel)
|
||||
{
|
||||
// Hack alert.
|
||||
return !baseItem.EnableMediaSourceDisplay;
|
||||
}
|
||||
|
||||
if (libraryTypeOptions is not null)
|
||||
{
|
||||
return libraryTypeOptions.MetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
var itemConfig = _serverConfigurationManager.GetMetadataOptionsForType(baseItem.GetType().Name);
|
||||
return itemConfig is null || !itemConfig.DisabledMetadataFetchers.Contains(name, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsImageFetcherEnabled(BaseItem baseItem, TypeOptions? libraryTypeOptions, string name)
|
||||
{
|
||||
if (baseItem is Channel)
|
||||
{
|
||||
// Hack alert.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (baseItem.SourceType == SourceType.Channel)
|
||||
{
|
||||
// Hack alert.
|
||||
return !baseItem.EnableMediaSourceDisplay;
|
||||
}
|
||||
|
||||
if (libraryTypeOptions is not null)
|
||||
{
|
||||
return libraryTypeOptions.ImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
var itemConfig = _serverConfigurationManager.GetMetadataOptionsForType(baseItem.GetType().Name);
|
||||
return itemConfig is null || !itemConfig.DisabledImageFetchers.Contains(name, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
|
||||
namespace MediaBrowser.Controller.BaseItemManager
|
||||
{
|
||||
/// <summary>
|
||||
/// The <c>BaseItem</c> manager.
|
||||
/// </summary>
|
||||
public interface IBaseItemManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Is metadata fetcher enabled.
|
||||
/// </summary>
|
||||
/// <param name="baseItem">The base item.</param>
|
||||
/// <param name="libraryTypeOptions">The type options for <c>baseItem</c> from the library (if defined).</param>
|
||||
/// <param name="name">The metadata fetcher name.</param>
|
||||
/// <returns><c>true</c> if metadata fetcher is enabled, else false.</returns>
|
||||
bool IsMetadataFetcherEnabled(BaseItem baseItem, TypeOptions? libraryTypeOptions, string name);
|
||||
|
||||
/// <summary>
|
||||
/// Is image fetcher enabled.
|
||||
/// </summary>
|
||||
/// <param name="baseItem">The base item.</param>
|
||||
/// <param name="libraryTypeOptions">The type options for <c>baseItem</c> from the library (if defined).</param>
|
||||
/// <param name="name">The image fetcher name.</param>
|
||||
/// <returns><c>true</c> if image fetcher is enabled, else false.</returns>
|
||||
bool IsImageFetcherEnabled(BaseItem baseItem, TypeOptions? libraryTypeOptions, string name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using Jellyfin.Data;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
|
||||
namespace MediaBrowser.Controller.Channels
|
||||
{
|
||||
public class Channel : Folder
|
||||
{
|
||||
[JsonIgnore]
|
||||
public override bool SupportsInheritedParentImages => false;
|
||||
|
||||
[JsonIgnore]
|
||||
public override SourceType SourceType => SourceType.Channel;
|
||||
|
||||
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
|
||||
{
|
||||
var blockedChannelsPreference = user.GetPreferenceValues<Guid>(PreferenceKind.BlockedChannels);
|
||||
if (blockedChannelsPreference.Length != 0)
|
||||
{
|
||||
if (blockedChannelsPreference.Contains(Id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!user.HasPermission(PermissionKind.EnableAllChannels)
|
||||
&& !user.GetPreferenceValues<Guid>(PreferenceKind.EnabledChannels).Contains(Id))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return base.IsVisible(user, skipAllowedTagsCheck);
|
||||
}
|
||||
|
||||
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
|
||||
{
|
||||
try
|
||||
{
|
||||
query.Parent = this;
|
||||
query.ChannelIds = new Guid[] { Id };
|
||||
|
||||
// Don't blow up here because it could cause parent screens with other content to fail
|
||||
return ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Already logged at lower levels
|
||||
return new QueryResult<BaseItem>();
|
||||
}
|
||||
}
|
||||
|
||||
protected override string GetInternalMetadataPath(string basePath)
|
||||
{
|
||||
return GetInternalMetadataPath(basePath, Id);
|
||||
}
|
||||
|
||||
public static string GetInternalMetadataPath(string basePath, Guid id)
|
||||
{
|
||||
return System.IO.Path.Combine(basePath, "channels", id.ToString("N", CultureInfo.InvariantCulture), "metadata");
|
||||
}
|
||||
|
||||
public override bool CanDelete()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool IsChannelVisible(BaseItem channelItem, User user)
|
||||
{
|
||||
var channel = ChannelManager.GetChannel(channelItem.ChannelId.ToString(string.Empty));
|
||||
|
||||
return channel.IsVisible(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1002, CA2227, CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Channels;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Channels
|
||||
{
|
||||
public class ChannelItemInfo : IHasProviderIds
|
||||
{
|
||||
public ChannelItemInfo()
|
||||
{
|
||||
MediaSources = new List<MediaSourceInfo>();
|
||||
TrailerTypes = new List<TrailerType>();
|
||||
Genres = new List<string>();
|
||||
Studios = new List<string>();
|
||||
People = new List<PersonInfo>();
|
||||
Tags = new List<string>();
|
||||
ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
Artists = new List<string>();
|
||||
AlbumArtists = new List<string>();
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string SeriesName { get; set; }
|
||||
|
||||
public string Id { get; set; }
|
||||
|
||||
public DateTime DateModified { get; set; }
|
||||
|
||||
public ChannelItemType Type { get; set; }
|
||||
|
||||
public string OfficialRating { get; set; }
|
||||
|
||||
public string Overview { get; set; }
|
||||
|
||||
public List<string> Genres { get; set; }
|
||||
|
||||
public List<string> Studios { get; set; }
|
||||
|
||||
public List<string> Tags { get; set; }
|
||||
|
||||
public List<PersonInfo> People { get; set; }
|
||||
|
||||
public float? CommunityRating { get; set; }
|
||||
|
||||
public long? RunTimeTicks { get; set; }
|
||||
|
||||
public string ImageUrl { get; set; }
|
||||
|
||||
public string OriginalTitle { get; set; }
|
||||
|
||||
public ChannelMediaType MediaType { get; set; }
|
||||
|
||||
public ChannelFolderType FolderType { get; set; }
|
||||
|
||||
public ChannelMediaContentType ContentType { get; set; }
|
||||
|
||||
public ExtraType ExtraType { get; set; }
|
||||
|
||||
public List<TrailerType> TrailerTypes { get; set; }
|
||||
|
||||
public Dictionary<string, string> ProviderIds { get; set; }
|
||||
|
||||
public DateTime? PremiereDate { get; set; }
|
||||
|
||||
public int? ProductionYear { get; set; }
|
||||
|
||||
public DateTime? DateCreated { get; set; }
|
||||
|
||||
public DateTime? StartDate { get; set; }
|
||||
|
||||
public DateTime? EndDate { get; set; }
|
||||
|
||||
public int? IndexNumber { get; set; }
|
||||
|
||||
public int? ParentIndexNumber { get; set; }
|
||||
|
||||
public List<MediaSourceInfo> MediaSources { get; set; }
|
||||
|
||||
public string HomePageUrl { get; set; }
|
||||
|
||||
public List<string> Artists { get; set; }
|
||||
|
||||
public List<string> AlbumArtists { get; set; }
|
||||
|
||||
public bool IsLiveStream { get; set; }
|
||||
|
||||
public string Etag { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Controller.Channels
|
||||
{
|
||||
public class ChannelItemResult
|
||||
{
|
||||
public ChannelItemResult()
|
||||
{
|
||||
Items = Array.Empty<ChannelItemInfo>();
|
||||
}
|
||||
|
||||
public IReadOnlyList<ChannelItemInfo> Items { get; set; }
|
||||
|
||||
public int? TotalRecordCount { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace MediaBrowser.Controller.Channels
|
||||
{
|
||||
public enum ChannelItemType
|
||||
{
|
||||
Media = 0,
|
||||
|
||||
Folder = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace MediaBrowser.Controller.Channels
|
||||
{
|
||||
public class ChannelLatestMediaSearch
|
||||
{
|
||||
public string UserId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace MediaBrowser.Controller.Channels
|
||||
{
|
||||
public enum ChannelParentalRating
|
||||
{
|
||||
GeneralAudience = 0,
|
||||
|
||||
UsPG = 1,
|
||||
|
||||
UsPG13 = 2,
|
||||
|
||||
UsR = 3,
|
||||
|
||||
Adult = 4
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace MediaBrowser.Controller.Channels
|
||||
{
|
||||
public class ChannelSearchInfo
|
||||
{
|
||||
public string SearchTerm { get; set; }
|
||||
|
||||
public string UserId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Channels
|
||||
{
|
||||
public interface IChannel
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the description.
|
||||
/// </summary>
|
||||
/// <value>The description.</value>
|
||||
string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data version.
|
||||
/// </summary>
|
||||
/// <value>The data version.</value>
|
||||
string DataVersion { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the home page URL.
|
||||
/// </summary>
|
||||
/// <value>The home page URL.</value>
|
||||
string HomePageUrl { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the parental rating.
|
||||
/// </summary>
|
||||
/// <value>The parental rating.</value>
|
||||
ChannelParentalRating ParentalRating { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the channel information.
|
||||
/// </summary>
|
||||
/// <returns>ChannelFeatures.</returns>
|
||||
InternalChannelFeatures GetChannelFeatures();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether [is enabled for] [the specified user].
|
||||
/// </summary>
|
||||
/// <param name="userId">The user identifier.</param>
|
||||
/// <returns><c>true</c> if [is enabled for] [the specified user]; otherwise, <c>false</c>.</returns>
|
||||
bool IsEnabledFor(string userId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the channel items.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{IEnumerable{ChannelItem}}.</returns>
|
||||
Task<ChannelItemResult> GetChannelItems(InternalChannelItemQuery query, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the channel image.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task{DynamicImageResponse}.</returns>
|
||||
Task<DynamicImageResponse> GetChannelImage(ImageType type, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the supported channel images.
|
||||
/// </summary>
|
||||
/// <returns>IEnumerable{ImageType}.</returns>
|
||||
IEnumerable<ImageType> GetSupportedChannelImages();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Channels;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Querying;
|
||||
|
||||
namespace MediaBrowser.Controller.Channels
|
||||
{
|
||||
public interface IChannelManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the channel features.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <returns>ChannelFeatures.</returns>
|
||||
ChannelFeatures GetChannelFeatures(Guid? id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all channel features.
|
||||
/// </summary>
|
||||
/// <returns>IEnumerable{ChannelFeatures}.</returns>
|
||||
ChannelFeatures[] GetAllChannelFeatures();
|
||||
|
||||
bool EnableMediaSourceDisplay(BaseItem item);
|
||||
|
||||
bool CanDelete(BaseItem item);
|
||||
|
||||
Task DeleteItem(BaseItem item);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the channel.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <returns>Channel.</returns>
|
||||
Channel GetChannel(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the channels internal.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <returns>The channels.</returns>
|
||||
Task<QueryResult<Channel>> GetChannelsInternalAsync(ChannelQuery query);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the channels.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <returns>The channels.</returns>
|
||||
Task<QueryResult<BaseItemDto>> GetChannelsAsync(ChannelQuery query);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the latest channel items.
|
||||
/// </summary>
|
||||
/// <param name="query">The item query.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The latest channels.</returns>
|
||||
Task<QueryResult<BaseItemDto>> GetLatestChannelItems(InternalItemsQuery query, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the latest channel items.
|
||||
/// </summary>
|
||||
/// <param name="query">The item query.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The latest channels.</returns>
|
||||
Task<QueryResult<BaseItem>> GetLatestChannelItemsInternal(InternalItemsQuery query, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the channel items.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The channel items.</returns>
|
||||
Task<QueryResult<BaseItemDto>> GetChannelItems(InternalItemsQuery query, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the channel items.
|
||||
/// </summary>
|
||||
/// <param name="query">The query.</param>
|
||||
/// <param name="progress">The progress to report to.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The channel items.</returns>
|
||||
Task<QueryResult<BaseItem>> GetChannelItemsInternal(InternalItemsQuery query, IProgress<double> progress, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the channel item media sources.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The item media sources.</returns>
|
||||
IEnumerable<MediaSourceInfo> GetStaticMediaSources(BaseItem item, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace MediaBrowser.Controller.Channels
|
||||
{
|
||||
/// <summary>
|
||||
/// Disable media source display.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="Channel"/> can inherit this interface to disable being displayed.
|
||||
/// </remarks>
|
||||
public interface IDisableMediaSourceDisplay
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace MediaBrowser.Controller.Channels
|
||||
{
|
||||
public interface IHasCacheKey
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the cache key.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user identifier.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
string? GetCacheKey(string? userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma warning disable CA1819, CS1591
|
||||
|
||||
namespace MediaBrowser.Controller.Channels
|
||||
{
|
||||
public interface IHasFolderAttributes
|
||||
{
|
||||
string[] Attributes { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Model.Dto;
|
||||
|
||||
namespace MediaBrowser.Controller.Channels
|
||||
{
|
||||
/// <summary>
|
||||
/// The channel requires a media info callback.
|
||||
/// </summary>
|
||||
public interface IRequiresMediaInfoCallback
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the channel item media information.
|
||||
/// </summary>
|
||||
/// <param name="id">The channel item id.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The enumerable of media source info.</returns>
|
||||
Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaInfo(string id, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Channels
|
||||
{
|
||||
public interface ISupportsDelete
|
||||
{
|
||||
bool CanDelete(BaseItem item);
|
||||
|
||||
Task DeleteItem(string id, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.Channels
|
||||
{
|
||||
public interface ISupportsLatestMedia
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the latest media.
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The latest media.</returns>
|
||||
Task<IEnumerable<ChannelItemInfo>> GetLatestMedia(ChannelLatestMediaSearch request, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace MediaBrowser.Controller.Channels
|
||||
{
|
||||
/// <summary>
|
||||
/// Channel supports media probe.
|
||||
/// </summary>
|
||||
public interface ISupportsMediaProbe
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1002, CA2227, CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Model.Channels;
|
||||
|
||||
namespace MediaBrowser.Controller.Channels
|
||||
{
|
||||
public class InternalChannelFeatures
|
||||
{
|
||||
public InternalChannelFeatures()
|
||||
{
|
||||
MediaTypes = new List<ChannelMediaType>();
|
||||
ContentTypes = new List<ChannelMediaContentType>();
|
||||
|
||||
DefaultSortFields = new List<ChannelItemSortField>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the media types.
|
||||
/// </summary>
|
||||
/// <value>The media types.</value>
|
||||
public List<ChannelMediaType> MediaTypes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the content types.
|
||||
/// </summary>
|
||||
/// <value>The content types.</value>
|
||||
public List<ChannelMediaContentType> ContentTypes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of records the channel allows retrieving at a time.
|
||||
/// </summary>
|
||||
public int? MaxPageSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default sort orders.
|
||||
/// </summary>
|
||||
/// <value>The default sort orders.</value>
|
||||
public List<ChannelItemSortField> DefaultSortFields { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether a sort ascending/descending toggle is supported or not.
|
||||
/// </summary>
|
||||
public bool SupportsSortOrderToggle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the automatic refresh levels.
|
||||
/// </summary>
|
||||
/// <value>The automatic refresh levels.</value>
|
||||
public int? AutoRefreshLevels { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the daily download limit.
|
||||
/// </summary>
|
||||
/// <value>The daily download limit.</value>
|
||||
public int? DailyDownloadLimit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether [supports downloading].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [supports downloading]; otherwise, <c>false</c>.</value>
|
||||
public bool SupportsContentDownloading { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using MediaBrowser.Model.Channels;
|
||||
|
||||
namespace MediaBrowser.Controller.Channels
|
||||
{
|
||||
public class InternalChannelItemQuery
|
||||
{
|
||||
public string FolderId { get; set; }
|
||||
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
public int? StartIndex { get; set; }
|
||||
|
||||
public int? Limit { get; set; }
|
||||
|
||||
public ChannelItemSortField? SortBy { get; set; }
|
||||
|
||||
public bool SortDescending { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Chapters;
|
||||
|
||||
/// <summary>
|
||||
/// Interface IChapterManager.
|
||||
/// </summary>
|
||||
public interface IChapterManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Saves the chapters.
|
||||
/// </summary>
|
||||
/// <param name="video">The video.</param>
|
||||
/// <param name="chapters">The set of chapters.</param>
|
||||
void SaveChapters(Video video, IReadOnlyList<ChapterInfo> chapters);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a single chapter of a BaseItem on a specific index.
|
||||
/// </summary>
|
||||
/// <param name="baseItemId">The BaseItems id.</param>
|
||||
/// <param name="index">The index of that chapter.</param>
|
||||
/// <returns>A chapter instance.</returns>
|
||||
ChapterInfo? GetChapter(Guid baseItemId, int index);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all chapters associated with the baseItem.
|
||||
/// </summary>
|
||||
/// <param name="baseItemId">The BaseItems id.</param>
|
||||
/// <returns>A readonly list of chapter instances.</returns>
|
||||
IReadOnlyList<ChapterInfo> GetChapters(Guid baseItemId);
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the chapter images.
|
||||
/// </summary>
|
||||
/// <param name="video">Video to use.</param>
|
||||
/// <param name="directoryService">Directory service to use.</param>
|
||||
/// <param name="chapters">Set of chapters to refresh.</param>
|
||||
/// <param name="extractImages">Option to extract images.</param>
|
||||
/// <param name="saveChapters">Option to save chapters.</param>
|
||||
/// <param name="cancellationToken">CancellationToken to use for operation.</param>
|
||||
/// <returns><c>true</c> if successful, <c>false</c> if not.</returns>
|
||||
Task<bool> RefreshChapterImages(Video video, IDirectoryService directoryService, IReadOnlyList<ChapterInfo> chapters, bool extractImages, bool saveChapters, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the chapter data.
|
||||
/// </summary>
|
||||
/// <param name="itemId">The item id.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task DeleteChapterDataAsync(Guid itemId, CancellationToken cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.ClientEvent
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public class ClientEventLogger : IClientEventLogger
|
||||
{
|
||||
private readonly IServerApplicationPaths _applicationPaths;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ClientEventLogger"/> class.
|
||||
/// </summary>
|
||||
/// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param>
|
||||
public ClientEventLogger(IServerApplicationPaths applicationPaths)
|
||||
{
|
||||
_applicationPaths = applicationPaths;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<string> WriteDocumentAsync(string clientName, string clientVersion, Stream fileContents)
|
||||
{
|
||||
var fileName = $"upload_{clientName}_{clientVersion}_{DateTime.UtcNow:yyyyMMddHHmmss}_{Guid.NewGuid():N}.log";
|
||||
var logFilePath = Path.Combine(_applicationPaths.LogDirectoryPath, fileName);
|
||||
var fileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None);
|
||||
await using (fileStream.ConfigureAwait(false))
|
||||
{
|
||||
await fileContents.CopyToAsync(fileStream).ConfigureAwait(false);
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MediaBrowser.Controller.ClientEvent
|
||||
{
|
||||
/// <summary>
|
||||
/// The client event logger.
|
||||
/// </summary>
|
||||
public interface IClientEventLogger
|
||||
{
|
||||
/// <summary>
|
||||
/// Writes a file to the log directory.
|
||||
/// </summary>
|
||||
/// <param name="clientName">The client name writing the document.</param>
|
||||
/// <param name="clientVersion">The client version writing the document.</param>
|
||||
/// <param name="fileContents">The file contents to write.</param>
|
||||
/// <returns>The created file name.</returns>
|
||||
Task<string> WriteDocumentAsync(
|
||||
string clientName,
|
||||
string clientVersion,
|
||||
Stream fileContents);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
|
||||
namespace MediaBrowser.Controller.Collections
|
||||
{
|
||||
public class CollectionCreatedEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the collection.
|
||||
/// </summary>
|
||||
/// <value>The collection.</value>
|
||||
public BoxSet Collection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the options.
|
||||
/// </summary>
|
||||
/// <value>The options.</value>
|
||||
public CollectionCreationOptions Options { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Collections
|
||||
{
|
||||
public class CollectionCreationOptions : IHasProviderIds
|
||||
{
|
||||
public CollectionCreationOptions()
|
||||
{
|
||||
ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
ItemIdList = Array.Empty<string>();
|
||||
UserIds = Array.Empty<Guid>();
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public Guid? ParentId { get; set; }
|
||||
|
||||
public bool IsLocked { get; set; }
|
||||
|
||||
public Dictionary<string, string> ProviderIds { get; set; }
|
||||
|
||||
public IReadOnlyList<string> ItemIdList { get; set; }
|
||||
|
||||
public IReadOnlyList<Guid> UserIds { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
|
||||
namespace MediaBrowser.Controller.Collections
|
||||
{
|
||||
public class CollectionModifiedEventArgs : EventArgs
|
||||
{
|
||||
public CollectionModifiedEventArgs(BoxSet collection, IReadOnlyCollection<BaseItem> itemsChanged)
|
||||
{
|
||||
Collection = collection;
|
||||
ItemsChanged = itemsChanged;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the collection.
|
||||
/// </summary>
|
||||
/// <value>The collection.</value>
|
||||
public BoxSet Collection { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the items changed.
|
||||
/// </summary>
|
||||
/// <value>The items changed.</value>
|
||||
public IReadOnlyCollection<BaseItem> ItemsChanged { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Controller.Entities.Movies;
|
||||
|
||||
namespace MediaBrowser.Controller.Collections
|
||||
{
|
||||
public interface ICollectionManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Occurs when [collection created].
|
||||
/// </summary>
|
||||
event EventHandler<CollectionCreatedEventArgs>? CollectionCreated;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when [items added to collection].
|
||||
/// </summary>
|
||||
event EventHandler<CollectionModifiedEventArgs>? ItemsAddedToCollection;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when [items removed from collection].
|
||||
/// </summary>
|
||||
event EventHandler<CollectionModifiedEventArgs>? ItemsRemovedFromCollection;
|
||||
|
||||
/// <summary>
|
||||
/// Creates the collection.
|
||||
/// </summary>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <returns>BoxSet wrapped in an awaitable task.</returns>
|
||||
Task<BoxSet> CreateCollectionAsync(CollectionCreationOptions options);
|
||||
|
||||
/// <summary>
|
||||
/// Adds to collection.
|
||||
/// </summary>
|
||||
/// <param name="collectionId">The collection identifier.</param>
|
||||
/// <param name="itemIds">The item ids.</param>
|
||||
/// <returns><see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
Task AddToCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds);
|
||||
|
||||
/// <summary>
|
||||
/// Removes from collection.
|
||||
/// </summary>
|
||||
/// <param name="collectionId">The collection identifier.</param>
|
||||
/// <param name="itemIds">The item ids.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||
Task RemoveFromCollectionAsync(Guid collectionId, IEnumerable<Guid> itemIds);
|
||||
|
||||
/// <summary>
|
||||
/// Collapses the items within box sets.
|
||||
/// </summary>
|
||||
/// <param name="items">The items.</param>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <returns>IEnumerable{BaseItem}.</returns>
|
||||
IEnumerable<BaseItem> CollapseItemsWithinBoxSets(IEnumerable<BaseItem> items, User user);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the folder where collections are stored.
|
||||
/// </summary>
|
||||
/// <param name="createIfNeeded">Will create the collection folder on the storage if set to true.</param>
|
||||
/// <returns>The folder instance referencing the collection storage.</returns>
|
||||
Task<Folder?> GetCollectionsFolder(bool createIfNeeded);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using MediaBrowser.Common.Configuration;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
|
||||
namespace MediaBrowser.Controller.Configuration
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface IServerConfigurationManager.
|
||||
/// </summary>
|
||||
public interface IServerConfigurationManager : IConfigurationManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the application paths.
|
||||
/// </summary>
|
||||
/// <value>The application paths.</value>
|
||||
IServerApplicationPaths ApplicationPaths { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration.
|
||||
/// </summary>
|
||||
/// <value>The configuration.</value>
|
||||
ServerConfiguration Configuration { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Dtos;
|
||||
using Jellyfin.Data.Events;
|
||||
using Jellyfin.Data.Queries;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Database.Implementations.Entities.Security;
|
||||
using MediaBrowser.Model.Devices;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MediaBrowser.Model.Session;
|
||||
|
||||
namespace MediaBrowser.Controller.Devices;
|
||||
|
||||
/// <summary>
|
||||
/// Device manager interface.
|
||||
/// </summary>
|
||||
public interface IDeviceManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Event handler for updated device options.
|
||||
/// </summary>
|
||||
event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new device.
|
||||
/// </summary>
|
||||
/// <param name="device">The device to create.</param>
|
||||
/// <returns>A <see cref="Task{Device}"/> representing the creation of the device.</returns>
|
||||
Task<Device> CreateDevice(Device device);
|
||||
|
||||
/// <summary>
|
||||
/// Saves the capabilities.
|
||||
/// </summary>
|
||||
/// <param name="deviceId">The device id.</param>
|
||||
/// <param name="capabilities">The capabilities.</param>
|
||||
void SaveCapabilities(string deviceId, ClientCapabilities capabilities);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the capabilities.
|
||||
/// </summary>
|
||||
/// <param name="deviceId">The device id.</param>
|
||||
/// <returns>ClientCapabilities.</returns>
|
||||
ClientCapabilities GetCapabilities(string? deviceId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the device information.
|
||||
/// </summary>
|
||||
/// <param name="id">The identifier.</param>
|
||||
/// <returns>DeviceInfoDto.</returns>
|
||||
DeviceInfoDto? GetDevice(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets devices based on the provided query.
|
||||
/// </summary>
|
||||
/// <param name="query">The device query.</param>
|
||||
/// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the devices.</returns>
|
||||
QueryResult<Device> GetDevices(DeviceQuery query);
|
||||
|
||||
/// <summary>
|
||||
/// Gets device information based on the provided query.
|
||||
/// </summary>
|
||||
/// <param name="query">The device query.</param>
|
||||
/// <returns>A <see cref="Task{QueryResult}"/> representing the retrieval of the device information.</returns>
|
||||
QueryResult<DeviceInfo> GetDeviceInfos(DeviceQuery query);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the device information.
|
||||
/// </summary>
|
||||
/// <param name="userId">The user's id, or <c>null</c>.</param>
|
||||
/// <returns>IEnumerable<DeviceInfoDto>.</returns>
|
||||
QueryResult<DeviceInfoDto> GetDevicesForUser(Guid? userId);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes a device.
|
||||
/// </summary>
|
||||
/// <param name="device">The device.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the deletion of the device.</returns>
|
||||
Task DeleteDevice(Device device);
|
||||
|
||||
/// <summary>
|
||||
/// Updates a device.
|
||||
/// </summary>
|
||||
/// <param name="device">The device.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the update of the device.</returns>
|
||||
Task UpdateDevice(Device device);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this instance [can access device] the specified user identifier.
|
||||
/// </summary>
|
||||
/// <param name="user">The user to test.</param>
|
||||
/// <param name="deviceId">The device id to test.</param>
|
||||
/// <returns>Whether the user can access the device.</returns>
|
||||
bool CanAccessDevice(User user, string deviceId);
|
||||
|
||||
/// <summary>
|
||||
/// Updates the options of a device.
|
||||
/// </summary>
|
||||
/// <param name="deviceId">The device id.</param>
|
||||
/// <param name="deviceName">The device name.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the update of the device options.</returns>
|
||||
Task UpdateDeviceOptions(string deviceId, string? deviceName);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the options of a device.
|
||||
/// </summary>
|
||||
/// <param name="deviceId">The device id.</param>
|
||||
/// <returns><see cref="DeviceOptions"/> of the device.</returns>
|
||||
DeviceOptionsDto? GetDeviceOptions(string deviceId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the dto for client capabilities.
|
||||
/// </summary>
|
||||
/// <param name="capabilities">The client capabilities.</param>
|
||||
/// <returns><see cref="ClientCapabilitiesDto"/> of the device.</returns>
|
||||
ClientCapabilitiesDto ToClientCapabilitiesDto(ClientCapabilities capabilities);
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
|
||||
namespace MediaBrowser.Controller.Drawing
|
||||
{
|
||||
public interface IImageEncoder
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the supported input formats.
|
||||
/// </summary>
|
||||
/// <value>The supported input formats.</value>
|
||||
IReadOnlyCollection<string> SupportedInputFormats { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the supported output formats.
|
||||
/// </summary>
|
||||
/// <value>The supported output formats.</value>
|
||||
IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display name for the encoder.
|
||||
/// </summary>
|
||||
/// <value>The display name.</value>
|
||||
string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether [supports image collage creation].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [supports image collage creation]; otherwise, <c>false</c>.</value>
|
||||
bool SupportsImageCollageCreation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether [supports image encoding].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [supports image encoding]; otherwise, <c>false</c>.</value>
|
||||
bool SupportsImageEncoding { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the dimensions of an image from the filesystem.
|
||||
/// </summary>
|
||||
/// <param name="path">The filepath of the image.</param>
|
||||
/// <returns>The image dimensions.</returns>
|
||||
ImageDimensions GetImageSize(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the blurhash of an image.
|
||||
/// </summary>
|
||||
/// <param name="xComp">Amount of X components of DCT to take.</param>
|
||||
/// <param name="yComp">Amount of Y components of DCT to take.</param>
|
||||
/// <param name="path">The filepath of the image.</param>
|
||||
/// <returns>The blurhash.</returns>
|
||||
string GetImageBlurHash(int xComp, int yComp, string path);
|
||||
|
||||
/// <summary>
|
||||
/// Encode an image.
|
||||
/// </summary>
|
||||
/// <param name="inputPath">Input path of image.</param>
|
||||
/// <param name="dateModified">Date modified.</param>
|
||||
/// <param name="outputPath">Output path of image.</param>
|
||||
/// <param name="autoOrient">Auto-orient image.</param>
|
||||
/// <param name="orientation">Desired orientation of image.</param>
|
||||
/// <param name="quality">Quality of encoded image.</param>
|
||||
/// <param name="options">Image processing options.</param>
|
||||
/// <param name="outputFormat">Image format of output.</param>
|
||||
/// <returns>Path of encoded image.</returns>
|
||||
string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat);
|
||||
|
||||
/// <summary>
|
||||
/// Create an image collage.
|
||||
/// </summary>
|
||||
/// <param name="options">The options to use when creating the collage.</param>
|
||||
/// <param name="libraryName">Optional. </param>
|
||||
void CreateImageCollage(ImageCollageOptions options, string? libraryName);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new splashscreen image.
|
||||
/// </summary>
|
||||
/// <param name="posters">The list of poster paths.</param>
|
||||
/// <param name="backdrops">The list of backdrop paths.</param>
|
||||
void CreateSplashscreen(IReadOnlyList<string> posters, IReadOnlyList<string> backdrops);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new trickplay tile image.
|
||||
/// </summary>
|
||||
/// <param name="options">The options to use when creating the image. Width and Height are a quantity of thumbnails in this case, not pixels.</param>
|
||||
/// <param name="quality">The image encode quality.</param>
|
||||
/// <param name="imgWidth">The width of a single trickplay thumbnail.</param>
|
||||
/// <param name="imgHeight">Optional height of a single trickplay thumbnail, if it is known.</param>
|
||||
/// <returns>Height of single decoded trickplay thumbnail.</returns>
|
||||
int CreateTrickplayTile(ImageCollageOptions options, int quality, int imgWidth, int? imgHeight);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Drawing
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface IImageProcessor.
|
||||
/// </summary>
|
||||
public interface IImageProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the supported input formats.
|
||||
/// </summary>
|
||||
/// <value>The supported input formats.</value>
|
||||
IReadOnlyCollection<string> SupportedInputFormats { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether [supports image collage creation].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [supports image collage creation]; otherwise, <c>false</c>.</value>
|
||||
bool SupportsImageCollageCreation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the dimensions of the image.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to the image file.</param>
|
||||
/// <returns>ImageDimensions.</returns>
|
||||
ImageDimensions GetImageDimensions(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the dimensions of the image.
|
||||
/// </summary>
|
||||
/// <param name="item">The base item.</param>
|
||||
/// <param name="info">The information.</param>
|
||||
/// <returns>ImageDimensions.</returns>
|
||||
ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the blurhash of the image.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to the image file.</param>
|
||||
/// <returns>BlurHash.</returns>
|
||||
string GetImageBlurHash(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the blurhash of the image.
|
||||
/// </summary>
|
||||
/// <param name="path">Path to the image file.</param>
|
||||
/// <param name="imageDimensions">The image dimensions.</param>
|
||||
/// <returns>BlurHash.</returns>
|
||||
string GetImageBlurHash(string path, ImageDimensions imageDimensions);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image cache tag.
|
||||
/// </summary>
|
||||
/// <param name="baseItemPath">The items basePath.</param>
|
||||
/// <param name="imageDateModified">The image last modification date.</param>
|
||||
/// <returns>Guid.</returns>
|
||||
string? GetImageCacheTag(string baseItemPath, DateTime imageDateModified);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image cache tag.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="image">The image.</param>
|
||||
/// <returns>Guid.</returns>
|
||||
string? GetImageCacheTag(BaseItemDto item, ChapterInfo image);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image cache tag.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="image">The image.</param>
|
||||
/// <returns>Guid.</returns>
|
||||
string GetImageCacheTag(BaseItem item, ItemImageInfo image);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the image cache tag.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="image">The image.</param>
|
||||
/// <returns>Guid.</returns>
|
||||
string GetImageCacheTag(BaseItemDto item, ItemImageInfo image);
|
||||
|
||||
string? GetImageCacheTag(BaseItem item, ChapterInfo chapter);
|
||||
|
||||
string? GetImageCacheTag(User user);
|
||||
|
||||
/// <summary>
|
||||
/// Processes the image.
|
||||
/// </summary>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task<(string Path, string? MimeType, DateTime DateModified)> ProcessImage(ImageProcessingOptions options);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the supported image output formats.
|
||||
/// </summary>
|
||||
/// <returns><see cref="IReadOnlyCollection{ImageOutput}" />.</returns>
|
||||
IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats();
|
||||
|
||||
/// <summary>
|
||||
/// Creates the image collage.
|
||||
/// </summary>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="libraryName">The library name to draw onto the collage.</param>
|
||||
void CreateImageCollage(ImageCollageOptions options, string? libraryName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace MediaBrowser.Controller.Drawing
|
||||
{
|
||||
public class ImageCollageOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the input paths.
|
||||
/// </summary>
|
||||
/// <value>The input paths.</value>
|
||||
public IReadOnlyList<string> InputPaths { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the output path.
|
||||
/// </summary>
|
||||
/// <value>The output path.</value>
|
||||
public string OutputPath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the width.
|
||||
/// </summary>
|
||||
/// <value>The width.</value>
|
||||
public int Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the height.
|
||||
/// </summary>
|
||||
/// <value>The height.</value>
|
||||
public int Height { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using MediaBrowser.Model.Drawing;
|
||||
|
||||
namespace MediaBrowser.Controller.Drawing
|
||||
{
|
||||
public static class ImageHelper
|
||||
{
|
||||
public static ImageDimensions GetNewImageSize(ImageProcessingOptions options, ImageDimensions originalImageSize)
|
||||
{
|
||||
// Determine the output size based on incoming parameters
|
||||
var newSize = DrawingUtils.Resize(originalImageSize, options.Width ?? 0, options.Height ?? 0, options.MaxWidth ?? 0, options.MaxHeight ?? 0);
|
||||
newSize = DrawingUtils.ResizeFill(newSize, options.FillWidth, options.FillHeight);
|
||||
return newSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
|
||||
namespace MediaBrowser.Controller.Drawing
|
||||
{
|
||||
public class ImageProcessingOptions
|
||||
{
|
||||
public ImageProcessingOptions()
|
||||
{
|
||||
RequiresAutoOrientation = true;
|
||||
}
|
||||
|
||||
public Guid ItemId { get; set; }
|
||||
|
||||
public BaseItem Item { get; set; }
|
||||
|
||||
public ItemImageInfo Image { get; set; }
|
||||
|
||||
public int ImageIndex { get; set; }
|
||||
|
||||
public int? Width { get; set; }
|
||||
|
||||
public int? Height { get; set; }
|
||||
|
||||
public int? MaxWidth { get; set; }
|
||||
|
||||
public int? MaxHeight { get; set; }
|
||||
|
||||
public int? FillWidth { get; set; }
|
||||
|
||||
public int? FillHeight { get; set; }
|
||||
|
||||
public int Quality { get; set; }
|
||||
|
||||
public IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; set; }
|
||||
|
||||
public int? UnplayedCount { get; set; }
|
||||
|
||||
public int? Blur { get; set; }
|
||||
|
||||
public double PercentPlayed { get; set; }
|
||||
|
||||
public string BackgroundColor { get; set; }
|
||||
|
||||
public string ForegroundLayer { get; set; }
|
||||
|
||||
public bool RequiresAutoOrientation { get; set; }
|
||||
|
||||
private bool HasDefaultOptions(string originalImagePath)
|
||||
{
|
||||
return HasDefaultOptionsWithoutSize(originalImagePath) &&
|
||||
!Width.HasValue &&
|
||||
!Height.HasValue &&
|
||||
!MaxWidth.HasValue &&
|
||||
!MaxHeight.HasValue;
|
||||
}
|
||||
|
||||
public bool HasDefaultOptions(string originalImagePath, ImageDimensions? size)
|
||||
{
|
||||
if (!size.HasValue)
|
||||
{
|
||||
return HasDefaultOptions(originalImagePath);
|
||||
}
|
||||
|
||||
if (!HasDefaultOptionsWithoutSize(originalImagePath))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var sizeValue = size.Value;
|
||||
|
||||
if (Width.HasValue && !sizeValue.Width.Equals(Width.Value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Height.HasValue && !sizeValue.Height.Equals(Height.Value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (MaxWidth.HasValue && sizeValue.Width > MaxWidth.Value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (MaxHeight.HasValue && sizeValue.Height > MaxHeight.Value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sizeValue.Width > FillWidth || sizeValue.Height > FillHeight)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool HasDefaultOptionsWithoutSize(string originalImagePath)
|
||||
{
|
||||
return (Quality >= 90) &&
|
||||
IsFormatSupported(originalImagePath) &&
|
||||
PercentPlayed.Equals(0) &&
|
||||
!UnplayedCount.HasValue &&
|
||||
!Blur.HasValue &&
|
||||
string.IsNullOrEmpty(BackgroundColor) &&
|
||||
string.IsNullOrEmpty(ForegroundLayer);
|
||||
}
|
||||
|
||||
private bool IsFormatSupported(string originalImagePath)
|
||||
{
|
||||
var ext = Path.GetExtension(originalImagePath);
|
||||
ext = ext.Replace(".jpeg", ".jpg", StringComparison.OrdinalIgnoreCase);
|
||||
return SupportedOutputFormats.Any(outputFormat => string.Equals(ext, outputFormat.GetExtension(), StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Drawing
|
||||
{
|
||||
public static class ImageProcessorExtensions
|
||||
{
|
||||
public static string? GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType)
|
||||
{
|
||||
return processor.GetImageCacheTag(item, imageType, 0);
|
||||
}
|
||||
|
||||
public static string? GetImageCacheTag(this IImageProcessor processor, BaseItem item, ImageType imageType, int imageIndex)
|
||||
{
|
||||
var imageInfo = item.GetImageInfo(imageType, imageIndex);
|
||||
|
||||
if (imageInfo is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return processor.GetImageCacheTag(item, imageInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
|
||||
namespace MediaBrowser.Controller.Dto
|
||||
{
|
||||
public class DtoOptions
|
||||
{
|
||||
private static readonly ItemFields[] DefaultExcludedFields = new[]
|
||||
{
|
||||
ItemFields.SeasonUserData,
|
||||
ItemFields.RefreshState
|
||||
};
|
||||
|
||||
private static readonly ImageType[] AllImageTypes = Enum.GetValues<ImageType>();
|
||||
|
||||
private static readonly ItemFields[] AllItemFields = Enum.GetValues<ItemFields>()
|
||||
.Except(DefaultExcludedFields)
|
||||
.ToArray();
|
||||
|
||||
public DtoOptions()
|
||||
: this(true)
|
||||
{
|
||||
}
|
||||
|
||||
public DtoOptions(bool allFields)
|
||||
{
|
||||
ImageTypeLimit = int.MaxValue;
|
||||
EnableImages = true;
|
||||
EnableUserData = true;
|
||||
AddCurrentProgram = true;
|
||||
|
||||
Fields = allFields ? AllItemFields : [];
|
||||
ImageTypes = AllImageTypes;
|
||||
}
|
||||
|
||||
public IReadOnlyList<ItemFields> Fields { get; set; }
|
||||
|
||||
public IReadOnlyList<ImageType> ImageTypes { get; set; }
|
||||
|
||||
public int ImageTypeLimit { get; set; }
|
||||
|
||||
public bool EnableImages { get; set; }
|
||||
|
||||
public bool AddProgramRecordingInfo { get; set; }
|
||||
|
||||
public bool EnableUserData { get; set; }
|
||||
|
||||
public bool AddCurrentProgram { get; set; }
|
||||
|
||||
public bool ContainsField(ItemFields field)
|
||||
=> Fields.Contains(field);
|
||||
|
||||
public int GetImageLimit(ImageType type)
|
||||
{
|
||||
if (EnableImages && ImageTypes.Contains(type))
|
||||
{
|
||||
return ImageTypeLimit;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma warning disable CA1002
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using MediaBrowser.Controller.Entities;
|
||||
using MediaBrowser.Model.Dto;
|
||||
|
||||
namespace MediaBrowser.Controller.Dto
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface IDtoService.
|
||||
/// </summary>
|
||||
public interface IDtoService
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the primary image aspect ratio.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>System.Nullable<System.Double>.</returns>
|
||||
double? GetPrimaryImageAspectRatio(BaseItem item);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the base item dto.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="owner">The owner.</param>
|
||||
/// <returns>BaseItemDto.</returns>
|
||||
BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the base item dtos.
|
||||
/// </summary>
|
||||
/// <param name="items">The items.</param>
|
||||
/// <param name="options">The options.</param>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="owner">The owner.</param>
|
||||
/// <returns>The <see cref="IReadOnlyList{T}"/> of <see cref="BaseItemDto"/>.</returns>
|
||||
IReadOnlyList<BaseItemDto> GetBaseItemDtos(IReadOnlyList<BaseItem> items, DtoOptions options, User? user = null, BaseItem? owner = null);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item by name dto.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="options">The dto options.</param>
|
||||
/// <param name="taggedItems">The list of tagged items.</param>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <returns>The item dto.</returns>
|
||||
BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List<BaseItem>? taggedItems, User? user = null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1819, CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Specialized folder that can have items added to it's children by external entities.
|
||||
/// Used for our RootFolder so plugins can add items.
|
||||
/// </summary>
|
||||
public class AggregateFolder : Folder
|
||||
{
|
||||
private readonly Lock _childIdsLock = new();
|
||||
|
||||
/// <summary>
|
||||
/// The _virtual children.
|
||||
/// </summary>
|
||||
private readonly ConcurrentBag<BaseItem> _virtualChildren = new ConcurrentBag<BaseItem>();
|
||||
private bool _requiresRefresh;
|
||||
private Guid[] _childrenIds = null;
|
||||
|
||||
public AggregateFolder()
|
||||
{
|
||||
PhysicalLocationsList = Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the virtual children.
|
||||
/// </summary>
|
||||
/// <value>The virtual children.</value>
|
||||
public ConcurrentBag<BaseItem> VirtualChildren => _virtualChildren;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool IsPhysicalRoot => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPlayedStatus => false;
|
||||
|
||||
[JsonIgnore]
|
||||
public override string[] PhysicalLocations => PhysicalLocationsList;
|
||||
|
||||
public string[] PhysicalLocationsList { get; set; }
|
||||
|
||||
public override bool CanDelete()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
|
||||
{
|
||||
return CreateResolveArgs(directoryService, true).FileSystemChildren;
|
||||
}
|
||||
|
||||
protected override IReadOnlyList<BaseItem> LoadChildren()
|
||||
{
|
||||
lock (_childIdsLock)
|
||||
{
|
||||
if (_childrenIds is null || _childrenIds.Length == 0)
|
||||
{
|
||||
var list = base.LoadChildren();
|
||||
_childrenIds = list.Select(i => i.Id).ToArray();
|
||||
return list;
|
||||
}
|
||||
|
||||
return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i is not null).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearCache()
|
||||
{
|
||||
lock (_childIdsLock)
|
||||
{
|
||||
_childrenIds = null;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool RequiresRefresh()
|
||||
{
|
||||
var changed = base.RequiresRefresh() || _requiresRefresh;
|
||||
|
||||
if (!changed)
|
||||
{
|
||||
var locations = PhysicalLocations;
|
||||
|
||||
var newLocations = CreateResolveArgs(new DirectoryService(FileSystem), false).PhysicalLocations;
|
||||
|
||||
if (!locations.SequenceEqual(newLocations))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
|
||||
{
|
||||
ClearCache();
|
||||
|
||||
var changed = base.BeforeMetadataRefresh(replaceAllMetadata) || _requiresRefresh;
|
||||
_requiresRefresh = false;
|
||||
return changed;
|
||||
}
|
||||
|
||||
private ItemResolveArgs CreateResolveArgs(IDirectoryService directoryService, bool setPhysicalLocations)
|
||||
{
|
||||
ClearCache();
|
||||
|
||||
var path = ContainingFolderPath;
|
||||
|
||||
var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, LibraryManager)
|
||||
{
|
||||
FileInfo = FileSystem.GetDirectoryInfo(path)
|
||||
};
|
||||
|
||||
// Gather child folder and files
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
// When resolving the root, we need it's grandchildren (children of user views)
|
||||
var flattenFolderDepth = 2;
|
||||
|
||||
var files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, FileSystem, CollectionFolder.ApplicationHost, Logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: true);
|
||||
|
||||
// Need to remove subpaths that may have been resolved from shortcuts
|
||||
// Example: if \\server\movies exists, then strip out \\server\movies\action
|
||||
files = LibraryManager.NormalizeRootPathList(files).ToArray();
|
||||
|
||||
args.FileSystemChildren = files;
|
||||
}
|
||||
|
||||
_requiresRefresh = _requiresRefresh || !args.PhysicalLocations.SequenceEqual(PhysicalLocations);
|
||||
if (setPhysicalLocations)
|
||||
{
|
||||
PhysicalLocationsList = args.PhysicalLocations;
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
|
||||
{
|
||||
return base.GetNonCachedChildren(directoryService).Concat(_virtualChildren);
|
||||
}
|
||||
|
||||
protected override async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, bool allowRemoveRoot, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
|
||||
{
|
||||
ClearCache();
|
||||
|
||||
await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, allowRemoveRoot, refreshOptions, directoryService, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
ClearCache();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the virtual child.
|
||||
/// </summary>
|
||||
/// <param name="child">The child.</param>
|
||||
/// <exception cref="ArgumentNullException">Throws if child is null.</exception>
|
||||
public void AddVirtualChild(BaseItem child)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(child);
|
||||
|
||||
_virtualChildren.Add(child);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the virtual child.
|
||||
/// </summary>
|
||||
/// <param name="id">The id.</param>
|
||||
/// <returns>BaseItem.</returns>
|
||||
/// <exception cref="ArgumentNullException">The id is empty.</exception>
|
||||
public BaseItem FindVirtualChild(Guid id)
|
||||
{
|
||||
if (id.IsEmpty())
|
||||
{
|
||||
throw new ArgumentNullException(nameof(id));
|
||||
}
|
||||
|
||||
foreach (var child in _virtualChildren)
|
||||
{
|
||||
if (child.Id.Equals(id))
|
||||
{
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1002, CA1724, CA1826, CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Dto;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Audio.
|
||||
/// </summary>
|
||||
public class Audio : BaseItem,
|
||||
IHasAlbumArtist,
|
||||
IHasArtist,
|
||||
IHasMusicGenres,
|
||||
IHasLookupInfo<SongInfo>,
|
||||
IHasMediaSources
|
||||
{
|
||||
public Audio()
|
||||
{
|
||||
Artists = Array.Empty<string>();
|
||||
AlbumArtists = Array.Empty<string>();
|
||||
LyricFiles = Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public IReadOnlyList<string> Artists { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public IReadOnlyList<string> AlbumArtists { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPlayedStatus => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPeople => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsAddingToPlaylist => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsInheritedParentImages => true;
|
||||
|
||||
[JsonIgnore]
|
||||
protected override bool SupportsOwnedItems => false;
|
||||
|
||||
[JsonIgnore]
|
||||
public override Folder LatestItemsIndexContainer => AlbumEntity;
|
||||
|
||||
[JsonIgnore]
|
||||
public MusicAlbum AlbumEntity => FindParent<MusicAlbum>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the media.
|
||||
/// </summary>
|
||||
/// <value>The type of the media.</value>
|
||||
[JsonIgnore]
|
||||
public override MediaType MediaType => MediaType.Audio;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this audio has lyrics.
|
||||
/// </summary>
|
||||
public bool? HasLyrics { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the list of lyric paths.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> LyricFiles { get; set; }
|
||||
|
||||
public override double GetDefaultPrimaryImageAspectRatio()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
public override bool CanDownload()
|
||||
{
|
||||
return IsFileProtocol;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the name of the sort.
|
||||
/// </summary>
|
||||
/// <returns>System.String.</returns>
|
||||
protected override string CreateSortName()
|
||||
{
|
||||
return (ParentIndexNumber is not null ? ParentIndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty)
|
||||
+ (IndexNumber is not null ? IndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) + Name;
|
||||
}
|
||||
|
||||
public override List<string> GetUserDataKeys()
|
||||
{
|
||||
var list = base.GetUserDataKeys();
|
||||
|
||||
var songKey = IndexNumber.HasValue ? IndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) : string.Empty;
|
||||
|
||||
if (ParentIndexNumber.HasValue)
|
||||
{
|
||||
songKey = ParentIndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) + "-" + songKey;
|
||||
}
|
||||
|
||||
songKey += Name;
|
||||
|
||||
if (!string.IsNullOrEmpty(Album))
|
||||
{
|
||||
songKey = Album + "-" + songKey;
|
||||
}
|
||||
|
||||
var albumArtist = AlbumArtists.FirstOrDefault();
|
||||
if (!string.IsNullOrEmpty(albumArtist))
|
||||
{
|
||||
songKey = albumArtist + "-" + songKey;
|
||||
}
|
||||
|
||||
list.Insert(0, songKey);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public override UnratedItem GetBlockUnratedType()
|
||||
{
|
||||
if (SourceType == SourceType.Library)
|
||||
{
|
||||
return UnratedItem.Music;
|
||||
}
|
||||
|
||||
return base.GetBlockUnratedType();
|
||||
}
|
||||
|
||||
public SongInfo GetLookupInfo()
|
||||
{
|
||||
var info = GetItemLookupInfo<SongInfo>();
|
||||
|
||||
info.AlbumArtists = AlbumArtists;
|
||||
info.Album = Album;
|
||||
info.Artists = Artists;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
protected override IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources()
|
||||
=> new[] { ((BaseItem)this, MediaSourceType.Default) };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Controller.Library;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.Audio
|
||||
{
|
||||
public interface IHasAlbumArtist
|
||||
{
|
||||
IReadOnlyList<string> AlbumArtists { get; set; }
|
||||
}
|
||||
|
||||
public interface IHasArtist
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the artists.
|
||||
/// </summary>
|
||||
/// <value>The artists.</value>
|
||||
IReadOnlyList<string> Artists { get; set; }
|
||||
}
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
public static IEnumerable<string> GetAllArtists<T>(this T item)
|
||||
where T : IHasArtist, IHasAlbumArtist
|
||||
{
|
||||
return item.AlbumArtists.Concat(item.Artists).DistinctNames();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1819, CS1591
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.Audio
|
||||
{
|
||||
public interface IHasMusicGenres
|
||||
{
|
||||
string[] Genres { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1721, CA1826, CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MusicAlbum.
|
||||
/// </summary>
|
||||
[Common.RequiresSourceSerialisation]
|
||||
public class MusicAlbum : Folder, IHasAlbumArtist, IHasArtist, IHasMusicGenres, IHasLookupInfo<AlbumInfo>, IMetadataContainer
|
||||
{
|
||||
public MusicAlbum()
|
||||
{
|
||||
Artists = Array.Empty<string>();
|
||||
AlbumArtists = Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> AlbumArtists { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Artists { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsAddingToPlaylist => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsInheritedParentImages => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public MusicArtist MusicArtist => GetMusicArtist(new DtoOptions(true));
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPlayedStatus => false;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsCumulativeRunTimeTicks => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public string AlbumArtist => AlbumArtists.FirstOrDefault();
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPeople => true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tracks.
|
||||
/// </summary>
|
||||
/// <value>The tracks.</value>
|
||||
[JsonIgnore]
|
||||
public IEnumerable<Audio> Tracks => GetRecursiveChildren(i => i is Audio).Cast<Audio>();
|
||||
|
||||
public MusicArtist GetMusicArtist(DtoOptions options)
|
||||
{
|
||||
var parents = GetParents();
|
||||
foreach (var parent in parents)
|
||||
{
|
||||
if (parent is MusicArtist artist)
|
||||
{
|
||||
return artist;
|
||||
}
|
||||
}
|
||||
|
||||
var name = AlbumArtist;
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
return LibraryManager.GetArtist(name, options);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
|
||||
{
|
||||
return Tracks;
|
||||
}
|
||||
|
||||
public override double GetDefaultPrimaryImageAspectRatio()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
public override List<string> GetUserDataKeys()
|
||||
{
|
||||
var list = base.GetUserDataKeys();
|
||||
|
||||
var albumArtist = AlbumArtist;
|
||||
if (!string.IsNullOrEmpty(albumArtist))
|
||||
{
|
||||
list.Insert(0, albumArtist + "-" + Name);
|
||||
}
|
||||
|
||||
var id = this.GetProviderId(MetadataProvider.MusicBrainzAlbum);
|
||||
|
||||
if (!string.IsNullOrEmpty(id))
|
||||
{
|
||||
list.Insert(0, "MusicAlbum-Musicbrainz-" + id);
|
||||
}
|
||||
|
||||
id = this.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup);
|
||||
|
||||
if (!string.IsNullOrEmpty(id))
|
||||
{
|
||||
list.Insert(0, "MusicAlbum-MusicBrainzReleaseGroup-" + id);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
protected override bool GetBlockUnratedValue(User user)
|
||||
{
|
||||
return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music);
|
||||
}
|
||||
|
||||
public override UnratedItem GetBlockUnratedType()
|
||||
{
|
||||
return UnratedItem.Music;
|
||||
}
|
||||
|
||||
public AlbumInfo GetLookupInfo()
|
||||
{
|
||||
var id = GetItemLookupInfo<AlbumInfo>();
|
||||
|
||||
id.AlbumArtists = AlbumArtists;
|
||||
|
||||
var artist = GetMusicArtist(new DtoOptions(false));
|
||||
|
||||
if (artist is not null)
|
||||
{
|
||||
id.ArtistProviderIds = artist.ProviderIds;
|
||||
}
|
||||
|
||||
id.SongInfos = GetRecursiveChildren(i => i is Audio)
|
||||
.Cast<Audio>()
|
||||
.Select(i => i.GetLookupInfo())
|
||||
.ToList();
|
||||
|
||||
var album = id.SongInfos
|
||||
.Select(i => i.Album)
|
||||
.FirstOrDefault(i => !string.IsNullOrEmpty(i));
|
||||
|
||||
if (!string.IsNullOrEmpty(album))
|
||||
{
|
||||
id.Name = album;
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
var items = GetRecursiveChildren();
|
||||
|
||||
var totalItems = items.Count;
|
||||
var numComplete = 0;
|
||||
|
||||
var childUpdateType = ItemUpdateType.None;
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var updateType = await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||
childUpdateType = childUpdateType | updateType;
|
||||
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= totalItems;
|
||||
progress.Report(percent * 95);
|
||||
}
|
||||
|
||||
var parentRefreshOptions = refreshOptions;
|
||||
if (childUpdateType > ItemUpdateType.None)
|
||||
{
|
||||
parentRefreshOptions = new MetadataRefreshOptions(refreshOptions)
|
||||
{
|
||||
MetadataRefreshMode = MetadataRefreshMode.FullRefresh
|
||||
};
|
||||
}
|
||||
|
||||
// Refresh current item
|
||||
await RefreshMetadata(parentRefreshOptions, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!refreshOptions.IsAutomated)
|
||||
{
|
||||
await RefreshArtists(refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RefreshArtists(MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var i in this.GetAllArtists())
|
||||
{
|
||||
// This should not be necessary but we're seeing some cases of it
|
||||
if (string.IsNullOrEmpty(i))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var artist = LibraryManager.GetArtist(i);
|
||||
|
||||
if (!artist.IsAccessedByName)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await artist.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MusicArtist.
|
||||
/// </summary>
|
||||
[Common.RequiresSourceSerialisation]
|
||||
public class MusicArtist : Folder, IItemByName, IHasMusicGenres, IHasDualAccess, IHasLookupInfo<ArtistInfo>
|
||||
{
|
||||
[JsonIgnore]
|
||||
public bool IsAccessedByName => ParentId.IsEmpty();
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool IsFolder => !IsAccessedByName;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsInheritedParentImages => false;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsCumulativeRunTimeTicks => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool IsDisplayedAsFolder => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsAddingToPlaylist => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPlayedStatus => false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the folder containing the item.
|
||||
/// If the item is a folder, it returns the folder itself.
|
||||
/// </summary>
|
||||
/// <value>The containing folder path.</value>
|
||||
[JsonIgnore]
|
||||
public override string ContainingFolderPath => Path;
|
||||
|
||||
[JsonIgnore]
|
||||
public override IEnumerable<BaseItem> Children
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsAccessedByName)
|
||||
{
|
||||
return Enumerable.Empty<BaseItem>();
|
||||
}
|
||||
|
||||
return base.Children;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPeople => false;
|
||||
|
||||
public static string GetPath(string name)
|
||||
{
|
||||
return GetPath(name, true);
|
||||
}
|
||||
|
||||
public override double GetDefaultPrimaryImageAspectRatio()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
public override bool CanDelete()
|
||||
{
|
||||
return !IsAccessedByName;
|
||||
}
|
||||
|
||||
public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query)
|
||||
{
|
||||
if (query.IncludeItemTypes.Length == 0)
|
||||
{
|
||||
query.IncludeItemTypes = new[] { BaseItemKind.Audio, BaseItemKind.MusicVideo, BaseItemKind.MusicAlbum };
|
||||
query.ArtistIds = new[] { Id };
|
||||
}
|
||||
|
||||
return LibraryManager.GetItemList(query);
|
||||
}
|
||||
|
||||
public override int GetChildCount(User user)
|
||||
{
|
||||
return IsAccessedByName ? 0 : base.GetChildCount(user);
|
||||
}
|
||||
|
||||
public override bool IsSaveLocalMetadataEnabled()
|
||||
{
|
||||
if (IsAccessedByName)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.IsSaveLocalMetadataEnabled();
|
||||
}
|
||||
|
||||
protected override async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, bool allowRemoveRoot, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
|
||||
{
|
||||
if (IsAccessedByName)
|
||||
{
|
||||
// Should never get in here anyway
|
||||
return;
|
||||
}
|
||||
|
||||
await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, false, refreshOptions, directoryService, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override List<string> GetUserDataKeys()
|
||||
{
|
||||
var list = base.GetUserDataKeys();
|
||||
|
||||
list.InsertRange(0, GetUserDataKeys(this));
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user data key.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
private static List<string> GetUserDataKeys(MusicArtist item)
|
||||
{
|
||||
var list = new List<string>();
|
||||
if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out var externalId))
|
||||
{
|
||||
list.Add("Artist-Musicbrainz-" + externalId);
|
||||
}
|
||||
|
||||
list.Add("Artist-" + (item.Name ?? string.Empty).RemoveDiacritics());
|
||||
return list;
|
||||
}
|
||||
|
||||
public override string CreatePresentationUniqueKey()
|
||||
{
|
||||
return "Artist-" + (Name ?? string.Empty).RemoveDiacritics();
|
||||
}
|
||||
|
||||
protected override bool GetBlockUnratedValue(User user)
|
||||
{
|
||||
return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Music);
|
||||
}
|
||||
|
||||
public override UnratedItem GetBlockUnratedType()
|
||||
{
|
||||
return UnratedItem.Music;
|
||||
}
|
||||
|
||||
public ArtistInfo GetLookupInfo()
|
||||
{
|
||||
var info = GetItemLookupInfo<ArtistInfo>();
|
||||
|
||||
info.SongInfos = GetRecursiveChildren(i => i is Audio)
|
||||
.Cast<Audio>()
|
||||
.Select(i => i.GetLookupInfo())
|
||||
.ToList();
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
public static string GetPath(string name, bool normalizeName)
|
||||
{
|
||||
// Trim the period at the end because windows will have a hard time with that
|
||||
var validName = normalizeName ?
|
||||
FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
|
||||
name;
|
||||
|
||||
return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.ArtistsPath, validName);
|
||||
}
|
||||
|
||||
private string GetRebasedPath()
|
||||
{
|
||||
return GetPath(System.IO.Path.GetFileName(Path), false);
|
||||
}
|
||||
|
||||
public override bool RequiresRefresh()
|
||||
{
|
||||
if (IsAccessedByName)
|
||||
{
|
||||
var newPath = GetRebasedPath();
|
||||
if (!string.Equals(Path, newPath, StringComparison.Ordinal))
|
||||
{
|
||||
Logger.LogDebug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return base.RequiresRefresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is called before any metadata refresh and returns true or false indicating if changes were made.
|
||||
/// </summary>
|
||||
/// <param name="replaceAllMetadata">Option to replace metadata.</param>
|
||||
/// <returns>True if metadata changed.</returns>
|
||||
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
|
||||
{
|
||||
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
|
||||
|
||||
if (IsAccessedByName)
|
||||
{
|
||||
var newPath = GetRebasedPath();
|
||||
if (!string.Equals(Path, newPath, StringComparison.Ordinal))
|
||||
{
|
||||
Path = newPath;
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanges;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.Audio
|
||||
{
|
||||
/// <summary>
|
||||
/// Class MusicGenre.
|
||||
/// </summary>
|
||||
[Common.RequiresSourceSerialisation]
|
||||
public class MusicGenre : BaseItem, IItemByName
|
||||
{
|
||||
[JsonIgnore]
|
||||
public override bool SupportsAddingToPlaylist => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsAncestors => false;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool IsDisplayedAsFolder => true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the folder containing the item.
|
||||
/// If the item is a folder, it returns the folder itself.
|
||||
/// </summary>
|
||||
/// <value>The containing folder path.</value>
|
||||
[JsonIgnore]
|
||||
public override string ContainingFolderPath => Path;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPeople => false;
|
||||
|
||||
public override List<string> GetUserDataKeys()
|
||||
{
|
||||
var list = base.GetUserDataKeys();
|
||||
|
||||
list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
|
||||
return list;
|
||||
}
|
||||
|
||||
public override string CreatePresentationUniqueKey()
|
||||
{
|
||||
return GetUserDataKeys()[0];
|
||||
}
|
||||
|
||||
public override double GetDefaultPrimaryImageAspectRatio()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
public override bool CanDelete()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool IsSaveLocalMetadataEnabled()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query)
|
||||
{
|
||||
query.GenreIds = new[] { Id };
|
||||
query.IncludeItemTypes = new[] { BaseItemKind.MusicVideo, BaseItemKind.Audio, BaseItemKind.MusicAlbum, BaseItemKind.MusicArtist };
|
||||
|
||||
return LibraryManager.GetItemList(query);
|
||||
}
|
||||
|
||||
public static string GetPath(string name)
|
||||
{
|
||||
return GetPath(name, true);
|
||||
}
|
||||
|
||||
public static string GetPath(string name, bool normalizeName)
|
||||
{
|
||||
// Trim the period at the end because windows will have a hard time with that
|
||||
var validName = normalizeName ?
|
||||
FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
|
||||
name;
|
||||
|
||||
return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.MusicGenrePath, validName);
|
||||
}
|
||||
|
||||
private string GetRebasedPath()
|
||||
{
|
||||
return GetPath(System.IO.Path.GetFileName(Path), false);
|
||||
}
|
||||
|
||||
public override bool RequiresRefresh()
|
||||
{
|
||||
var newPath = GetRebasedPath();
|
||||
if (!string.Equals(Path, newPath, StringComparison.Ordinal))
|
||||
{
|
||||
Logger.LogDebug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.RequiresRefresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is called before any metadata refresh and returns true or false indicating if changes were made.
|
||||
/// </summary>
|
||||
/// <param name="replaceAllMetadata">Option to replace metadata.</param>
|
||||
/// <returns>True if metadata changed.</returns>
|
||||
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
|
||||
{
|
||||
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
|
||||
|
||||
var newPath = GetRebasedPath();
|
||||
if (!string.Equals(Path, newPath, StringComparison.Ordinal))
|
||||
{
|
||||
Path = newPath;
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
return hasChanges;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1724, CS1591
|
||||
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
[Common.RequiresSourceSerialisation]
|
||||
public class AudioBook : Audio.Audio, IHasSeries, IHasLookupInfo<SongInfo>
|
||||
{
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPositionTicksResume => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPlayedStatus => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public string SeriesPresentationUniqueKey { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string SeriesName { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Guid SeriesId { get; set; }
|
||||
|
||||
public string FindSeriesSortName()
|
||||
{
|
||||
return SeriesName;
|
||||
}
|
||||
|
||||
public string FindSeriesName()
|
||||
{
|
||||
return SeriesName;
|
||||
}
|
||||
|
||||
public string FindSeriesPresentationUniqueKey()
|
||||
{
|
||||
return SeriesPresentationUniqueKey;
|
||||
}
|
||||
|
||||
public override double GetDefaultPrimaryImageAspectRatio()
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
public Guid FindSeriesId()
|
||||
{
|
||||
return SeriesId;
|
||||
}
|
||||
|
||||
public override bool CanDownload()
|
||||
{
|
||||
return IsFileProtocol;
|
||||
}
|
||||
|
||||
public override UnratedItem GetBlockUnratedType()
|
||||
{
|
||||
return UnratedItem.Book;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,132 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public static class BaseItemExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the image path.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="imageType">Type of the image.</param>
|
||||
/// <returns>System.String.</returns>
|
||||
public static string GetImagePath(this BaseItem item, ImageType imageType)
|
||||
{
|
||||
return item.GetImagePath(imageType, 0);
|
||||
}
|
||||
|
||||
public static bool HasImage(this BaseItem item, ImageType imageType)
|
||||
{
|
||||
return item.HasImage(imageType, 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the image path.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="imageType">Type of the image.</param>
|
||||
/// <param name="file">The file.</param>
|
||||
public static void SetImagePath(this BaseItem item, ImageType imageType, FileSystemMetadata file)
|
||||
{
|
||||
item.SetImagePath(imageType, 0, file);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the image path.
|
||||
/// </summary>
|
||||
/// <param name="item">The item.</param>
|
||||
/// <param name="imageType">Type of the image.</param>
|
||||
/// <param name="file">The file.</param>
|
||||
public static void SetImagePath(this BaseItem item, ImageType imageType, string file)
|
||||
{
|
||||
if (file.StartsWith("http", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
item.SetImage(
|
||||
new ItemImageInfo
|
||||
{
|
||||
Path = file,
|
||||
Type = imageType
|
||||
},
|
||||
0);
|
||||
}
|
||||
else
|
||||
{
|
||||
item.SetImagePath(imageType, BaseItem.FileSystem.GetFileInfo(file));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies all properties on object. Skips properties that do not exist.
|
||||
/// </summary>
|
||||
/// <param name="source">The source object.</param>
|
||||
/// <param name="dest">The destination object.</param>
|
||||
/// <typeparam name="T">Source type.</typeparam>
|
||||
/// <typeparam name="TU">Destination type.</typeparam>
|
||||
public static void DeepCopy<T, TU>(this T source, TU dest)
|
||||
where T : BaseItem
|
||||
where TU : BaseItem
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
|
||||
ArgumentNullException.ThrowIfNull(dest);
|
||||
|
||||
var destProps = typeof(TU).GetProperties().Where(x => x.CanWrite).ToList();
|
||||
|
||||
foreach (var sourceProp in typeof(T).GetProperties())
|
||||
{
|
||||
// We should be able to write to the property
|
||||
// for both the source and destination type
|
||||
// This is only false when the derived type hides the base member
|
||||
// (which we shouldn't copy anyway)
|
||||
if (!sourceProp.CanRead || !sourceProp.CanWrite)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var v = sourceProp.GetValue(source);
|
||||
if (v is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var p = destProps.Find(x => x.Name == sourceProp.Name);
|
||||
p?.SetValue(dest, v);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies all properties on newly created object. Skips properties that do not exist.
|
||||
/// </summary>
|
||||
/// <param name="source">The source object.</param>
|
||||
/// <typeparam name="T">Source type.</typeparam>
|
||||
/// <typeparam name="TU">Destination type.</typeparam>
|
||||
/// <returns>Destination object.</returns>
|
||||
public static TU DeepCopy<T, TU>(this T source)
|
||||
where T : BaseItem
|
||||
where TU : BaseItem, new()
|
||||
{
|
||||
var dest = new TU();
|
||||
source.DeepCopy(dest);
|
||||
return dest;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the item has changed.
|
||||
/// </summary>
|
||||
/// <param name="source">The source object.</param>
|
||||
/// <param name="asOf">The timestamp to detect changes as of.</param>
|
||||
/// <typeparam name="T">Source type.</typeparam>
|
||||
/// <returns>Whether the item has changed.</returns>
|
||||
public static bool HasChanged<T>(this T source, DateTime asOf)
|
||||
where T : BaseItem
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
return source.DateModified.Subtract(asOf).Duration().TotalSeconds > 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Jellyfin.Data.Enums;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Plugins derive from and export this class to create a folder that will appear in the root along
|
||||
/// with all the other actual physical folders in the system.
|
||||
/// </summary>
|
||||
public abstract class BasePluginFolder : Folder, ICollectionFolder
|
||||
{
|
||||
[JsonIgnore]
|
||||
public virtual CollectionType? CollectionType => null;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsInheritedParentImages => false;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPeople => false;
|
||||
|
||||
public override bool CanDelete()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool IsSaveLocalMetadataEnabled()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
[Common.RequiresSourceSerialisation]
|
||||
public class Book : BaseItem, IHasLookupInfo<BookInfo>, IHasSeries
|
||||
{
|
||||
public Book()
|
||||
{
|
||||
this.RunTimeTicks = TimeSpan.TicksPerSecond;
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public override MediaType MediaType => MediaType.Book;
|
||||
|
||||
public override bool SupportsPlayedStatus => true;
|
||||
|
||||
public override bool SupportsPositionTicksResume => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPeople => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public string SeriesPresentationUniqueKey { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string SeriesName { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Guid SeriesId { get; set; }
|
||||
|
||||
public string FindSeriesSortName()
|
||||
{
|
||||
return SeriesName;
|
||||
}
|
||||
|
||||
public string FindSeriesName()
|
||||
{
|
||||
return SeriesName;
|
||||
}
|
||||
|
||||
public string FindSeriesPresentationUniqueKey()
|
||||
{
|
||||
return SeriesPresentationUniqueKey;
|
||||
}
|
||||
|
||||
public Guid FindSeriesId()
|
||||
{
|
||||
return SeriesId;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanDownload()
|
||||
{
|
||||
return IsFileProtocol;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override UnratedItem GetBlockUnratedType()
|
||||
{
|
||||
return UnratedItem.Book;
|
||||
}
|
||||
|
||||
public BookInfo GetLookupInfo()
|
||||
{
|
||||
var info = GetItemLookupInfo<BookInfo>();
|
||||
|
||||
if (string.IsNullOrEmpty(SeriesName))
|
||||
{
|
||||
info.SeriesName = GetParents().Select(i => i.Name).FirstOrDefault();
|
||||
}
|
||||
else
|
||||
{
|
||||
info.SeriesName = SeriesName;
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Extensions.Json;
|
||||
using MediaBrowser.Controller.IO;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Configuration;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.Serialization;
|
||||
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Specialized Folder class that points to a subset of the physical folders in the system.
|
||||
/// It is created from the user-specific folders within the system root.
|
||||
/// </summary>
|
||||
public class CollectionFolder : Folder, ICollectionFolder
|
||||
{
|
||||
private static readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
|
||||
private static readonly ConcurrentDictionary<string, LibraryOptions> _libraryOptions = new ConcurrentDictionary<string, LibraryOptions>();
|
||||
private bool _requiresRefresh;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CollectionFolder"/> class.
|
||||
/// </summary>
|
||||
public CollectionFolder()
|
||||
{
|
||||
PhysicalLocationsList = Array.Empty<string>();
|
||||
PhysicalFolderIds = Array.Empty<Guid>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display preferences id.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Allow different display preferences for each collection folder.
|
||||
/// </remarks>
|
||||
/// <value>The display prefs id.</value>
|
||||
[JsonIgnore]
|
||||
public override Guid DisplayPreferencesId => Id;
|
||||
|
||||
[JsonIgnore]
|
||||
public override string[] PhysicalLocations => PhysicalLocationsList;
|
||||
|
||||
public string[] PhysicalLocationsList { get; set; }
|
||||
|
||||
public Guid[] PhysicalFolderIds { get; set; }
|
||||
|
||||
public static IXmlSerializer XmlSerializer { get; set; }
|
||||
|
||||
public static IServerApplicationHost ApplicationHost { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPlayedStatus => false;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsInheritedParentImages => false;
|
||||
|
||||
public CollectionType? CollectionType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the item's children.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Our children are actually just references to the ones in the physical root...
|
||||
/// </remarks>
|
||||
/// <value>The actual children.</value>
|
||||
[JsonIgnore]
|
||||
public override IEnumerable<BaseItem> Children => GetActualChildren();
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPeople => false;
|
||||
|
||||
public override bool CanDelete()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public LibraryOptions GetLibraryOptions()
|
||||
{
|
||||
return GetLibraryOptions(Path);
|
||||
}
|
||||
|
||||
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
|
||||
{
|
||||
if (GetLibraryOptions().Enabled)
|
||||
{
|
||||
return base.IsVisible(user, skipAllowedTagsCheck);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static LibraryOptions LoadLibraryOptions(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (XmlSerializer.DeserializeFromFile(typeof(LibraryOptions), GetLibraryOptionsPath(path)) is not LibraryOptions result)
|
||||
{
|
||||
return new LibraryOptions();
|
||||
}
|
||||
|
||||
foreach (var mediaPath in result.PathInfos)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(mediaPath.Path))
|
||||
{
|
||||
mediaPath.Path = ApplicationHost.ExpandVirtualPath(mediaPath.Path);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (FileNotFoundException)
|
||||
{
|
||||
return new LibraryOptions();
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return new LibraryOptions();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error loading library options");
|
||||
|
||||
return new LibraryOptions();
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetLibraryOptionsPath(string path)
|
||||
{
|
||||
return System.IO.Path.Combine(path, "options.xml");
|
||||
}
|
||||
|
||||
public void UpdateLibraryOptions(LibraryOptions options)
|
||||
{
|
||||
SaveLibraryOptions(Path, options);
|
||||
}
|
||||
|
||||
public static LibraryOptions GetLibraryOptions(string path)
|
||||
=> _libraryOptions.GetOrAdd(path, LoadLibraryOptions);
|
||||
|
||||
public static void SaveLibraryOptions(string path, LibraryOptions options)
|
||||
{
|
||||
_libraryOptions[path] = options;
|
||||
|
||||
var clone = JsonSerializer.Deserialize<LibraryOptions>(JsonSerializer.SerializeToUtf8Bytes(options, _jsonOptions), _jsonOptions);
|
||||
foreach (var mediaPath in clone.PathInfos)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(mediaPath.Path))
|
||||
{
|
||||
mediaPath.Path = ApplicationHost.ReverseVirtualPath(mediaPath.Path);
|
||||
}
|
||||
}
|
||||
|
||||
XmlSerializer.SerializeToFile(clone, GetLibraryOptionsPath(path));
|
||||
}
|
||||
|
||||
public static void OnCollectionFolderChange()
|
||||
=> _libraryOptions.Clear();
|
||||
|
||||
public override bool IsSaveLocalMetadataEnabled()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override FileSystemMetadata[] GetFileSystemChildren(IDirectoryService directoryService)
|
||||
{
|
||||
return CreateResolveArgs(directoryService, true).FileSystemChildren;
|
||||
}
|
||||
|
||||
public override bool RequiresRefresh()
|
||||
{
|
||||
var changed = base.RequiresRefresh() || _requiresRefresh;
|
||||
|
||||
if (!changed)
|
||||
{
|
||||
var locations = PhysicalLocations;
|
||||
|
||||
var newLocations = CreateResolveArgs(new DirectoryService(FileSystem), false).PhysicalLocations;
|
||||
|
||||
if (!locations.SequenceEqual(newLocations))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!changed)
|
||||
{
|
||||
var folderIds = PhysicalFolderIds;
|
||||
|
||||
var newFolderIds = GetPhysicalFolders(false).Select(i => i.Id).ToList();
|
||||
|
||||
if (!folderIds.SequenceEqual(newFolderIds))
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
|
||||
{
|
||||
var changed = base.BeforeMetadataRefresh(replaceAllMetadata) || _requiresRefresh;
|
||||
_requiresRefresh = false;
|
||||
return changed;
|
||||
}
|
||||
|
||||
public override double? GetRefreshProgress()
|
||||
{
|
||||
var folders = GetPhysicalFolders(true).ToList();
|
||||
double totalProgresses = 0;
|
||||
var foldersWithProgress = 0;
|
||||
|
||||
foreach (var folder in folders)
|
||||
{
|
||||
var progress = ProviderManager.GetRefreshProgress(folder.Id);
|
||||
if (progress.HasValue)
|
||||
{
|
||||
totalProgresses += progress.Value;
|
||||
foldersWithProgress++;
|
||||
}
|
||||
}
|
||||
|
||||
if (foldersWithProgress == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return totalProgresses / foldersWithProgress;
|
||||
}
|
||||
|
||||
protected override bool RefreshLinkedChildren(IEnumerable<FileSystemMetadata> fileSystemChildren)
|
||||
{
|
||||
return RefreshLinkedChildrenInternal(true);
|
||||
}
|
||||
|
||||
private bool RefreshLinkedChildrenInternal(bool setFolders)
|
||||
{
|
||||
var physicalFolders = GetPhysicalFolders(false)
|
||||
.ToList();
|
||||
|
||||
var linkedChildren = physicalFolders
|
||||
.SelectMany(c => c.LinkedChildren)
|
||||
.ToList();
|
||||
|
||||
var changed = !linkedChildren.SequenceEqual(LinkedChildren, new LinkedChildComparer(FileSystem));
|
||||
|
||||
LinkedChildren = linkedChildren.ToArray();
|
||||
|
||||
var folderIds = PhysicalFolderIds;
|
||||
var newFolderIds = physicalFolders.Select(i => i.Id).ToArray();
|
||||
|
||||
if (!folderIds.SequenceEqual(newFolderIds))
|
||||
{
|
||||
changed = true;
|
||||
if (setFolders)
|
||||
{
|
||||
PhysicalFolderIds = newFolderIds;
|
||||
}
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
private ItemResolveArgs CreateResolveArgs(IDirectoryService directoryService, bool setPhysicalLocations)
|
||||
{
|
||||
var path = ContainingFolderPath;
|
||||
|
||||
var args = new ItemResolveArgs(ConfigurationManager.ApplicationPaths, LibraryManager)
|
||||
{
|
||||
FileInfo = FileSystem.GetDirectoryInfo(path),
|
||||
Parent = GetParent() as Folder,
|
||||
CollectionType = CollectionType
|
||||
};
|
||||
|
||||
// Gather child folder and files
|
||||
if (args.IsDirectory)
|
||||
{
|
||||
var flattenFolderDepth = 0;
|
||||
|
||||
var files = FileData.GetFilteredFileSystemEntries(directoryService, args.Path, FileSystem, ApplicationHost, Logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: true);
|
||||
|
||||
args.FileSystemChildren = files;
|
||||
}
|
||||
|
||||
_requiresRefresh = _requiresRefresh || !args.PhysicalLocations.SequenceEqual(PhysicalLocations);
|
||||
|
||||
if (setPhysicalLocations)
|
||||
{
|
||||
PhysicalLocationsList = args.PhysicalLocations;
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compare our current children (presumably just read from the repo) with the current state of the file system and adjust for any changes
|
||||
/// ***Currently does not contain logic to maintain items that are unavailable in the file system***.
|
||||
/// </summary>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="recursive">if set to <c>true</c> [recursive].</param>
|
||||
/// <param name="refreshChildMetadata">if set to <c>true</c> [refresh child metadata].</param>
|
||||
/// <param name="allowRemoveRoot">remove item even this folder is root.</param>
|
||||
/// <param name="refreshOptions">The refresh options.</param>
|
||||
/// <param name="directoryService">The directory service.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, bool allowRemoveRoot, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public IEnumerable<BaseItem> GetActualChildren()
|
||||
{
|
||||
return GetPhysicalFolders(true).SelectMany(c => c.Children);
|
||||
}
|
||||
|
||||
public IEnumerable<Folder> GetPhysicalFolders()
|
||||
{
|
||||
return GetPhysicalFolders(true);
|
||||
}
|
||||
|
||||
private IEnumerable<Folder> GetPhysicalFolders(bool enableCache)
|
||||
{
|
||||
if (enableCache)
|
||||
{
|
||||
return PhysicalFolderIds.Select(i => LibraryManager.GetItemById(i)).OfType<Folder>();
|
||||
}
|
||||
|
||||
var rootChildren = LibraryManager.RootFolder.Children
|
||||
.OfType<Folder>()
|
||||
.ToList();
|
||||
|
||||
return PhysicalLocations
|
||||
.Where(i => !FileSystem.AreEqual(i, Path))
|
||||
.SelectMany(i => GetPhysicalParents(i, rootChildren))
|
||||
.DistinctBy(x => x.Id);
|
||||
}
|
||||
|
||||
private IEnumerable<Folder> GetPhysicalParents(string path, List<Folder> rootChildren)
|
||||
{
|
||||
var result = rootChildren
|
||||
.Where(i => FileSystem.AreEqual(i.Path, path))
|
||||
.ToList();
|
||||
|
||||
if (result.Count == 0)
|
||||
{
|
||||
if (LibraryManager.FindByPath(path, true) is Folder folder)
|
||||
{
|
||||
result.Add(folder);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Extensions.
|
||||
/// </summary>
|
||||
public static class Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds the trailer URL.
|
||||
/// </summary>
|
||||
/// <param name="item">Media item.</param>
|
||||
/// <param name="url">Trailer URL.</param>
|
||||
public static void AddTrailerUrl(this BaseItem item, string url)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(url);
|
||||
|
||||
var current = item.RemoteTrailers.FirstOrDefault(i => string.Equals(i.Url, url, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (current is null)
|
||||
{
|
||||
var mediaUrl = new MediaUrl
|
||||
{
|
||||
Url = url
|
||||
};
|
||||
|
||||
if (item.RemoteTrailers.Count == 0)
|
||||
{
|
||||
item.RemoteTrailers = [mediaUrl];
|
||||
}
|
||||
else
|
||||
{
|
||||
item.RemoteTrailers = [..item.RemoteTrailers, mediaUrl];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,130 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Genre.
|
||||
/// </summary>
|
||||
[Common.RequiresSourceSerialisation]
|
||||
public class Genre : BaseItem, IItemByName
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the folder containing the item.
|
||||
/// If the item is a folder, it returns the folder itself.
|
||||
/// </summary>
|
||||
/// <value>The containing folder path.</value>
|
||||
[JsonIgnore]
|
||||
public override string ContainingFolderPath => Path;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool IsDisplayedAsFolder => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsAncestors => false;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPeople => false;
|
||||
|
||||
public override List<string> GetUserDataKeys()
|
||||
{
|
||||
var list = base.GetUserDataKeys();
|
||||
|
||||
list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
|
||||
return list;
|
||||
}
|
||||
|
||||
public override string CreatePresentationUniqueKey()
|
||||
{
|
||||
return GetUserDataKeys()[0];
|
||||
}
|
||||
|
||||
public override double GetDefaultPrimaryImageAspectRatio()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
public override bool IsSaveLocalMetadataEnabled()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool CanDelete()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query)
|
||||
{
|
||||
query.GenreIds = new[] { Id };
|
||||
query.ExcludeItemTypes = new[]
|
||||
{
|
||||
BaseItemKind.MusicVideo,
|
||||
BaseItemKind.Audio,
|
||||
BaseItemKind.MusicAlbum,
|
||||
BaseItemKind.MusicArtist
|
||||
};
|
||||
|
||||
return LibraryManager.GetItemList(query);
|
||||
}
|
||||
|
||||
public static string GetPath(string name)
|
||||
{
|
||||
return GetPath(name, true);
|
||||
}
|
||||
|
||||
public static string GetPath(string name, bool normalizeName)
|
||||
{
|
||||
// Trim the period at the end because windows will have a hard time with that
|
||||
var validName = normalizeName ?
|
||||
FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
|
||||
name;
|
||||
|
||||
return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.GenrePath, validName);
|
||||
}
|
||||
|
||||
private string GetRebasedPath()
|
||||
{
|
||||
return GetPath(System.IO.Path.GetFileName(Path), false);
|
||||
}
|
||||
|
||||
public override bool RequiresRefresh()
|
||||
{
|
||||
var newPath = GetRebasedPath();
|
||||
if (!string.Equals(Path, newPath, StringComparison.Ordinal))
|
||||
{
|
||||
Logger.LogDebug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.RequiresRefresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is called before any metadata refresh and returns true if changes were made.
|
||||
/// </summary>
|
||||
/// <param name="replaceAllMetadata">Whether to replace all metadata.</param>
|
||||
/// <returns>true if the item has change, else false.</returns>
|
||||
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
|
||||
{
|
||||
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
|
||||
|
||||
var newPath = GetRebasedPath();
|
||||
if (!string.Equals(Path, newPath, StringComparison.Ordinal))
|
||||
{
|
||||
Path = newPath;
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
return hasChanges;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1819, CS1591
|
||||
|
||||
using System;
|
||||
using Jellyfin.Data.Enums;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// This is just a marker interface to denote top level folders.
|
||||
/// </summary>
|
||||
public interface ICollectionFolder : IHasCollectionType
|
||||
{
|
||||
string Path { get; }
|
||||
|
||||
string Name { get; }
|
||||
|
||||
Guid Id { get; }
|
||||
|
||||
string[] PhysicalLocations { get; }
|
||||
}
|
||||
|
||||
public interface ISupportsUserSpecificView
|
||||
{
|
||||
bool EnableUserSpecificView { get; }
|
||||
}
|
||||
|
||||
public interface IHasCollectionType
|
||||
{
|
||||
CollectionType? CollectionType { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#nullable disable
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface IHasAspectRatio.
|
||||
/// </summary>
|
||||
public interface IHasAspectRatio
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the aspect ratio.
|
||||
/// </summary>
|
||||
/// <value>The aspect ratio.</value>
|
||||
string AspectRatio { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#nullable disable
|
||||
|
||||
using Jellyfin.Data.Enums;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface IHasDisplayOrder.
|
||||
/// </summary>
|
||||
public interface IHasDisplayOrder
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the display order.
|
||||
/// </summary>
|
||||
/// <value>The display order.</value>
|
||||
string DisplayOrder { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public interface IHasMediaSources
|
||||
{
|
||||
Guid Id { get; set; }
|
||||
|
||||
long? RunTimeTicks { get; set; }
|
||||
|
||||
string Path { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the media sources.
|
||||
/// </summary>
|
||||
/// <param name="enablePathSubstitution"><c>true</c> to enable path substitution, <c>false</c> to not.</param>
|
||||
/// <returns>A list of media sources.</returns>
|
||||
IReadOnlyList<MediaSourceInfo> GetMediaSources(bool enablePathSubstitution);
|
||||
|
||||
IReadOnlyList<MediaStream> GetMediaStreams();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using MediaBrowser.Model.LiveTv;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public interface IHasProgramAttributes
|
||||
{
|
||||
bool IsMovie { get; set; }
|
||||
|
||||
bool IsSports { get; }
|
||||
|
||||
bool IsNews { get; }
|
||||
|
||||
bool IsKids { get; }
|
||||
|
||||
bool IsRepeat { get; set; }
|
||||
|
||||
bool IsSeries { get; set; }
|
||||
|
||||
ProgramAudio? Audio { get; set; }
|
||||
|
||||
string EpisodeTitle { get; set; }
|
||||
|
||||
string ServiceName { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public interface IHasSeries
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the series.
|
||||
/// </summary>
|
||||
/// <value>The name of the series.</value>
|
||||
string SeriesName { get; set; }
|
||||
|
||||
Guid SeriesId { get; set; }
|
||||
|
||||
string SeriesPresentationUniqueKey { get; set; }
|
||||
|
||||
string FindSeriesName();
|
||||
|
||||
string FindSeriesSortName();
|
||||
|
||||
Guid FindSeriesId();
|
||||
|
||||
string FindSeriesPresentationUniqueKey();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public interface IHasSpecialFeatures
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the special feature ids.
|
||||
/// </summary>
|
||||
/// <value>The special feature ids.</value>
|
||||
IReadOnlyList<Guid> SpecialFeatureIds { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public interface IHasStartDate
|
||||
{
|
||||
DateTime StartDate { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public interface IHasTrailers : IHasProviderIds
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the remote trailers.
|
||||
/// </summary>
|
||||
/// <value>The remote trailers.</value>
|
||||
IReadOnlyList<MediaUrl> RemoteTrailers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the local trailers.
|
||||
/// </summary>
|
||||
/// <value>The local trailers.</value>
|
||||
IReadOnlyList<BaseItem> LocalTrailers { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Class providing extension methods for working with <see cref="IHasTrailers" />.
|
||||
/// </summary>
|
||||
public static class HasTrailerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the trailer count.
|
||||
/// </summary>
|
||||
/// <param name="item">Media item.</param>
|
||||
/// <returns><see cref="IReadOnlyList{Guid}" />.</returns>
|
||||
public static int GetTrailerCount(this IHasTrailers item)
|
||||
=> item.LocalTrailers.Count + item.RemoteTrailers.Count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Marker interface.
|
||||
/// </summary>
|
||||
public interface IItemByName
|
||||
{
|
||||
IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query);
|
||||
}
|
||||
|
||||
public interface IHasDualAccess : IItemByName
|
||||
{
|
||||
bool IsAccessedByName { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public interface IMetadataContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Refreshes all metadata.
|
||||
/// </summary>
|
||||
/// <param name="refreshOptions">The refresh options.</param>
|
||||
/// <param name="progress">The progress.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>Task.</returns>
|
||||
Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Marker interface to denote a class that supports being hidden underneath it's boxset.
|
||||
/// Just about anything can be placed into a boxset,
|
||||
/// but movies should also only appear underneath and not outside separately (subject to configuration).
|
||||
/// </summary>
|
||||
public interface ISupportsBoxSetGrouping
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public interface ISupportsPlaceHolders
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this instance is place holder.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance is place holder; otherwise, <c>false</c>.</value>
|
||||
bool IsPlaceHolder { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
#pragma warning disable CA1044, CA1819, CA2227, CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Diacritics.Extensions;
|
||||
using Jellyfin.Data;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public class InternalItemsQuery
|
||||
{
|
||||
public InternalItemsQuery()
|
||||
{
|
||||
AlbumArtistIds = Array.Empty<Guid>();
|
||||
AlbumIds = Array.Empty<Guid>();
|
||||
AncestorIds = Array.Empty<Guid>();
|
||||
ArtistIds = Array.Empty<Guid>();
|
||||
BlockUnratedItems = Array.Empty<UnratedItem>();
|
||||
BoxSetLibraryFolders = Array.Empty<Guid>();
|
||||
ChannelIds = Array.Empty<Guid>();
|
||||
ContributingArtistIds = Array.Empty<Guid>();
|
||||
DtoOptions = new DtoOptions();
|
||||
EnableTotalRecordCount = true;
|
||||
ExcludeArtistIds = Array.Empty<Guid>();
|
||||
ExcludeInheritedTags = Array.Empty<string>();
|
||||
IncludeInheritedTags = Array.Empty<string>();
|
||||
ExcludeItemIds = Array.Empty<Guid>();
|
||||
ExcludeItemTypes = Array.Empty<BaseItemKind>();
|
||||
ExcludeTags = Array.Empty<string>();
|
||||
GenreIds = Array.Empty<Guid>();
|
||||
Genres = Array.Empty<string>();
|
||||
GroupByPresentationUniqueKey = true;
|
||||
ImageTypes = Array.Empty<ImageType>();
|
||||
IncludeItemTypes = Array.Empty<BaseItemKind>();
|
||||
ItemIds = Array.Empty<Guid>();
|
||||
MediaTypes = Array.Empty<MediaType>();
|
||||
OfficialRatings = Array.Empty<string>();
|
||||
OrderBy = Array.Empty<(ItemSortBy, SortOrder)>();
|
||||
PersonIds = Array.Empty<Guid>();
|
||||
PersonTypes = Array.Empty<string>();
|
||||
PresetViews = Array.Empty<CollectionType?>();
|
||||
SeriesStatuses = Array.Empty<SeriesStatus>();
|
||||
SourceTypes = Array.Empty<SourceType>();
|
||||
StudioIds = Array.Empty<Guid>();
|
||||
Tags = Array.Empty<string>();
|
||||
TopParentIds = Array.Empty<Guid>();
|
||||
TrailerTypes = Array.Empty<TrailerType>();
|
||||
VideoTypes = Array.Empty<VideoType>();
|
||||
Years = Array.Empty<int>();
|
||||
SkipDeserialization = false;
|
||||
}
|
||||
|
||||
public InternalItemsQuery(User? user)
|
||||
: this()
|
||||
{
|
||||
if (user is not null)
|
||||
{
|
||||
SetUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
public bool Recursive { get; set; }
|
||||
|
||||
public int? StartIndex { get; set; }
|
||||
|
||||
public int? Limit { get; set; }
|
||||
|
||||
public User? User { get; set; }
|
||||
|
||||
public bool? IsFolder { get; set; }
|
||||
|
||||
public bool? IsFavorite { get; set; }
|
||||
|
||||
public bool? IsFavoriteOrLiked { get; set; }
|
||||
|
||||
public bool? IsLiked { get; set; }
|
||||
|
||||
public bool? IsPlayed { get; set; }
|
||||
|
||||
public bool? IsResumable { get; set; }
|
||||
|
||||
public bool? IncludeItemsByName { get; set; }
|
||||
|
||||
public MediaType[] MediaTypes { get; set; }
|
||||
|
||||
public BaseItemKind[] IncludeItemTypes { get; set; }
|
||||
|
||||
public BaseItemKind[] ExcludeItemTypes { get; set; }
|
||||
|
||||
public string[] ExcludeTags { get; set; }
|
||||
|
||||
public string[] ExcludeInheritedTags { get; set; }
|
||||
|
||||
public string[] IncludeInheritedTags { get; set; }
|
||||
|
||||
public IReadOnlyList<string> Genres { get; set; }
|
||||
|
||||
public bool? IsSpecialSeason { get; set; }
|
||||
|
||||
public bool? IsMissing { get; set; }
|
||||
|
||||
public bool? IsUnaired { get; set; }
|
||||
|
||||
public bool? CollapseBoxSetItems { get; set; }
|
||||
|
||||
public string? NameStartsWithOrGreater { get; set; }
|
||||
|
||||
public string? NameStartsWith { get; set; }
|
||||
|
||||
public string? NameLessThan { get; set; }
|
||||
|
||||
public string? NameContains { get; set; }
|
||||
|
||||
public string? MinSortName { get; set; }
|
||||
|
||||
public string? PresentationUniqueKey { get; set; }
|
||||
|
||||
public string? Path { get; set; }
|
||||
|
||||
public string? Name { get; set; }
|
||||
|
||||
public bool? UseRawName { get; set; }
|
||||
|
||||
public string? Person { get; set; }
|
||||
|
||||
public Guid[] PersonIds { get; set; }
|
||||
|
||||
public Guid[] ItemIds { get; set; }
|
||||
|
||||
public Guid[] ExcludeItemIds { get; set; }
|
||||
|
||||
public Guid? AdjacentTo { get; set; }
|
||||
|
||||
public string[] PersonTypes { get; set; }
|
||||
|
||||
public bool? Is3D { get; set; }
|
||||
|
||||
public bool? IsHD { get; set; }
|
||||
|
||||
public bool? IsLocked { get; set; }
|
||||
|
||||
public bool? IsPlaceHolder { get; set; }
|
||||
|
||||
public bool? HasImdbId { get; set; }
|
||||
|
||||
public bool? HasOverview { get; set; }
|
||||
|
||||
public bool? HasTmdbId { get; set; }
|
||||
|
||||
public bool? HasOfficialRating { get; set; }
|
||||
|
||||
public bool? HasTvdbId { get; set; }
|
||||
|
||||
public bool? HasThemeSong { get; set; }
|
||||
|
||||
public bool? HasThemeVideo { get; set; }
|
||||
|
||||
public bool? HasSubtitles { get; set; }
|
||||
|
||||
public bool? HasSpecialFeature { get; set; }
|
||||
|
||||
public bool? HasTrailer { get; set; }
|
||||
|
||||
public bool? HasParentalRating { get; set; }
|
||||
|
||||
public Guid[] StudioIds { get; set; }
|
||||
|
||||
public IReadOnlyList<Guid> GenreIds { get; set; }
|
||||
|
||||
public ImageType[] ImageTypes { get; set; }
|
||||
|
||||
public VideoType[] VideoTypes { get; set; }
|
||||
|
||||
public UnratedItem[] BlockUnratedItems { get; set; }
|
||||
|
||||
public int[] Years { get; set; }
|
||||
|
||||
public string[] Tags { get; set; }
|
||||
|
||||
public string[] OfficialRatings { get; set; }
|
||||
|
||||
public DateTime? MinPremiereDate { get; set; }
|
||||
|
||||
public DateTime? MaxPremiereDate { get; set; }
|
||||
|
||||
public DateTime? MinStartDate { get; set; }
|
||||
|
||||
public DateTime? MaxStartDate { get; set; }
|
||||
|
||||
public DateTime? MinEndDate { get; set; }
|
||||
|
||||
public DateTime? MaxEndDate { get; set; }
|
||||
|
||||
public bool? IsAiring { get; set; }
|
||||
|
||||
public bool? IsMovie { get; set; }
|
||||
|
||||
public bool? IsSports { get; set; }
|
||||
|
||||
public bool? IsKids { get; set; }
|
||||
|
||||
public bool? IsNews { get; set; }
|
||||
|
||||
public bool? IsSeries { get; set; }
|
||||
|
||||
public int? MinIndexNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum ParentIndexNumber and IndexNumber.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It produces this where clause:
|
||||
/// <para>(ParentIndexNumber = X and IndexNumber >= Y) or ParentIndexNumber > X.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public (int ParentIndexNumber, int IndexNumber)? MinParentAndIndexNumber { get; set; }
|
||||
|
||||
public int? AiredDuringSeason { get; set; }
|
||||
|
||||
public double? MinCriticRating { get; set; }
|
||||
|
||||
public double? MinCommunityRating { get; set; }
|
||||
|
||||
public IReadOnlyList<Guid> ChannelIds { get; set; }
|
||||
|
||||
public int? ParentIndexNumber { get; set; }
|
||||
|
||||
public int? ParentIndexNumberNotEquals { get; set; }
|
||||
|
||||
public int? IndexNumber { get; set; }
|
||||
|
||||
public ParentalRatingScore? MinParentalRating { get; set; }
|
||||
|
||||
public ParentalRatingScore? MaxParentalRating { get; set; }
|
||||
|
||||
public bool? HasDeadParentId { get; set; }
|
||||
|
||||
public bool? IsVirtualItem { get; set; }
|
||||
|
||||
public Guid ParentId { get; set; }
|
||||
|
||||
public BaseItemKind? ParentType { get; set; }
|
||||
|
||||
public Guid[] AncestorIds { get; set; }
|
||||
|
||||
public Guid[] TopParentIds { get; set; }
|
||||
|
||||
public CollectionType?[] PresetViews { get; set; }
|
||||
|
||||
public TrailerType[] TrailerTypes { get; set; }
|
||||
|
||||
public SourceType[] SourceTypes { get; set; }
|
||||
|
||||
public SeriesStatus[] SeriesStatuses { get; set; }
|
||||
|
||||
public string? ExternalSeriesId { get; set; }
|
||||
|
||||
public string? ExternalId { get; set; }
|
||||
|
||||
public Guid[] AlbumIds { get; set; }
|
||||
|
||||
public Guid[] ArtistIds { get; set; }
|
||||
|
||||
public Guid[] ExcludeArtistIds { get; set; }
|
||||
|
||||
public string? AncestorWithPresentationUniqueKey { get; set; }
|
||||
|
||||
public string? SeriesPresentationUniqueKey { get; set; }
|
||||
|
||||
public bool GroupByPresentationUniqueKey { get; set; }
|
||||
|
||||
public bool GroupBySeriesPresentationUniqueKey { get; set; }
|
||||
|
||||
public bool EnableTotalRecordCount { get; set; }
|
||||
|
||||
public bool ForceDirect { get; set; }
|
||||
|
||||
public Dictionary<string, string>? ExcludeProviderIds { get; set; }
|
||||
|
||||
public bool EnableGroupByMetadataKey { get; set; }
|
||||
|
||||
public bool? HasChapterImages { get; set; }
|
||||
|
||||
public IReadOnlyList<(ItemSortBy OrderBy, SortOrder SortOrder)> OrderBy { get; set; }
|
||||
|
||||
public DateTime? MinDateCreated { get; set; }
|
||||
|
||||
public DateTime? MinDateLastSaved { get; set; }
|
||||
|
||||
public DateTime? MinDateLastSavedForUser { get; set; }
|
||||
|
||||
public DtoOptions DtoOptions { get; set; }
|
||||
|
||||
public string? HasNoAudioTrackWithLanguage { get; set; }
|
||||
|
||||
public string? HasNoInternalSubtitleTrackWithLanguage { get; set; }
|
||||
|
||||
public string? HasNoExternalSubtitleTrackWithLanguage { get; set; }
|
||||
|
||||
public string? HasNoSubtitleTrackWithLanguage { get; set; }
|
||||
|
||||
public bool? IsDeadArtist { get; set; }
|
||||
|
||||
public bool? IsDeadStudio { get; set; }
|
||||
|
||||
public bool? IsDeadGenre { get; set; }
|
||||
|
||||
public bool? IsDeadPerson { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether album sub-folders should be returned if they exist.
|
||||
/// </summary>
|
||||
public bool? DisplayAlbumFolders { get; set; }
|
||||
|
||||
public BaseItem? Parent
|
||||
{
|
||||
set
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
ParentId = Guid.Empty;
|
||||
ParentType = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
ParentId = value.Id;
|
||||
ParentType = value.GetBaseItemKind();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<string, string>? HasAnyProviderId { get; set; }
|
||||
|
||||
public Guid[] AlbumArtistIds { get; set; }
|
||||
|
||||
public Guid[] BoxSetLibraryFolders { get; set; }
|
||||
|
||||
public Guid[] ContributingArtistIds { get; set; }
|
||||
|
||||
public bool? HasAired { get; set; }
|
||||
|
||||
public bool? HasOwnerId { get; set; }
|
||||
|
||||
public bool? Is4K { get; set; }
|
||||
|
||||
public int? MaxHeight { get; set; }
|
||||
|
||||
public int? MaxWidth { get; set; }
|
||||
|
||||
public int? MinHeight { get; set; }
|
||||
|
||||
public int? MinWidth { get; set; }
|
||||
|
||||
public string? SearchTerm { get; set; }
|
||||
|
||||
public string? SeriesTimerId { get; set; }
|
||||
|
||||
public bool SkipDeserialization { get; set; }
|
||||
|
||||
public void SetUser(User user)
|
||||
{
|
||||
var maxRating = user.MaxParentalRatingScore;
|
||||
if (maxRating.HasValue)
|
||||
{
|
||||
MaxParentalRating = new(maxRating.Value, user.MaxParentalRatingSubScore);
|
||||
}
|
||||
|
||||
var other = UnratedItem.Other.ToString();
|
||||
BlockUnratedItems = user.GetPreference(PreferenceKind.BlockUnratedItems)
|
||||
.Where(i => i != other)
|
||||
.Select(e => Enum.Parse<UnratedItem>(e, true)).ToArray();
|
||||
|
||||
ExcludeInheritedTags = user.GetPreference(PreferenceKind.BlockedTags)
|
||||
.Where(tag => !string.IsNullOrWhiteSpace(tag))
|
||||
.Select(tag => tag.RemoveDiacritics().ToLowerInvariant())
|
||||
.ToArray();
|
||||
|
||||
IncludeInheritedTags = user.GetPreference(PreferenceKind.AllowedTags)
|
||||
.Where(tag => !string.IsNullOrWhiteSpace(tag))
|
||||
.Select(tag => tag.RemoveDiacritics().ToLowerInvariant())
|
||||
.ToArray();
|
||||
|
||||
User = user;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public class InternalPeopleQuery
|
||||
{
|
||||
public InternalPeopleQuery()
|
||||
: this(Array.Empty<string>(), Array.Empty<string>())
|
||||
{
|
||||
}
|
||||
|
||||
public InternalPeopleQuery(IReadOnlyList<string> personTypes, IReadOnlyList<string> excludePersonTypes)
|
||||
{
|
||||
PersonTypes = personTypes;
|
||||
ExcludePersonTypes = excludePersonTypes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of items the query should return.
|
||||
/// </summary>
|
||||
public int Limit { get; set; }
|
||||
|
||||
public Guid ItemId { get; set; }
|
||||
|
||||
public IReadOnlyList<string> PersonTypes { get; }
|
||||
|
||||
public IReadOnlyList<string> ExcludePersonTypes { get; }
|
||||
|
||||
public int? MaxListOrder { get; set; }
|
||||
|
||||
public Guid AppearsInItemId { get; set; }
|
||||
|
||||
public string NameContains { get; set; }
|
||||
|
||||
public User User { get; set; }
|
||||
|
||||
public bool? IsFavorite { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public class ItemImageInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the path.
|
||||
/// </summary>
|
||||
/// <value>The path.</value>
|
||||
public required string Path { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type.
|
||||
/// </summary>
|
||||
/// <value>The type.</value>
|
||||
public ImageType Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the date modified.
|
||||
/// </summary>
|
||||
/// <value>The date modified.</value>
|
||||
public DateTime DateModified { get; set; }
|
||||
|
||||
public int Width { get; set; }
|
||||
|
||||
public int Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the blurhash.
|
||||
/// </summary>
|
||||
/// <value>The blurhash.</value>
|
||||
public string? BlurHash { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsLocalFile => !Path.StartsWith("http", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public class LinkedChild
|
||||
{
|
||||
public LinkedChild()
|
||||
{
|
||||
}
|
||||
|
||||
public string Path { get; set; }
|
||||
|
||||
public LinkedChildType Type { get; set; }
|
||||
|
||||
public string LibraryItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the linked item id.
|
||||
/// </summary>
|
||||
public Guid? ItemId { get; set; }
|
||||
|
||||
public static LinkedChild Create(BaseItem item)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(item);
|
||||
|
||||
var child = new LinkedChild
|
||||
{
|
||||
Path = item.Path,
|
||||
Type = LinkedChildType.Manual
|
||||
};
|
||||
|
||||
if (string.IsNullOrEmpty(child.Path))
|
||||
{
|
||||
child.LibraryItemId = item.Id.ToString("N", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MediaBrowser.Model.IO;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public class LinkedChildComparer : IEqualityComparer<LinkedChild>
|
||||
{
|
||||
private readonly IFileSystem _fileSystem;
|
||||
|
||||
public LinkedChildComparer(IFileSystem fileSystem)
|
||||
{
|
||||
_fileSystem = fileSystem;
|
||||
}
|
||||
|
||||
public bool Equals(LinkedChild x, LinkedChild y)
|
||||
{
|
||||
if (x.Type == y.Type)
|
||||
{
|
||||
return _fileSystem.AreEqual(x.Path, y.Path);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public int GetHashCode(LinkedChild obj)
|
||||
{
|
||||
return ((obj.Path ?? string.Empty) + (obj.LibraryItemId ?? string.Empty) + obj.Type).GetHashCode(StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// The linked child type.
|
||||
/// </summary>
|
||||
public enum LinkedChildType
|
||||
{
|
||||
/// <summary>
|
||||
/// Manually linked child.
|
||||
/// </summary>
|
||||
Manual = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Shortcut linked child.
|
||||
/// </summary>
|
||||
Shortcut = 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
using MediaBrowser.Model.Dto;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// Compare MediaSource of the same file by Video width <see cref="IComparer{T}" />.
|
||||
/// </summary>
|
||||
public class MediaSourceWidthComparator : IComparer<MediaSourceInfo>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int Compare(MediaSourceInfo? x, MediaSourceInfo? y)
|
||||
{
|
||||
if (x is null && y is null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (x is null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (y is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (string.Equals(x.Path, y.Path, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (x.VideoStream is null && y.VideoStream is null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (x.VideoStream is null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (y.VideoStream is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var xWidth = x.VideoStream.Width ?? 0;
|
||||
var yWidth = y.VideoStream.Width ?? 0;
|
||||
|
||||
return xWidth - yWidth;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1721, CA1819, CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using Jellyfin.Data;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Querying;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.Movies
|
||||
{
|
||||
/// <summary>
|
||||
/// Class BoxSet.
|
||||
/// </summary>
|
||||
public class BoxSet : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<BoxSetInfo>
|
||||
{
|
||||
public BoxSet()
|
||||
{
|
||||
DisplayOrder = "PremiereDate";
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
protected override bool FilterLinkedChildrenPerUser => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsInheritedParentImages => false;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPeople => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
|
||||
.Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
|
||||
.ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display order.
|
||||
/// </summary>
|
||||
/// <value>The display order.</value>
|
||||
public string DisplayOrder { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
private bool IsLegacyBoxSet
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrEmpty(Path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (LinkedChildren.Length > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return !FileSystem.ContainsSubPath(ConfigurationManager.ApplicationPaths.DataPath, Path);
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool IsPreSorted => true;
|
||||
|
||||
public Guid[] LibraryFolderIds { get; set; }
|
||||
|
||||
protected override bool GetBlockUnratedValue(User user)
|
||||
{
|
||||
return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Movie);
|
||||
}
|
||||
|
||||
public override double GetDefaultPrimaryImageAspectRatio()
|
||||
=> 2.0 / 3;
|
||||
|
||||
public override UnratedItem GetBlockUnratedType()
|
||||
{
|
||||
return UnratedItem.Movie;
|
||||
}
|
||||
|
||||
protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
|
||||
{
|
||||
if (IsLegacyBoxSet)
|
||||
{
|
||||
return base.GetNonCachedChildren(directoryService);
|
||||
}
|
||||
|
||||
return Enumerable.Empty<BaseItem>();
|
||||
}
|
||||
|
||||
protected override IReadOnlyList<BaseItem> LoadChildren()
|
||||
{
|
||||
if (IsLegacyBoxSet)
|
||||
{
|
||||
return base.LoadChildren();
|
||||
}
|
||||
|
||||
// Save a trip to the database
|
||||
return [];
|
||||
}
|
||||
|
||||
public override bool IsAuthorizedToDelete(User user, List<Folder> allCollectionFolders)
|
||||
{
|
||||
return user.HasPermission(PermissionKind.IsAdministrator) || user.HasPermission(PermissionKind.EnableCollectionManagement);
|
||||
}
|
||||
|
||||
public override bool IsSaveLocalMetadataEnabled()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
private IEnumerable<BaseItem> Sort(IEnumerable<BaseItem> items, User user)
|
||||
{
|
||||
if (!Enum.TryParse<ItemSortBy>(DisplayOrder, out var sortBy))
|
||||
{
|
||||
sortBy = ItemSortBy.PremiereDate;
|
||||
}
|
||||
|
||||
if (sortBy == ItemSortBy.Default)
|
||||
{
|
||||
return items;
|
||||
}
|
||||
|
||||
return LibraryManager.Sort(items, user, new[] { sortBy }, SortOrder.Ascending);
|
||||
}
|
||||
|
||||
public override IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
|
||||
{
|
||||
var children = base.GetChildren(user, includeLinkedChildren, query);
|
||||
return Sort(children, user).ToArray();
|
||||
}
|
||||
|
||||
public override IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, out int totalItemCount, InternalItemsQuery query = null)
|
||||
{
|
||||
var children = base.GetChildren(user, includeLinkedChildren, out totalItemCount, query);
|
||||
return Sort(children, user).ToArray();
|
||||
}
|
||||
|
||||
public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount)
|
||||
{
|
||||
var children = base.GetRecursiveChildren(user, query, out totalCount);
|
||||
return Sort(children, user).ToArray();
|
||||
}
|
||||
|
||||
public BoxSetInfo GetLookupInfo()
|
||||
{
|
||||
return GetItemLookupInfo<BoxSetInfo>();
|
||||
}
|
||||
|
||||
public override bool IsVisible(User user, bool skipAllowedTagsCheck = false)
|
||||
{
|
||||
if (IsLegacyBoxSet)
|
||||
{
|
||||
return base.IsVisible(user, skipAllowedTagsCheck);
|
||||
}
|
||||
|
||||
if (base.IsVisible(user, skipAllowedTagsCheck))
|
||||
{
|
||||
if (LinkedChildren.Length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var userLibraryFolderIds = GetLibraryFolderIds(user);
|
||||
var libraryFolderIds = LibraryFolderIds ?? GetLibraryFolderIds();
|
||||
|
||||
if (libraryFolderIds.Length == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return userLibraryFolderIds.Any(i => libraryFolderIds.Contains(i));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool IsVisibleStandalone(User user)
|
||||
{
|
||||
if (IsLegacyBoxSet)
|
||||
{
|
||||
return base.IsVisibleStandalone(user);
|
||||
}
|
||||
|
||||
return IsVisible(user);
|
||||
}
|
||||
|
||||
private Guid[] GetLibraryFolderIds(User user)
|
||||
{
|
||||
return LibraryManager.GetUserRootFolder().GetChildren(user, true)
|
||||
.Select(i => i.Id)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public Guid[] GetLibraryFolderIds()
|
||||
{
|
||||
var expandedFolders = new List<Guid>();
|
||||
|
||||
return FlattenItems(this, expandedFolders)
|
||||
.SelectMany(LibraryManager.GetCollectionFolders)
|
||||
.Select(i => i.Id)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private IEnumerable<BaseItem> FlattenItems(IEnumerable<BaseItem> items, List<Guid> expandedFolders)
|
||||
{
|
||||
return items
|
||||
.SelectMany(i => FlattenItems(i, expandedFolders));
|
||||
}
|
||||
|
||||
private IEnumerable<BaseItem> FlattenItems(BaseItem item, List<Guid> expandedFolders)
|
||||
{
|
||||
if (item is BoxSet boxset)
|
||||
{
|
||||
if (!expandedFolders.Contains(item.Id))
|
||||
{
|
||||
expandedFolders.Add(item.Id);
|
||||
|
||||
return FlattenItems(boxset.GetLinkedChildren(), expandedFolders);
|
||||
}
|
||||
|
||||
return Array.Empty<BaseItem>();
|
||||
}
|
||||
|
||||
return new[] { item };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.Movies
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Movie.
|
||||
/// </summary>
|
||||
public class Movie : Video, IHasSpecialFeatures, IHasTrailers, IHasLookupInfo<MovieInfo>, ISupportsBoxSetGrouping
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public IReadOnlyList<Guid> SpecialFeatureIds => GetExtras()
|
||||
.Where(extra => extra.ExtraType is not null && extra is Video)
|
||||
.Select(extra => extra.Id)
|
||||
.ToArray();
|
||||
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
|
||||
.Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
|
||||
.ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the TMDb collection.
|
||||
/// </summary>
|
||||
/// <value>The name of the TMDb collection.</value>
|
||||
public string TmdbCollectionName { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string CollectionName
|
||||
{
|
||||
get => TmdbCollectionName;
|
||||
set => TmdbCollectionName = value;
|
||||
}
|
||||
|
||||
public override double GetDefaultPrimaryImageAspectRatio()
|
||||
{
|
||||
// hack for tv plugins
|
||||
if (SourceType == SourceType.Channel)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 2.0 / 3;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override UnratedItem GetBlockUnratedType()
|
||||
{
|
||||
return UnratedItem.Movie;
|
||||
}
|
||||
|
||||
public MovieInfo GetLookupInfo()
|
||||
{
|
||||
var info = GetItemLookupInfo<MovieInfo>();
|
||||
|
||||
if (!IsInMixedFolder)
|
||||
{
|
||||
var name = System.IO.Path.GetFileName(ContainingFolderPath);
|
||||
|
||||
if (VideoType == VideoType.VideoFile || VideoType == VideoType.Iso)
|
||||
{
|
||||
if (string.Equals(name, System.IO.Path.GetFileName(Path), StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// if the folder has the file extension, strip it
|
||||
name = System.IO.Path.GetFileNameWithoutExtension(name);
|
||||
}
|
||||
}
|
||||
|
||||
info.Name = name;
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
|
||||
{
|
||||
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
|
||||
|
||||
if (!ProductionYear.HasValue)
|
||||
{
|
||||
var info = LibraryManager.ParseName(Name);
|
||||
|
||||
var yearInName = info.Year;
|
||||
|
||||
if (yearInName.HasValue)
|
||||
{
|
||||
ProductionYear = yearInName;
|
||||
hasChanges = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try to get the year from the folder name
|
||||
if (!IsInMixedFolder)
|
||||
{
|
||||
info = LibraryManager.ParseName(System.IO.Path.GetFileName(ContainingFolderPath));
|
||||
|
||||
yearInName = info.Year;
|
||||
|
||||
if (yearInName.HasValue)
|
||||
{
|
||||
ProductionYear = yearInName;
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanges;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Entities.Audio;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public class MusicVideo : Video, IHasArtist, IHasMusicGenres, IHasLookupInfo<MusicVideoInfo>
|
||||
{
|
||||
public MusicVideo()
|
||||
{
|
||||
Artists = Array.Empty<string>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public IReadOnlyList<string> Artists { get; set; }
|
||||
|
||||
public override UnratedItem GetBlockUnratedType()
|
||||
{
|
||||
return UnratedItem.Music;
|
||||
}
|
||||
|
||||
public MusicVideoInfo GetLookupInfo()
|
||||
{
|
||||
var info = GetItemLookupInfo<MusicVideoInfo>();
|
||||
|
||||
info.Artists = Artists;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
|
||||
{
|
||||
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
|
||||
|
||||
if (!ProductionYear.HasValue)
|
||||
{
|
||||
var info = LibraryManager.ParseName(Name);
|
||||
|
||||
var yearInName = info.Year;
|
||||
|
||||
if (yearInName.HasValue)
|
||||
{
|
||||
ProductionYear = yearInName;
|
||||
hasChanges = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try to get the year from the folder name
|
||||
if (!IsInMixedFolder)
|
||||
{
|
||||
info = LibraryManager.ParseName(System.IO.Path.GetFileName(ContainingFolderPath));
|
||||
|
||||
yearInName = info.Year;
|
||||
|
||||
if (yearInName.HasValue)
|
||||
{
|
||||
ProductionYear = yearInName;
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanges;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public static class PeopleHelper
|
||||
{
|
||||
public static void AddPerson(ICollection<PersonInfo> people, PersonInfo person)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(person);
|
||||
ArgumentException.ThrowIfNullOrEmpty(person.Name);
|
||||
|
||||
person.Name = person.Name.Trim();
|
||||
|
||||
// Normalize
|
||||
if (string.Equals(person.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
person.Type = PersonKind.GuestStar;
|
||||
}
|
||||
else if (string.Equals(person.Role, PersonType.Director, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
person.Type = PersonKind.Director;
|
||||
}
|
||||
else if (string.Equals(person.Role, PersonType.Producer, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
person.Type = PersonKind.Producer;
|
||||
}
|
||||
else if (string.Equals(person.Role, PersonType.Writer, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
person.Type = PersonKind.Writer;
|
||||
}
|
||||
|
||||
// If the type is GuestStar and there's already an Actor entry, then update it to avoid dupes
|
||||
if (person.Type == PersonKind.GuestStar)
|
||||
{
|
||||
var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && p.Type == PersonKind.Actor);
|
||||
|
||||
if (existing is not null)
|
||||
{
|
||||
existing.Type = PersonKind.GuestStar;
|
||||
MergeExisting(existing, person);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (person.Type == PersonKind.Actor)
|
||||
{
|
||||
// If the actor already exists without a role and we have one, fill it in
|
||||
var existing = people.FirstOrDefault(p => p.Name.Equals(person.Name, StringComparison.OrdinalIgnoreCase) && (p.Type == PersonKind.Actor || p.Type == PersonKind.GuestStar));
|
||||
if (existing is null)
|
||||
{
|
||||
// Wasn't there - add it
|
||||
people.Add(person);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Was there, if no role and we have one - fill it in
|
||||
if (string.IsNullOrEmpty(existing.Role) && !string.IsNullOrEmpty(person.Role))
|
||||
{
|
||||
existing.Role = person.Role;
|
||||
}
|
||||
|
||||
MergeExisting(existing, person);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var existing = people.FirstOrDefault(p =>
|
||||
string.Equals(p.Name, person.Name, StringComparison.OrdinalIgnoreCase)
|
||||
&& p.Type == person.Type);
|
||||
|
||||
// Check for dupes based on the combination of Name and Type
|
||||
if (existing is null)
|
||||
{
|
||||
people.Add(person);
|
||||
}
|
||||
else
|
||||
{
|
||||
MergeExisting(existing, person);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void MergeExisting(PersonInfo existing, PersonInfo person)
|
||||
{
|
||||
existing.SortOrder = person.SortOrder ?? existing.SortOrder;
|
||||
existing.ImageUrl = person.ImageUrl ?? existing.ImageUrl;
|
||||
|
||||
foreach (var id in person.ProviderIds)
|
||||
{
|
||||
existing.SetProviderId(id.Key, id.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// This is the full Person object that can be retrieved with all of it's data.
|
||||
/// </summary>
|
||||
[Common.RequiresSourceSerialisation]
|
||||
public class Person : BaseItem, IItemByName, IHasLookupInfo<PersonLookupInfo>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the folder containing the item.
|
||||
/// If the item is a folder, it returns the folder itself.
|
||||
/// </summary>
|
||||
/// <value>The containing folder path.</value>
|
||||
[JsonIgnore]
|
||||
public override string ContainingFolderPath => Path;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether to enable alpha numeric sorting.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public override bool EnableAlphaNumericSorting => false;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPeople => false;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsAncestors => false;
|
||||
|
||||
public override List<string> GetUserDataKeys()
|
||||
{
|
||||
var list = base.GetUserDataKeys();
|
||||
|
||||
list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
|
||||
return list;
|
||||
}
|
||||
|
||||
public override string CreatePresentationUniqueKey()
|
||||
{
|
||||
return GetUserDataKeys()[0];
|
||||
}
|
||||
|
||||
public PersonLookupInfo GetLookupInfo()
|
||||
{
|
||||
return GetItemLookupInfo<PersonLookupInfo>();
|
||||
}
|
||||
|
||||
public override double GetDefaultPrimaryImageAspectRatio()
|
||||
{
|
||||
double value = 2;
|
||||
value /= 3;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query)
|
||||
{
|
||||
query.PersonIds = new[] { Id };
|
||||
|
||||
return LibraryManager.GetItemList(query);
|
||||
}
|
||||
|
||||
public override bool CanDelete()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool IsSaveLocalMetadataEnabled()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string GetPath(string name)
|
||||
{
|
||||
return GetPath(name, true);
|
||||
}
|
||||
|
||||
public static string GetPath(string name, bool normalizeName)
|
||||
{
|
||||
// Trim the period at the end because windows will have a hard time with that
|
||||
var validFilename = normalizeName ?
|
||||
FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
|
||||
name;
|
||||
|
||||
string subFolderPrefix = null;
|
||||
|
||||
foreach (char c in validFilename)
|
||||
{
|
||||
if (char.IsLetterOrDigit(c))
|
||||
{
|
||||
subFolderPrefix = c.ToString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var path = ConfigurationManager.ApplicationPaths.PeoplePath;
|
||||
|
||||
return string.IsNullOrEmpty(subFolderPrefix) ?
|
||||
System.IO.Path.Combine(path, validFilename) :
|
||||
System.IO.Path.Combine(path, subFolderPrefix, validFilename);
|
||||
}
|
||||
|
||||
private string GetRebasedPath()
|
||||
{
|
||||
return GetPath(System.IO.Path.GetFileName(Path), false);
|
||||
}
|
||||
|
||||
public override bool RequiresRefresh()
|
||||
{
|
||||
var newPath = GetRebasedPath();
|
||||
if (!string.Equals(Path, newPath, StringComparison.Ordinal))
|
||||
{
|
||||
Logger.LogDebug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.RequiresRefresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is called before any metadata refresh and returns true or false indicating if changes were made.
|
||||
/// </summary>
|
||||
/// <param name="replaceAllMetadata"><c>true</c> to replace all metadata, <c>false</c> to not.</param>
|
||||
/// <returns><c>true</c> if changes were made, <c>false</c> if not.</returns>
|
||||
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
|
||||
{
|
||||
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
|
||||
|
||||
var newPath = GetRebasedPath();
|
||||
if (!string.Equals(Path, newPath, StringComparison.Ordinal))
|
||||
{
|
||||
Path = newPath;
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
return hasChanges;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA2227, CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Model.Entities;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// This is a small Person stub that is attached to BaseItems.
|
||||
/// </summary>
|
||||
public sealed class PersonInfo : IHasProviderIds
|
||||
{
|
||||
public PersonInfo()
|
||||
{
|
||||
ProviderIds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
Id = Guid.NewGuid();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or Sets the PersonId.
|
||||
/// </summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
public Guid ItemId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the role.
|
||||
/// </summary>
|
||||
/// <value>The role.</value>
|
||||
public string Role { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type.
|
||||
/// </summary>
|
||||
/// <value>The type.</value>
|
||||
public PersonKind Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ascending sort order.
|
||||
/// </summary>
|
||||
/// <value>The sort order.</value>
|
||||
public int? SortOrder { get; set; }
|
||||
|
||||
public string ImageUrl { get; set; }
|
||||
|
||||
public Dictionary<string, string> ProviderIds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="string" /> that represents this instance.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="string" /> that represents this instance.</returns>
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
|
||||
public bool IsType(PersonKind type) => Type == type || string.Equals(type.ToString(), Role, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Model.Drawing;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public class Photo : BaseItem
|
||||
{
|
||||
[JsonIgnore]
|
||||
public override bool SupportsLocalMetadata => false;
|
||||
|
||||
[JsonIgnore]
|
||||
public override MediaType MediaType => MediaType.Photo;
|
||||
|
||||
[JsonIgnore]
|
||||
public override Folder LatestItemsIndexContainer => AlbumEntity;
|
||||
|
||||
[JsonIgnore]
|
||||
public PhotoAlbum AlbumEntity
|
||||
{
|
||||
get
|
||||
{
|
||||
var parents = GetParents();
|
||||
foreach (var parent in parents)
|
||||
{
|
||||
if (parent is PhotoAlbum photoAlbum)
|
||||
{
|
||||
return photoAlbum;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public string CameraMake { get; set; }
|
||||
|
||||
public string CameraModel { get; set; }
|
||||
|
||||
public string Software { get; set; }
|
||||
|
||||
public double? ExposureTime { get; set; }
|
||||
|
||||
public double? FocalLength { get; set; }
|
||||
|
||||
public ImageOrientation? Orientation { get; set; }
|
||||
|
||||
public double? Aperture { get; set; }
|
||||
|
||||
public double? ShutterSpeed { get; set; }
|
||||
|
||||
public double? Latitude { get; set; }
|
||||
|
||||
public double? Longitude { get; set; }
|
||||
|
||||
public double? Altitude { get; set; }
|
||||
|
||||
public int? IsoSpeedRating { get; set; }
|
||||
|
||||
public override bool CanDownload()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public override double GetDefaultPrimaryImageAspectRatio()
|
||||
{
|
||||
// REVIEW: @bond
|
||||
if (Width != 0 && Height != 0)
|
||||
{
|
||||
double width = Width;
|
||||
double height = Height;
|
||||
|
||||
if (Orientation.HasValue)
|
||||
{
|
||||
switch (Orientation.Value)
|
||||
{
|
||||
case ImageOrientation.LeftBottom:
|
||||
case ImageOrientation.LeftTop:
|
||||
case ImageOrientation.RightBottom:
|
||||
case ImageOrientation.RightTop:
|
||||
var temp = height;
|
||||
height = width;
|
||||
width = temp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return width / height;
|
||||
}
|
||||
|
||||
return base.GetDefaultPrimaryImageAspectRatio();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
[Common.RequiresSourceSerialisation]
|
||||
public class PhotoAlbum : Folder
|
||||
{
|
||||
[JsonIgnore]
|
||||
public override bool AlwaysScanInternalMetadataPath => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPlayedStatus => false;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsInheritedParentImages => false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public enum SourceType
|
||||
{
|
||||
Library = 0,
|
||||
Channel = 1,
|
||||
LiveTV = 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
using Jellyfin.Extensions;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Studio.
|
||||
/// </summary>
|
||||
[Common.RequiresSourceSerialisation]
|
||||
public class Studio : BaseItem, IItemByName
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the folder containing the item.
|
||||
/// If the item is a folder, it returns the folder itself.
|
||||
/// </summary>
|
||||
/// <value>The containing folder path.</value>
|
||||
[JsonIgnore]
|
||||
public override string ContainingFolderPath => Path;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool IsDisplayedAsFolder => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsAncestors => false;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPeople => false;
|
||||
|
||||
public override List<string> GetUserDataKeys()
|
||||
{
|
||||
var list = base.GetUserDataKeys();
|
||||
|
||||
list.Insert(0, GetType().Name + "-" + (Name ?? string.Empty).RemoveDiacritics());
|
||||
return list;
|
||||
}
|
||||
|
||||
public override string CreatePresentationUniqueKey()
|
||||
{
|
||||
return GetUserDataKeys()[0];
|
||||
}
|
||||
|
||||
public override double GetDefaultPrimaryImageAspectRatio()
|
||||
{
|
||||
double value = 16;
|
||||
value /= 9;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public override bool CanDelete()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool IsSaveLocalMetadataEnabled()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query)
|
||||
{
|
||||
query.StudioIds = new[] { Id };
|
||||
|
||||
return LibraryManager.GetItemList(query);
|
||||
}
|
||||
|
||||
public static string GetPath(string name)
|
||||
{
|
||||
return GetPath(name, true);
|
||||
}
|
||||
|
||||
public static string GetPath(string name, bool normalizeName)
|
||||
{
|
||||
// Trim the period at the end because windows will have a hard time with that
|
||||
var validName = normalizeName ?
|
||||
FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
|
||||
name;
|
||||
|
||||
return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.StudioPath, validName);
|
||||
}
|
||||
|
||||
private string GetRebasedPath()
|
||||
{
|
||||
return GetPath(System.IO.Path.GetFileName(Path), false);
|
||||
}
|
||||
|
||||
public override bool RequiresRefresh()
|
||||
{
|
||||
var newPath = GetRebasedPath();
|
||||
if (!string.Equals(Path, newPath, StringComparison.Ordinal))
|
||||
{
|
||||
Logger.LogDebug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.RequiresRefresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is called before any metadata refresh and returns true or false indicating if changes were made.
|
||||
/// </summary>
|
||||
/// <param name="replaceAllMetadata"><c>true</c> to replace all metadata, <c>false</c> to not.</param>
|
||||
/// <returns><c>true</c> if changes were made, <c>false</c> if not.</returns>
|
||||
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
|
||||
{
|
||||
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
|
||||
|
||||
var newPath = GetRebasedPath();
|
||||
if (!string.Equals(Path, newPath, StringComparison.Ordinal))
|
||||
{
|
||||
Path = newPath;
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
return hasChanges;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.MediaEncoding;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Dlna;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.TV
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Episode.
|
||||
/// </summary>
|
||||
public class Episode : Video, IHasTrailers, IHasLookupInfo<EpisodeInfo>, IHasSeries
|
||||
{
|
||||
public static IMediaEncoder MediaEncoder { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
|
||||
.Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
|
||||
.ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the season in which it aired.
|
||||
/// </summary>
|
||||
/// <value>The aired season.</value>
|
||||
public int? AirsBeforeSeasonNumber { get; set; }
|
||||
|
||||
public int? AirsAfterSeasonNumber { get; set; }
|
||||
|
||||
public int? AirsBeforeEpisodeNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ending episode number for double episodes.
|
||||
/// </summary>
|
||||
/// <value>The index number.</value>
|
||||
public int? IndexNumberEnd { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
protected override bool SupportsOwnedItems => IsStacked || MediaSourceCount > 1;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsInheritedParentImages => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPeople => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public int? AiredSeasonNumber => AirsAfterSeasonNumber ?? AirsBeforeSeasonNumber ?? ParentIndexNumber;
|
||||
|
||||
[JsonIgnore]
|
||||
public override Folder LatestItemsIndexContainer => Series;
|
||||
|
||||
[JsonIgnore]
|
||||
public override Guid DisplayParentId => SeasonId;
|
||||
|
||||
[JsonIgnore]
|
||||
protected override bool EnableDefaultVideoUserDataKeys => false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Episode's Series Instance.
|
||||
/// </summary>
|
||||
/// <value>The series.</value>
|
||||
[JsonIgnore]
|
||||
public Series Series
|
||||
{
|
||||
get
|
||||
{
|
||||
var seriesId = SeriesId;
|
||||
if (seriesId.IsEmpty())
|
||||
{
|
||||
seriesId = FindSeriesId();
|
||||
}
|
||||
|
||||
return seriesId.IsEmpty() ? null : (LibraryManager.GetItemById(seriesId) as Series);
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public Season Season
|
||||
{
|
||||
get
|
||||
{
|
||||
var seasonId = SeasonId;
|
||||
if (seasonId.IsEmpty())
|
||||
{
|
||||
seasonId = FindSeasonId();
|
||||
}
|
||||
|
||||
return seasonId.IsEmpty() ? null : (LibraryManager.GetItemById(seasonId) as Season);
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsInSeasonFolder => FindParent<Season>() is not null;
|
||||
|
||||
[JsonIgnore]
|
||||
public string SeriesPresentationUniqueKey { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string SeriesName { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string SeasonName { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsRemoteImageDownloading
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsMissingEpisode)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsMissingEpisode => LocationType == LocationType.Virtual;
|
||||
|
||||
[JsonIgnore]
|
||||
public Guid SeasonId { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Guid SeriesId { get; set; }
|
||||
|
||||
public string FindSeriesSortName()
|
||||
{
|
||||
var series = Series;
|
||||
return series is null ? SeriesName : series.SortName;
|
||||
}
|
||||
|
||||
public override double GetDefaultPrimaryImageAspectRatio()
|
||||
{
|
||||
// hack for tv plugins
|
||||
if (SourceType == SourceType.Channel)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 16.0 / 9;
|
||||
}
|
||||
|
||||
public override List<string> GetUserDataKeys()
|
||||
{
|
||||
var list = base.GetUserDataKeys();
|
||||
|
||||
var series = Series;
|
||||
if (series is not null && ParentIndexNumber.HasValue && IndexNumber.HasValue)
|
||||
{
|
||||
var seriesUserDataKeys = series.GetUserDataKeys();
|
||||
var take = seriesUserDataKeys.Count;
|
||||
if (seriesUserDataKeys.Count > 1)
|
||||
{
|
||||
take--;
|
||||
}
|
||||
|
||||
var newList = seriesUserDataKeys.GetRange(0, take);
|
||||
var suffix = ParentIndexNumber.Value.ToString("000", CultureInfo.InvariantCulture) + IndexNumber.Value.ToString("000", CultureInfo.InvariantCulture);
|
||||
for (int i = 0; i < take; i++)
|
||||
{
|
||||
newList[i] = newList[i] + suffix;
|
||||
}
|
||||
|
||||
newList.AddRange(list);
|
||||
list = newList;
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public string FindSeriesPresentationUniqueKey()
|
||||
=> Series?.PresentationUniqueKey;
|
||||
|
||||
public string FindSeasonName()
|
||||
{
|
||||
var season = Season;
|
||||
|
||||
if (season is null)
|
||||
{
|
||||
if (ParentIndexNumber.HasValue)
|
||||
{
|
||||
return "Season " + ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return "Season Unknown";
|
||||
}
|
||||
|
||||
return season.Name;
|
||||
}
|
||||
|
||||
public string FindSeriesName()
|
||||
{
|
||||
var series = Series;
|
||||
return series is null ? SeriesName : series.Name;
|
||||
}
|
||||
|
||||
public Guid FindSeasonId()
|
||||
{
|
||||
var season = FindParent<Season>();
|
||||
|
||||
// Episodes directly in series folder
|
||||
if (season is null)
|
||||
{
|
||||
var series = Series;
|
||||
|
||||
if (series is not null && ParentIndexNumber.HasValue)
|
||||
{
|
||||
var findNumber = ParentIndexNumber.Value;
|
||||
|
||||
season = series.Children
|
||||
.OfType<Season>()
|
||||
.FirstOrDefault(i => i.IndexNumber.HasValue && i.IndexNumber.Value == findNumber);
|
||||
}
|
||||
}
|
||||
|
||||
return season is null ? Guid.Empty : season.Id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the name of the sort.
|
||||
/// </summary>
|
||||
/// <returns>System.String.</returns>
|
||||
protected override string CreateSortName()
|
||||
{
|
||||
return (ParentIndexNumber is not null ? ParentIndexNumber.Value.ToString("000 - ", CultureInfo.InvariantCulture) : string.Empty)
|
||||
+ (IndexNumber is not null ? IndexNumber.Value.ToString("0000 - ", CultureInfo.InvariantCulture) : string.Empty) + Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether [contains episode number] [the specified number].
|
||||
/// </summary>
|
||||
/// <param name="number">The number.</param>
|
||||
/// <returns><c>true</c> if [contains episode number] [the specified number]; otherwise, <c>false</c>.</returns>
|
||||
public bool ContainsEpisodeNumber(int number)
|
||||
{
|
||||
if (IndexNumber.HasValue)
|
||||
{
|
||||
if (IndexNumberEnd.HasValue)
|
||||
{
|
||||
return number >= IndexNumber.Value && number <= IndexNumberEnd.Value;
|
||||
}
|
||||
|
||||
return IndexNumber.Value == number;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public Guid FindSeriesId()
|
||||
{
|
||||
var series = FindParent<Series>();
|
||||
return series is null ? Guid.Empty : series.Id;
|
||||
}
|
||||
|
||||
public override IEnumerable<Guid> GetAncestorIds()
|
||||
{
|
||||
var list = base.GetAncestorIds().ToList();
|
||||
|
||||
var seasonId = SeasonId;
|
||||
|
||||
if (!seasonId.IsEmpty() && !list.Contains(seasonId))
|
||||
{
|
||||
list.Add(seasonId);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public override IEnumerable<FileSystemMetadata> GetDeletePaths()
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new FileSystemMetadata
|
||||
{
|
||||
FullName = Path,
|
||||
IsDirectory = IsFolder
|
||||
}
|
||||
}.Concat(GetLocalMetadataFilesToDelete());
|
||||
}
|
||||
|
||||
public override UnratedItem GetBlockUnratedType()
|
||||
{
|
||||
return UnratedItem.Series;
|
||||
}
|
||||
|
||||
public EpisodeInfo GetLookupInfo()
|
||||
{
|
||||
var id = GetItemLookupInfo<EpisodeInfo>();
|
||||
|
||||
var series = Series;
|
||||
|
||||
if (series is not null)
|
||||
{
|
||||
id.SeriesProviderIds = series.ProviderIds;
|
||||
id.SeriesDisplayOrder = series.DisplayOrder;
|
||||
}
|
||||
|
||||
if (Season is not null)
|
||||
{
|
||||
id.SeasonProviderIds = Season.ProviderIds;
|
||||
}
|
||||
|
||||
id.IsMissingEpisode = IsMissingEpisode;
|
||||
id.IndexNumberEnd = IndexNumberEnd;
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
|
||||
{
|
||||
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
|
||||
|
||||
if (!IsLocked)
|
||||
{
|
||||
if (SourceType == SourceType.Library || SourceType == SourceType.LiveTV)
|
||||
{
|
||||
var libraryOptions = LibraryManager.GetLibraryOptions(this);
|
||||
if (libraryOptions.EnableEmbeddedEpisodeInfos && string.Equals(Container, "mp4", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
var mediaInfo = MediaEncoder.GetMediaInfo(
|
||||
new MediaInfoRequest
|
||||
{
|
||||
MediaSource = GetMediaSources(false)[0],
|
||||
MediaType = DlnaProfileType.Video
|
||||
},
|
||||
CancellationToken.None).GetAwaiter().GetResult();
|
||||
if (mediaInfo.ParentIndexNumber > 0)
|
||||
{
|
||||
ParentIndexNumber = mediaInfo.ParentIndexNumber;
|
||||
}
|
||||
|
||||
if (mediaInfo.IndexNumber > 0)
|
||||
{
|
||||
IndexNumber = mediaInfo.IndexNumber;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(mediaInfo.ShowName))
|
||||
{
|
||||
SeriesName = mediaInfo.ShowName;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error reading the episode information with ffprobe. Episode: {EpisodeInfo}", Path);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (LibraryManager.FillMissingEpisodeNumbersFromPath(this, replaceAllMetadata))
|
||||
{
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Error in FillMissingEpisodeNumbersFromPath. Episode: {Episode}", Path ?? Name ?? Id.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanges;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Common;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Querying;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.TV
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Season.
|
||||
/// </summary>
|
||||
[RequiresSourceSerialisation]
|
||||
public class Season : Folder, IHasSeries, IHasLookupInfo<SeasonInfo>
|
||||
{
|
||||
[JsonIgnore]
|
||||
public override bool SupportsAddingToPlaylist => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool IsPreSorted => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsDateLastMediaAdded => false;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPeople => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsInheritedParentImages => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override Guid DisplayParentId => SeriesId;
|
||||
|
||||
/// <summary>
|
||||
/// Gets this Episode's Series Instance.
|
||||
/// </summary>
|
||||
/// <value>The series.</value>
|
||||
[JsonIgnore]
|
||||
public Series Series
|
||||
{
|
||||
get
|
||||
{
|
||||
var seriesId = SeriesId;
|
||||
if (seriesId.IsEmpty())
|
||||
{
|
||||
seriesId = FindSeriesId();
|
||||
}
|
||||
|
||||
return seriesId.IsEmpty() ? null : (LibraryManager.GetItemById(seriesId) as Series);
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string SeriesPath
|
||||
{
|
||||
get
|
||||
{
|
||||
var series = Series;
|
||||
|
||||
if (series is not null)
|
||||
{
|
||||
return series.Path;
|
||||
}
|
||||
|
||||
return System.IO.Path.GetDirectoryName(Path);
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string SeriesPresentationUniqueKey { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string SeriesName { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public Guid SeriesId { get; set; }
|
||||
|
||||
public override double GetDefaultPrimaryImageAspectRatio()
|
||||
{
|
||||
double value = 2;
|
||||
value /= 3;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public string FindSeriesSortName()
|
||||
{
|
||||
var series = Series;
|
||||
return series is null ? SeriesName : series.SortName;
|
||||
}
|
||||
|
||||
public override List<string> GetUserDataKeys()
|
||||
{
|
||||
var list = base.GetUserDataKeys();
|
||||
|
||||
var series = Series;
|
||||
if (series is not null)
|
||||
{
|
||||
var newList = series.GetUserDataKeys();
|
||||
var suffix = (IndexNumber ?? 0).ToString("000", CultureInfo.InvariantCulture);
|
||||
for (int i = 0; i < newList.Count; i++)
|
||||
{
|
||||
newList[i] = newList[i] + suffix;
|
||||
}
|
||||
|
||||
newList.AddRange(list);
|
||||
list = newList;
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public override int GetChildCount(User user)
|
||||
{
|
||||
var result = GetChildren(user, true, null).Count;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override string CreatePresentationUniqueKey()
|
||||
{
|
||||
if (IndexNumber.HasValue)
|
||||
{
|
||||
var series = Series;
|
||||
if (series is not null)
|
||||
{
|
||||
return series.PresentationUniqueKey + "-" + IndexNumber.Value.ToString("000", CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
|
||||
return base.CreatePresentationUniqueKey();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the name of the sort.
|
||||
/// </summary>
|
||||
/// <returns>System.String.</returns>
|
||||
protected override string CreateSortName()
|
||||
{
|
||||
return IndexNumber is not null ? IndexNumber.Value.ToString("0000", CultureInfo.InvariantCulture) : Name;
|
||||
}
|
||||
|
||||
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
|
||||
{
|
||||
if (SourceType == SourceType.Channel)
|
||||
{
|
||||
try
|
||||
{
|
||||
query.Parent = this;
|
||||
query.ChannelIds = new[] { ChannelId };
|
||||
return ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Already logged at lower levels
|
||||
return new QueryResult<BaseItem>();
|
||||
}
|
||||
}
|
||||
|
||||
if (query.User is null)
|
||||
{
|
||||
return base.GetItemsInternal(query);
|
||||
}
|
||||
|
||||
var user = query.User;
|
||||
|
||||
Func<BaseItem, bool> filter = i => UserViewBuilder.Filter(i, user, query, UserDataManager, LibraryManager);
|
||||
|
||||
var items = GetEpisodes(user, query.DtoOptions, true).Where(filter);
|
||||
|
||||
return PostFilterAndSort(items, query);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the episodes.
|
||||
/// </summary>
|
||||
/// <param name="user">The user.</param>
|
||||
/// <param name="options">The options to use.</param>
|
||||
/// <param name="shouldIncludeMissingEpisodes">If missing episodes should be included.</param>
|
||||
/// <returns>Set of episodes.</returns>
|
||||
public List<BaseItem> GetEpisodes(User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
||||
{
|
||||
return GetEpisodes(Series, user, options, shouldIncludeMissingEpisodes);
|
||||
}
|
||||
|
||||
public List<BaseItem> GetEpisodes(Series series, User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
||||
{
|
||||
return GetEpisodes(series, user, null, options, shouldIncludeMissingEpisodes);
|
||||
}
|
||||
|
||||
public List<BaseItem> GetEpisodes(Series series, User user, IEnumerable<Episode> allSeriesEpisodes, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
||||
{
|
||||
return series.GetSeasonEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes);
|
||||
}
|
||||
|
||||
public List<BaseItem> GetEpisodes()
|
||||
{
|
||||
return Series.GetSeasonEpisodes(this, null, null, new DtoOptions(true), true);
|
||||
}
|
||||
|
||||
public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
|
||||
{
|
||||
return GetEpisodes(user, new DtoOptions(true), true);
|
||||
}
|
||||
|
||||
protected override bool GetBlockUnratedValue(User user)
|
||||
{
|
||||
// Don't block. Let either the entire series rating or episode rating determine it
|
||||
return false;
|
||||
}
|
||||
|
||||
public override UnratedItem GetBlockUnratedType()
|
||||
{
|
||||
return UnratedItem.Series;
|
||||
}
|
||||
|
||||
public string FindSeriesPresentationUniqueKey()
|
||||
{
|
||||
var series = Series;
|
||||
return series is null ? null : series.PresentationUniqueKey;
|
||||
}
|
||||
|
||||
public string FindSeriesName()
|
||||
{
|
||||
var series = Series;
|
||||
return series is null ? SeriesName : series.Name;
|
||||
}
|
||||
|
||||
public Guid FindSeriesId()
|
||||
{
|
||||
var series = FindParent<Series>();
|
||||
return series?.Id ?? Guid.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the lookup information.
|
||||
/// </summary>
|
||||
/// <returns>SeasonInfo.</returns>
|
||||
public SeasonInfo GetLookupInfo()
|
||||
{
|
||||
var id = GetItemLookupInfo<SeasonInfo>();
|
||||
|
||||
var series = Series;
|
||||
|
||||
if (series is not null)
|
||||
{
|
||||
id.SeriesProviderIds = series.ProviderIds;
|
||||
id.SeriesDisplayOrder = series.DisplayOrder;
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is called before any metadata refresh and returns true or false indicating if changes were made.
|
||||
/// </summary>
|
||||
/// <param name="replaceAllMetadata"><c>true</c> to replace metadata, <c>false</c> to not.</param>
|
||||
/// <returns><c>true</c> if XXXX, <c>false</c> otherwise.</returns>
|
||||
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
|
||||
{
|
||||
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
|
||||
|
||||
if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path))
|
||||
{
|
||||
IndexNumber ??= LibraryManager.GetSeasonNumberFromPath(Path, ParentId);
|
||||
|
||||
// If a change was made record it
|
||||
if (IndexNumber.HasValue)
|
||||
{
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanges;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,528 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Database.Implementations.Enums;
|
||||
using MediaBrowser.Controller.Dto;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Querying;
|
||||
using MetadataProvider = MediaBrowser.Model.Entities.MetadataProvider;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities.TV
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Series.
|
||||
/// </summary>
|
||||
public class Series : Folder, IHasTrailers, IHasDisplayOrder, IHasLookupInfo<SeriesInfo>, IMetadataContainer, ISupportsBoxSetGrouping
|
||||
{
|
||||
public Series()
|
||||
{
|
||||
AirDays = Array.Empty<DayOfWeek>();
|
||||
}
|
||||
|
||||
public DayOfWeek[] AirDays { get; set; }
|
||||
|
||||
public string AirTime { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsAddingToPlaylist => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool IsPreSorted => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsDateLastMediaAdded => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsInheritedParentImages => false;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPeople => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
|
||||
.Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
|
||||
.ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display order.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Valid options are airdate, dvd or absolute.
|
||||
/// </remarks>
|
||||
public string DisplayOrder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the status.
|
||||
/// </summary>
|
||||
/// <value>The status.</value>
|
||||
public SeriesStatus? Status { get; set; }
|
||||
|
||||
public override double GetDefaultPrimaryImageAspectRatio()
|
||||
{
|
||||
double value = 2;
|
||||
value /= 3;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public override string CreatePresentationUniqueKey()
|
||||
{
|
||||
if (LibraryManager.GetLibraryOptions(this).EnableAutomaticSeriesGrouping)
|
||||
{
|
||||
var userdatakeys = GetUserDataKeys();
|
||||
|
||||
if (userdatakeys.Count > 1)
|
||||
{
|
||||
return AddLibrariesToPresentationUniqueKey(userdatakeys[0]);
|
||||
}
|
||||
}
|
||||
|
||||
return base.CreatePresentationUniqueKey();
|
||||
}
|
||||
|
||||
private string AddLibrariesToPresentationUniqueKey(string key)
|
||||
{
|
||||
var lang = GetPreferredMetadataLanguage();
|
||||
if (!string.IsNullOrEmpty(lang))
|
||||
{
|
||||
key += "-" + lang;
|
||||
}
|
||||
|
||||
var folders = LibraryManager.GetCollectionFolders(this)
|
||||
.Select(i => i.Id.ToString("N", CultureInfo.InvariantCulture))
|
||||
.ToArray();
|
||||
|
||||
if (folders.Length == 0)
|
||||
{
|
||||
return key;
|
||||
}
|
||||
|
||||
return key + "-" + string.Join('-', folders);
|
||||
}
|
||||
|
||||
private static string GetUniqueSeriesKey(BaseItem series)
|
||||
{
|
||||
return series.GetPresentationUniqueKey();
|
||||
}
|
||||
|
||||
public override int GetChildCount(User user)
|
||||
{
|
||||
var seriesKey = GetUniqueSeriesKey(this);
|
||||
|
||||
var result = LibraryManager.GetCount(new InternalItemsQuery(user)
|
||||
{
|
||||
AncestorWithPresentationUniqueKey = null,
|
||||
SeriesPresentationUniqueKey = seriesKey,
|
||||
IncludeItemTypes = new[] { BaseItemKind.Season },
|
||||
IsVirtualItem = false,
|
||||
Limit = 0,
|
||||
DtoOptions = new DtoOptions(false)
|
||||
{
|
||||
EnableImages = false
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override int GetRecursiveChildCount(User user)
|
||||
{
|
||||
var seriesKey = GetUniqueSeriesKey(this);
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
AncestorWithPresentationUniqueKey = null,
|
||||
SeriesPresentationUniqueKey = seriesKey,
|
||||
DtoOptions = new DtoOptions(false)
|
||||
{
|
||||
EnableImages = false
|
||||
}
|
||||
};
|
||||
|
||||
if (query.IncludeItemTypes.Length == 0)
|
||||
{
|
||||
query.IncludeItemTypes = new[] { BaseItemKind.Episode };
|
||||
}
|
||||
|
||||
query.IsVirtualItem = false;
|
||||
query.Limit = 0;
|
||||
var totalRecordCount = LibraryManager.GetCount(query);
|
||||
|
||||
return totalRecordCount;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the user data key.
|
||||
/// </summary>
|
||||
/// <returns>System.String.</returns>
|
||||
public override List<string> GetUserDataKeys()
|
||||
{
|
||||
var list = base.GetUserDataKeys();
|
||||
|
||||
if (this.TryGetProviderId(MetadataProvider.Imdb, out var key))
|
||||
{
|
||||
list.Insert(0, key);
|
||||
}
|
||||
|
||||
if (this.TryGetProviderId(MetadataProvider.Tvdb, out key))
|
||||
{
|
||||
list.Insert(0, key);
|
||||
}
|
||||
|
||||
if (this.TryGetProviderId(MetadataProvider.Custom, out key))
|
||||
{
|
||||
list.Insert(0, key);
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public override IReadOnlyList<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
|
||||
{
|
||||
return GetSeasons(user, new DtoOptions(true));
|
||||
}
|
||||
|
||||
public IReadOnlyList<BaseItem> GetSeasons(User user, DtoOptions options)
|
||||
{
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
DtoOptions = options
|
||||
};
|
||||
|
||||
SetSeasonQueryOptions(query, user);
|
||||
|
||||
return LibraryManager.GetItemList(query);
|
||||
}
|
||||
|
||||
private void SetSeasonQueryOptions(InternalItemsQuery query, User user)
|
||||
{
|
||||
var seriesKey = GetUniqueSeriesKey(this);
|
||||
|
||||
query.AncestorWithPresentationUniqueKey = null;
|
||||
query.SeriesPresentationUniqueKey = seriesKey;
|
||||
query.IncludeItemTypes = new[] { BaseItemKind.Season };
|
||||
query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
|
||||
|
||||
if (user is not null && !user.DisplayMissingEpisodes)
|
||||
{
|
||||
query.IsMissing = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
|
||||
{
|
||||
var user = query.User;
|
||||
|
||||
if (SourceType == SourceType.Channel)
|
||||
{
|
||||
try
|
||||
{
|
||||
query.Parent = this;
|
||||
query.ChannelIds = [ChannelId];
|
||||
return ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None).GetAwaiter().GetResult();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Already logged at lower levels
|
||||
return new QueryResult<BaseItem>();
|
||||
}
|
||||
}
|
||||
|
||||
if (query.Recursive)
|
||||
{
|
||||
var seriesKey = GetUniqueSeriesKey(this);
|
||||
|
||||
query.AncestorWithPresentationUniqueKey = null;
|
||||
query.SeriesPresentationUniqueKey = seriesKey;
|
||||
if (query.OrderBy.Count == 0)
|
||||
{
|
||||
query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
|
||||
}
|
||||
|
||||
if (query.IncludeItemTypes.Length == 0)
|
||||
{
|
||||
query.IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season };
|
||||
}
|
||||
|
||||
query.IsVirtualItem = false;
|
||||
return LibraryManager.GetItemsResult(query);
|
||||
}
|
||||
|
||||
SetSeasonQueryOptions(query, user);
|
||||
|
||||
return LibraryManager.GetItemsResult(query);
|
||||
}
|
||||
|
||||
public IEnumerable<BaseItem> GetEpisodes(User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
||||
{
|
||||
var seriesKey = GetUniqueSeriesKey(this);
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
AncestorWithPresentationUniqueKey = null,
|
||||
SeriesPresentationUniqueKey = seriesKey,
|
||||
IncludeItemTypes = new[] { BaseItemKind.Episode, BaseItemKind.Season },
|
||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
|
||||
DtoOptions = options,
|
||||
};
|
||||
|
||||
if (!shouldIncludeMissingEpisodes)
|
||||
{
|
||||
query.IsMissing = false;
|
||||
}
|
||||
|
||||
var allItems = LibraryManager.GetItemList(query);
|
||||
|
||||
var allSeriesEpisodes = allItems.OfType<Episode>().ToList();
|
||||
|
||||
var allEpisodes = allItems.OfType<Season>()
|
||||
.SelectMany(i => i.GetEpisodes(this, user, allSeriesEpisodes, options, shouldIncludeMissingEpisodes))
|
||||
.Reverse();
|
||||
|
||||
// Specials could appear twice based on above - once in season 0, once in the aired season
|
||||
// This depends on settings for that series
|
||||
// When this happens, remove the duplicate from season 0
|
||||
|
||||
return allEpisodes.DistinctBy(i => i.Id).Reverse();
|
||||
}
|
||||
|
||||
public async Task RefreshAllMetadata(MetadataRefreshOptions refreshOptions, IProgress<double> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
Children = null; // invalidate cached children.
|
||||
// Refresh bottom up, seasons and episodes first, then the series
|
||||
var items = GetRecursiveChildren();
|
||||
|
||||
var totalItems = items.Count;
|
||||
var numComplete = 0;
|
||||
|
||||
// Refresh seasons
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item is not Season)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (refreshOptions.RefreshItem(item))
|
||||
{
|
||||
await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= totalItems;
|
||||
progress.Report(percent * 100);
|
||||
}
|
||||
|
||||
// Refresh episodes and other children
|
||||
foreach (var item in items)
|
||||
{
|
||||
if (item is Season)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
bool skipItem = item is Episode episode
|
||||
&& refreshOptions.MetadataRefreshMode != MetadataRefreshMode.FullRefresh
|
||||
&& !refreshOptions.ReplaceAllMetadata
|
||||
&& episode.IsMissingEpisode
|
||||
&& episode.LocationType == LocationType.Virtual
|
||||
&& episode.PremiereDate.HasValue
|
||||
&& (DateTime.UtcNow - episode.PremiereDate.Value).TotalDays > 30;
|
||||
|
||||
if (!skipItem)
|
||||
{
|
||||
if (refreshOptions.RefreshItem(item))
|
||||
{
|
||||
await item.RefreshMetadata(refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
numComplete++;
|
||||
double percent = numComplete;
|
||||
percent /= totalItems;
|
||||
progress.Report(percent * 100);
|
||||
}
|
||||
|
||||
refreshOptions = new MetadataRefreshOptions(refreshOptions);
|
||||
await ProviderManager.RefreshSingleItem(this, refreshOptions, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
||||
{
|
||||
var queryFromSeries = ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons;
|
||||
|
||||
// add optimization when this setting is not enabled
|
||||
var seriesKey = queryFromSeries ?
|
||||
GetUniqueSeriesKey(this) :
|
||||
GetUniqueSeriesKey(parentSeason);
|
||||
|
||||
var query = new InternalItemsQuery(user)
|
||||
{
|
||||
AncestorWithPresentationUniqueKey = queryFromSeries ? null : seriesKey,
|
||||
SeriesPresentationUniqueKey = queryFromSeries ? seriesKey : null,
|
||||
IncludeItemTypes = new[] { BaseItemKind.Episode },
|
||||
OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) },
|
||||
DtoOptions = options
|
||||
};
|
||||
|
||||
if (!shouldIncludeMissingEpisodes)
|
||||
{
|
||||
query.IsMissing = false;
|
||||
}
|
||||
|
||||
IReadOnlyList<BaseItem> allItems;
|
||||
if (SourceType == SourceType.Channel)
|
||||
{
|
||||
try
|
||||
{
|
||||
query.Parent = parentSeason;
|
||||
query.ChannelIds = [ChannelId];
|
||||
allItems = [.. ChannelManager.GetChannelItemsInternal(query, new Progress<double>(), CancellationToken.None).GetAwaiter().GetResult().Items];
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Already logged at lower levels
|
||||
return [];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
allItems = LibraryManager.GetItemList(query);
|
||||
}
|
||||
|
||||
return GetSeasonEpisodes(parentSeason, user, allItems, options, shouldIncludeMissingEpisodes);
|
||||
}
|
||||
|
||||
public List<BaseItem> GetSeasonEpisodes(Season parentSeason, User user, IEnumerable<BaseItem> allSeriesEpisodes, DtoOptions options, bool shouldIncludeMissingEpisodes)
|
||||
{
|
||||
if (allSeriesEpisodes is null)
|
||||
{
|
||||
return GetSeasonEpisodes(parentSeason, user, options, shouldIncludeMissingEpisodes);
|
||||
}
|
||||
|
||||
var episodes = FilterEpisodesBySeason(allSeriesEpisodes, parentSeason, ConfigurationManager.Configuration.DisplaySpecialsWithinSeasons);
|
||||
|
||||
var sortBy = (parentSeason.IndexNumber ?? -1) == 0 ? ItemSortBy.SortName : ItemSortBy.AiredEpisodeOrder;
|
||||
|
||||
return LibraryManager.Sort(episodes, user, new[] { sortBy }, SortOrder.Ascending).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters the episodes by season.
|
||||
/// </summary>
|
||||
/// <param name="episodes">The episodes.</param>
|
||||
/// <param name="parentSeason">The season.</param>
|
||||
/// <param name="includeSpecials"><c>true</c> to include special, <c>false</c> to not.</param>
|
||||
/// <returns>The set of episodes.</returns>
|
||||
public static IEnumerable<BaseItem> FilterEpisodesBySeason(IEnumerable<BaseItem> episodes, Season parentSeason, bool includeSpecials)
|
||||
{
|
||||
var seasonNumber = parentSeason.IndexNumber;
|
||||
var seasonPresentationKey = GetUniqueSeriesKey(parentSeason);
|
||||
|
||||
var supportSpecialsInSeason = includeSpecials && seasonNumber.HasValue && seasonNumber.Value != 0;
|
||||
|
||||
return episodes.Where(episode =>
|
||||
{
|
||||
var episodeItem = (Episode)episode;
|
||||
|
||||
var currentSeasonNumber = supportSpecialsInSeason ? episodeItem.AiredSeasonNumber : episode.ParentIndexNumber;
|
||||
if (currentSeasonNumber.HasValue && seasonNumber.HasValue && currentSeasonNumber.Value == seasonNumber.Value)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!currentSeasonNumber.HasValue && !seasonNumber.HasValue && parentSeason.LocationType == LocationType.Virtual)
|
||||
{
|
||||
return episodeItem.Season is null or { LocationType: LocationType.Virtual };
|
||||
}
|
||||
|
||||
var season = episodeItem.Season;
|
||||
return season is not null && string.Equals(GetUniqueSeriesKey(season), seasonPresentationKey, StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters the episodes by season.
|
||||
/// </summary>
|
||||
/// <param name="episodes">The episodes.</param>
|
||||
/// <param name="seasonNumber">The season.</param>
|
||||
/// <param name="includeSpecials"><c>true</c> to include special, <c>false</c> to not.</param>
|
||||
/// <returns>The set of episodes.</returns>
|
||||
public static IEnumerable<Episode> FilterEpisodesBySeason(IEnumerable<Episode> episodes, int seasonNumber, bool includeSpecials)
|
||||
{
|
||||
if (!includeSpecials || seasonNumber < 1)
|
||||
{
|
||||
return episodes.Where(i => (i.ParentIndexNumber ?? -1) == seasonNumber);
|
||||
}
|
||||
|
||||
return episodes.Where(i =>
|
||||
{
|
||||
var episode = i;
|
||||
|
||||
if (episode is not null)
|
||||
{
|
||||
var currentSeasonNumber = episode.AiredSeasonNumber;
|
||||
|
||||
return currentSeasonNumber.HasValue && currentSeasonNumber.Value == seasonNumber;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
protected override bool GetBlockUnratedValue(User user)
|
||||
{
|
||||
return user.GetPreferenceValues<UnratedItem>(PreferenceKind.BlockUnratedItems).Contains(UnratedItem.Series);
|
||||
}
|
||||
|
||||
public override UnratedItem GetBlockUnratedType()
|
||||
{
|
||||
return UnratedItem.Series;
|
||||
}
|
||||
|
||||
public SeriesInfo GetLookupInfo()
|
||||
{
|
||||
var info = GetItemLookupInfo<SeriesInfo>();
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
|
||||
{
|
||||
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
|
||||
|
||||
if (!ProductionYear.HasValue)
|
||||
{
|
||||
var info = LibraryManager.ParseName(Name);
|
||||
|
||||
var yearInName = info.Year;
|
||||
|
||||
if (yearInName.HasValue)
|
||||
{
|
||||
ProductionYear = yearInName;
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanges;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Jellyfin.Extensions;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public static class TagExtensions
|
||||
{
|
||||
public static void AddTag(this BaseItem item, string name)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
var current = item.Tags;
|
||||
|
||||
if (!current.Contains(name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (current.Length == 0)
|
||||
{
|
||||
item.Tags = [name];
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Tags = [..current, name];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1819, CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Serialization;
|
||||
using Jellyfin.Data.Enums;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.Providers;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Trailer.
|
||||
/// </summary>
|
||||
public class Trailer : Video, IHasLookupInfo<TrailerInfo>
|
||||
{
|
||||
public Trailer()
|
||||
{
|
||||
TrailerTypes = Array.Empty<TrailerType>();
|
||||
}
|
||||
|
||||
public TrailerType[] TrailerTypes { get; set; }
|
||||
|
||||
public override double GetDefaultPrimaryImageAspectRatio()
|
||||
=> 2.0 / 3;
|
||||
|
||||
public override UnratedItem GetBlockUnratedType()
|
||||
{
|
||||
return UnratedItem.Trailer;
|
||||
}
|
||||
|
||||
public TrailerInfo GetLookupInfo()
|
||||
{
|
||||
var info = GetItemLookupInfo<TrailerInfo>();
|
||||
|
||||
if (!IsInMixedFolder && IsFileProtocol)
|
||||
{
|
||||
info.Name = System.IO.Path.GetFileName(ContainingFolderPath);
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
|
||||
{
|
||||
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
|
||||
|
||||
if (!ProductionYear.HasValue)
|
||||
{
|
||||
var info = LibraryManager.ParseName(Name);
|
||||
|
||||
var yearInName = info.Year;
|
||||
|
||||
if (yearInName.HasValue)
|
||||
{
|
||||
ProductionYear = yearInName;
|
||||
hasChanges = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Try to get the year from the folder name
|
||||
if (!IsInMixedFolder)
|
||||
{
|
||||
info = LibraryManager.ParseName(System.IO.Path.GetFileName(ContainingFolderPath));
|
||||
|
||||
yearInName = info.Year;
|
||||
|
||||
if (yearInName.HasValue)
|
||||
{
|
||||
ProductionYear = yearInName;
|
||||
hasChanges = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanges;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Class UserItemData.
|
||||
/// </summary>
|
||||
public class UserItemData
|
||||
{
|
||||
public const double MinLikeValue = 6.5;
|
||||
|
||||
/// <summary>
|
||||
/// The _rating.
|
||||
/// </summary>
|
||||
private double? _rating;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the key.
|
||||
/// </summary>
|
||||
/// <value>The key.</value>
|
||||
public required string Key { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the users 0-10 rating.
|
||||
/// </summary>
|
||||
/// <value>The rating.</value>
|
||||
/// <exception cref="ArgumentOutOfRangeException">Rating;A 0 to 10 rating is required for UserItemData.</exception>
|
||||
public double? Rating
|
||||
{
|
||||
get => _rating;
|
||||
set
|
||||
{
|
||||
if (value.HasValue)
|
||||
{
|
||||
if (value.Value < 0 || value.Value > 10)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(value), "A 0 to 10 rating is required for UserItemData.");
|
||||
}
|
||||
}
|
||||
|
||||
_rating = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the playback position ticks.
|
||||
/// </summary>
|
||||
/// <value>The playback position ticks.</value>
|
||||
public long PlaybackPositionTicks { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the play count.
|
||||
/// </summary>
|
||||
/// <value>The play count.</value>
|
||||
public int PlayCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance is favorite.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance is favorite; otherwise, <c>false</c>.</value>
|
||||
public bool IsFavorite { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last played date.
|
||||
/// </summary>
|
||||
/// <value>The last played date.</value>
|
||||
public DateTime? LastPlayedDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this <see cref="UserItemData" /> is played.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if played; otherwise, <c>false</c>.</value>
|
||||
public bool Played { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index of the audio stream.
|
||||
/// </summary>
|
||||
/// <value>The index of the audio stream.</value>
|
||||
public int? AudioStreamIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index of the subtitle stream.
|
||||
/// </summary>
|
||||
/// <value>The index of the subtitle stream.</value>
|
||||
public int? SubtitleStreamIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the item is liked or not.
|
||||
/// This should never be serialized.
|
||||
/// </summary>
|
||||
/// <value><c>null</c> if [likes] contains no value, <c>true</c> if [likes]; otherwise, <c>false</c>.</value>
|
||||
[JsonIgnore]
|
||||
public bool? Likes
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Rating is not null)
|
||||
{
|
||||
return Rating >= MinLikeValue;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (value.HasValue)
|
||||
{
|
||||
Rating = value.Value ? 10 : 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
Rating = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Library;
|
||||
using MediaBrowser.Model.Querying;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Special class used for User Roots. Children contain actual ones defined for this user
|
||||
/// PLUS the virtual folders from the physical root (added by plug-ins).
|
||||
/// </summary>
|
||||
public class UserRootFolder : Folder
|
||||
{
|
||||
private readonly Lock _childIdsLock = new();
|
||||
private List<Guid> _childrenIds = null;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UserRootFolder"/> class.
|
||||
/// </summary>
|
||||
public UserRootFolder()
|
||||
{
|
||||
IsRoot = true;
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsInheritedParentImages => false;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPlayedStatus => false;
|
||||
|
||||
[JsonIgnore]
|
||||
protected override bool SupportsShortcutChildren => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool IsPreSorted => true;
|
||||
|
||||
private void ClearCache()
|
||||
{
|
||||
lock (_childIdsLock)
|
||||
{
|
||||
_childrenIds = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override IReadOnlyList<BaseItem> LoadChildren()
|
||||
{
|
||||
lock (_childIdsLock)
|
||||
{
|
||||
if (_childrenIds is null)
|
||||
{
|
||||
var list = base.LoadChildren();
|
||||
_childrenIds = list.Select(i => i.Id).ToList();
|
||||
return list;
|
||||
}
|
||||
|
||||
return _childrenIds.Select(LibraryManager.GetItemById).Where(i => i is not null).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
|
||||
{
|
||||
if (query.Recursive)
|
||||
{
|
||||
return QueryRecursive(query);
|
||||
}
|
||||
|
||||
var result = UserViewManager.GetUserViews(new UserViewQuery
|
||||
{
|
||||
User = query.User,
|
||||
PresetViews = query.PresetViews
|
||||
});
|
||||
|
||||
return UserViewBuilder.SortAndPage(result, null, query, LibraryManager);
|
||||
}
|
||||
|
||||
public override int GetChildCount(User user)
|
||||
{
|
||||
return GetChildren(user, true).Count;
|
||||
}
|
||||
|
||||
protected override IEnumerable<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
|
||||
{
|
||||
var list = base.GetEligibleChildrenForRecursiveChildren(user).ToList();
|
||||
list.AddRange(LibraryManager.RootFolder.VirtualChildren);
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
|
||||
{
|
||||
ClearCache();
|
||||
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
|
||||
|
||||
if (string.Equals("default", Name, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Name = "Media Folders";
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
return hasChanges;
|
||||
}
|
||||
|
||||
protected override IEnumerable<BaseItem> GetNonCachedChildren(IDirectoryService directoryService)
|
||||
{
|
||||
ClearCache();
|
||||
|
||||
return base.GetNonCachedChildren(directoryService);
|
||||
}
|
||||
|
||||
protected override async Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, bool allowRemoveRoot, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
|
||||
{
|
||||
ClearCache();
|
||||
|
||||
await base.ValidateChildrenInternal(progress, recursive, refreshChildMetadata, allowRemoveRoot, refreshOptions, directoryService, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
ClearCache();
|
||||
|
||||
// Not the best way to handle this, but it solves an issue
|
||||
// CollectionFolders aren't always getting saved after changes
|
||||
// This means that grabbing the item by Id may end up returning the old one
|
||||
// Fix is in two places - make sure the folder gets saved
|
||||
// And here to remedy it for affected users.
|
||||
// In theory this can be removed eventually.
|
||||
foreach (var item in Children)
|
||||
{
|
||||
LibraryManager.RegisterItem(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Database.Implementations.Entities;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Controller.TV;
|
||||
using MediaBrowser.Model.Querying;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
public class UserView : Folder, IHasCollectionType
|
||||
{
|
||||
private static readonly CollectionType?[] _viewTypesEligibleForGrouping =
|
||||
{
|
||||
Jellyfin.Data.Enums.CollectionType.movies,
|
||||
Jellyfin.Data.Enums.CollectionType.tvshows,
|
||||
null
|
||||
};
|
||||
|
||||
private static readonly CollectionType?[] _originalFolderViewTypes =
|
||||
{
|
||||
Jellyfin.Data.Enums.CollectionType.books,
|
||||
Jellyfin.Data.Enums.CollectionType.musicvideos,
|
||||
Jellyfin.Data.Enums.CollectionType.homevideos,
|
||||
Jellyfin.Data.Enums.CollectionType.photos,
|
||||
Jellyfin.Data.Enums.CollectionType.music,
|
||||
Jellyfin.Data.Enums.CollectionType.boxsets
|
||||
};
|
||||
|
||||
public static ITVSeriesManager TVSeriesManager { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the view type.
|
||||
/// </summary>
|
||||
public CollectionType? ViewType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the display parent id.
|
||||
/// </summary>
|
||||
public new Guid DisplayParentId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user id.
|
||||
/// </summary>
|
||||
public Guid? UserId { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public CollectionType? CollectionType => ViewType;
|
||||
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public override bool SupportsInheritedParentImages => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPlayedStatus => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPeople => false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IEnumerable<Guid> GetIdsForAncestorQuery()
|
||||
{
|
||||
if (!DisplayParentId.IsEmpty())
|
||||
{
|
||||
yield return DisplayParentId;
|
||||
}
|
||||
else if (!ParentId.IsEmpty())
|
||||
{
|
||||
yield return ParentId;
|
||||
}
|
||||
else
|
||||
{
|
||||
yield return Id;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetChildCount(User user)
|
||||
{
|
||||
return GetChildren(user, true, null).Count;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override QueryResult<BaseItem> GetItemsInternal(InternalItemsQuery query)
|
||||
{
|
||||
var parent = this as Folder;
|
||||
|
||||
if (!DisplayParentId.IsEmpty())
|
||||
{
|
||||
parent = LibraryManager.GetItemById(DisplayParentId) as Folder ?? parent;
|
||||
}
|
||||
else if (!ParentId.IsEmpty())
|
||||
{
|
||||
parent = LibraryManager.GetItemById(ParentId) as Folder ?? parent;
|
||||
}
|
||||
|
||||
return new UserViewBuilder(UserViewManager, LibraryManager, Logger, UserDataManager, TVSeriesManager)
|
||||
.GetUserItems(parent, this, CollectionType, query);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override List<BaseItem> GetChildren(User user, bool includeLinkedChildren, InternalItemsQuery query)
|
||||
{
|
||||
query ??= new InternalItemsQuery(user);
|
||||
|
||||
query.EnableTotalRecordCount = false;
|
||||
var result = GetItemList(query);
|
||||
|
||||
return result.ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanDelete()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsSaveLocalMetadataEnabled()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IReadOnlyList<BaseItem> GetRecursiveChildren(User user, InternalItemsQuery query, out int totalCount)
|
||||
{
|
||||
query.SetUser(user);
|
||||
query.Recursive = true;
|
||||
query.EnableTotalRecordCount = false;
|
||||
query.ForceDirect = true;
|
||||
var data = GetItemList(query);
|
||||
totalCount = data.Count;
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IReadOnlyList<BaseItem> GetEligibleChildrenForRecursiveChildren(User user)
|
||||
{
|
||||
return GetChildren(user, false, null);
|
||||
}
|
||||
|
||||
public static bool IsUserSpecific(Folder folder)
|
||||
{
|
||||
if (folder is not ICollectionFolder collectionFolder)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (folder is ISupportsUserSpecificView supportsUserSpecific
|
||||
&& supportsUserSpecific.EnableUserSpecificView)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return collectionFolder.CollectionType == Jellyfin.Data.Enums.CollectionType.playlists;
|
||||
}
|
||||
|
||||
public static bool IsEligibleForGrouping(Folder folder)
|
||||
{
|
||||
return folder is ICollectionFolder collectionFolder
|
||||
&& IsEligibleForGrouping(collectionFolder.CollectionType);
|
||||
}
|
||||
|
||||
public static bool IsEligibleForGrouping(CollectionType? viewType)
|
||||
{
|
||||
return _viewTypesEligibleForGrouping.Contains(viewType);
|
||||
}
|
||||
|
||||
public static bool EnableOriginalFolder(CollectionType? viewType)
|
||||
{
|
||||
return _originalFolderViewTypes.Contains(viewType);
|
||||
}
|
||||
|
||||
protected override Task ValidateChildrenInternal(IProgress<double> progress, bool recursive, bool refreshChildMetadata, bool allowRemoveRoot, MetadataRefreshOptions refreshOptions, IDirectoryService directoryService, CancellationToken cancellationToken)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,566 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jellyfin.Data.Enums;
|
||||
using Jellyfin.Extensions;
|
||||
using MediaBrowser.Controller.Library;
|
||||
using MediaBrowser.Controller.LiveTv;
|
||||
using MediaBrowser.Controller.Persistence;
|
||||
using MediaBrowser.Controller.Providers;
|
||||
using MediaBrowser.Model.Dto;
|
||||
using MediaBrowser.Model.Entities;
|
||||
using MediaBrowser.Model.IO;
|
||||
using MediaBrowser.Model.MediaInfo;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Video.
|
||||
/// </summary>
|
||||
public class Video : BaseItem,
|
||||
IHasAspectRatio,
|
||||
ISupportsPlaceHolders,
|
||||
IHasMediaSources
|
||||
{
|
||||
public Video()
|
||||
{
|
||||
AdditionalParts = Array.Empty<string>();
|
||||
LocalAlternateVersions = Array.Empty<string>();
|
||||
SubtitleFiles = Array.Empty<string>();
|
||||
AudioFiles = Array.Empty<string>();
|
||||
LinkedAlternateVersions = Array.Empty<LinkedChild>();
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public string PrimaryVersionId { get; set; }
|
||||
|
||||
public string[] AdditionalParts { get; set; }
|
||||
|
||||
public string[] LocalAlternateVersions { get; set; }
|
||||
|
||||
public LinkedChild[] LinkedAlternateVersions { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPlayedStatus => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPeople => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsInheritedParentImages => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPositionTicksResume
|
||||
{
|
||||
get
|
||||
{
|
||||
var extraType = ExtraType;
|
||||
if (extraType.HasValue)
|
||||
{
|
||||
if (extraType.Value == Model.Entities.ExtraType.Sample)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (extraType.Value == Model.Entities.ExtraType.ThemeVideo)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (extraType.Value == Model.Entities.ExtraType.Trailer)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsThemeMedia => true;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the timestamp.
|
||||
/// </summary>
|
||||
/// <value>The timestamp.</value>
|
||||
public TransportStreamTimestamp? Timestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the subtitle paths.
|
||||
/// </summary>
|
||||
/// <value>The subtitle paths.</value>
|
||||
public string[] SubtitleFiles { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the audio paths.
|
||||
/// </summary>
|
||||
/// <value>The audio paths.</value>
|
||||
public string[] AudioFiles { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this instance has subtitles.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if this instance has subtitles; otherwise, <c>false</c>.</value>
|
||||
public bool HasSubtitles { get; set; }
|
||||
|
||||
public bool IsPlaceHolder { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default index of the video stream.
|
||||
/// </summary>
|
||||
/// <value>The default index of the video stream.</value>
|
||||
public int? DefaultVideoStreamIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the video.
|
||||
/// </summary>
|
||||
/// <value>The type of the video.</value>
|
||||
public VideoType VideoType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type of the iso.
|
||||
/// </summary>
|
||||
/// <value>The type of the iso.</value>
|
||||
public IsoType? IsoType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the video3 D format.
|
||||
/// </summary>
|
||||
/// <value>The video3 D format.</value>
|
||||
public Video3DFormat? Video3DFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the aspect ratio.
|
||||
/// </summary>
|
||||
/// <value>The aspect ratio.</value>
|
||||
public string AspectRatio { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsAddingToPlaylist => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public int MediaSourceCount
|
||||
{
|
||||
get
|
||||
{
|
||||
return GetMediaSourceCount();
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsStacked => AdditionalParts.Length > 0;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool HasLocalAlternateVersions => LocalAlternateVersions.Length > 0;
|
||||
|
||||
public static IRecordingsManager RecordingsManager { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public override SourceType SourceType
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsActiveRecording())
|
||||
{
|
||||
return SourceType.LiveTV;
|
||||
}
|
||||
|
||||
return base.SourceType;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public bool IsCompleteMedia
|
||||
{
|
||||
get
|
||||
{
|
||||
if (SourceType == SourceType.Channel)
|
||||
{
|
||||
return !Tags.Contains("livestream", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
return !IsActiveRecording();
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
protected virtual bool EnableDefaultVideoUserDataKeys => true;
|
||||
|
||||
[JsonIgnore]
|
||||
public override string ContainingFolderPath
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsStacked)
|
||||
{
|
||||
return System.IO.Path.GetDirectoryName(Path);
|
||||
}
|
||||
|
||||
if (!IsPlaceHolder)
|
||||
{
|
||||
if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd)
|
||||
{
|
||||
return Path;
|
||||
}
|
||||
}
|
||||
|
||||
return base.ContainingFolderPath;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public override string FileNameWithoutExtension
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsFileProtocol)
|
||||
{
|
||||
if (VideoType == VideoType.BluRay || VideoType == VideoType.Dvd)
|
||||
{
|
||||
return System.IO.Path.GetFileName(Path);
|
||||
}
|
||||
|
||||
return System.IO.Path.GetFileNameWithoutExtension(Path);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether [is3 D].
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if [is3 D]; otherwise, <c>false</c>.</value>
|
||||
[JsonIgnore]
|
||||
public bool Is3D => Video3DFormat.HasValue;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the media.
|
||||
/// </summary>
|
||||
/// <value>The type of the media.</value>
|
||||
[JsonIgnore]
|
||||
public override MediaType MediaType => MediaType.Video;
|
||||
|
||||
private int GetMediaSourceCount(HashSet<Guid> callstack = null)
|
||||
{
|
||||
callstack ??= new();
|
||||
if (!string.IsNullOrEmpty(PrimaryVersionId))
|
||||
{
|
||||
var item = LibraryManager.GetItemById(PrimaryVersionId);
|
||||
if (item is Video video)
|
||||
{
|
||||
if (callstack.Contains(video.Id))
|
||||
{
|
||||
return video.LinkedAlternateVersions.Length + video.LocalAlternateVersions.Length + 1;
|
||||
}
|
||||
|
||||
callstack.Add(video.Id);
|
||||
return video.GetMediaSourceCount(callstack);
|
||||
}
|
||||
}
|
||||
|
||||
return LinkedAlternateVersions.Length + LocalAlternateVersions.Length + 1;
|
||||
}
|
||||
|
||||
public override List<string> GetUserDataKeys()
|
||||
{
|
||||
var list = base.GetUserDataKeys();
|
||||
|
||||
if (EnableDefaultVideoUserDataKeys)
|
||||
{
|
||||
if (ExtraType.HasValue)
|
||||
{
|
||||
var key = this.GetProviderId(MetadataProvider.Tmdb);
|
||||
if (!string.IsNullOrEmpty(key))
|
||||
{
|
||||
list.Insert(0, GetUserDataKey(key));
|
||||
}
|
||||
|
||||
key = this.GetProviderId(MetadataProvider.Imdb);
|
||||
if (!string.IsNullOrEmpty(key))
|
||||
{
|
||||
list.Insert(0, GetUserDataKey(key));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var key = this.GetProviderId(MetadataProvider.Imdb);
|
||||
if (!string.IsNullOrEmpty(key))
|
||||
{
|
||||
list.Insert(0, key);
|
||||
}
|
||||
|
||||
key = this.GetProviderId(MetadataProvider.Tmdb);
|
||||
if (!string.IsNullOrEmpty(key))
|
||||
{
|
||||
list.Insert(0, key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public void SetPrimaryVersionId(string id)
|
||||
{
|
||||
if (string.IsNullOrEmpty(id))
|
||||
{
|
||||
PrimaryVersionId = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
PrimaryVersionId = id;
|
||||
}
|
||||
|
||||
PresentationUniqueKey = CreatePresentationUniqueKey();
|
||||
}
|
||||
|
||||
public override string CreatePresentationUniqueKey()
|
||||
{
|
||||
if (!string.IsNullOrEmpty(PrimaryVersionId))
|
||||
{
|
||||
return PrimaryVersionId;
|
||||
}
|
||||
|
||||
return base.CreatePresentationUniqueKey();
|
||||
}
|
||||
|
||||
public override bool CanDownload()
|
||||
{
|
||||
if (VideoType == VideoType.Dvd || VideoType == VideoType.BluRay)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return IsFileProtocol;
|
||||
}
|
||||
|
||||
protected override bool IsActiveRecording()
|
||||
{
|
||||
return RecordingsManager.GetActiveRecordingInfo(Path) is not null;
|
||||
}
|
||||
|
||||
public override bool CanDelete()
|
||||
{
|
||||
if (IsActiveRecording())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return base.CanDelete();
|
||||
}
|
||||
|
||||
public IEnumerable<Guid> GetAdditionalPartIds()
|
||||
{
|
||||
return AdditionalParts.Select(i => LibraryManager.GetNewItemId(i, typeof(Video)));
|
||||
}
|
||||
|
||||
public IEnumerable<Guid> GetLocalAlternateVersionIds()
|
||||
{
|
||||
return LocalAlternateVersions.Select(i => LibraryManager.GetNewItemId(i, typeof(Video)));
|
||||
}
|
||||
|
||||
private string GetUserDataKey(string providerId)
|
||||
{
|
||||
var key = providerId + "-" + ExtraType.ToString().ToLowerInvariant();
|
||||
|
||||
// Make sure different trailers have their own data.
|
||||
if (RunTimeTicks.HasValue)
|
||||
{
|
||||
key += "-" + RunTimeTicks.Value.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
public IEnumerable<Video> GetLinkedAlternateVersions()
|
||||
{
|
||||
return LinkedAlternateVersions
|
||||
.Select(GetLinkedChild)
|
||||
.Where(i => i is not null)
|
||||
.OfType<Video>()
|
||||
.OrderBy(i => i.SortName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the additional parts.
|
||||
/// </summary>
|
||||
/// <returns>IEnumerable{Video}.</returns>
|
||||
public IOrderedEnumerable<Video> GetAdditionalParts()
|
||||
{
|
||||
return GetAdditionalPartIds()
|
||||
.Select(i => LibraryManager.GetItemById(i))
|
||||
.Where(i => i is not null)
|
||||
.OfType<Video>()
|
||||
.OrderBy(i => i.SortName);
|
||||
}
|
||||
|
||||
internal override ItemUpdateType UpdateFromResolvedItem(BaseItem newItem)
|
||||
{
|
||||
var updateType = base.UpdateFromResolvedItem(newItem);
|
||||
|
||||
if (newItem is Video newVideo)
|
||||
{
|
||||
if (!AdditionalParts.SequenceEqual(newVideo.AdditionalParts, StringComparer.Ordinal))
|
||||
{
|
||||
AdditionalParts = newVideo.AdditionalParts;
|
||||
updateType |= ItemUpdateType.MetadataImport;
|
||||
}
|
||||
|
||||
if (!LocalAlternateVersions.SequenceEqual(newVideo.LocalAlternateVersions, StringComparer.Ordinal))
|
||||
{
|
||||
LocalAlternateVersions = newVideo.LocalAlternateVersions;
|
||||
updateType |= ItemUpdateType.MetadataImport;
|
||||
}
|
||||
|
||||
if (VideoType != newVideo.VideoType)
|
||||
{
|
||||
VideoType = newVideo.VideoType;
|
||||
updateType |= ItemUpdateType.MetadataImport;
|
||||
}
|
||||
}
|
||||
|
||||
return updateType;
|
||||
}
|
||||
|
||||
protected override async Task<bool> RefreshedOwnedItems(MetadataRefreshOptions options, IReadOnlyList<FileSystemMetadata> fileSystemChildren, CancellationToken cancellationToken)
|
||||
{
|
||||
var hasChanges = await base.RefreshedOwnedItems(options, fileSystemChildren, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (IsStacked)
|
||||
{
|
||||
var tasks = AdditionalParts
|
||||
.Select(i => RefreshMetadataForOwnedVideo(options, true, i, cancellationToken));
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
// Must have a parent to have additional parts or alternate versions
|
||||
// In other words, it must be part of the Parent/Child tree
|
||||
// The additional parts won't have additional parts themselves
|
||||
if (IsFileProtocol && SupportsOwnedItems)
|
||||
{
|
||||
if (!IsStacked)
|
||||
{
|
||||
RefreshLinkedAlternateVersions();
|
||||
|
||||
var tasks = LocalAlternateVersions
|
||||
.Select(i => RefreshMetadataForOwnedVideo(options, false, i, cancellationToken));
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
return hasChanges;
|
||||
}
|
||||
|
||||
private void RefreshLinkedAlternateVersions()
|
||||
{
|
||||
foreach (var child in LinkedAlternateVersions)
|
||||
{
|
||||
// Reset the cached value
|
||||
if (child.ItemId.IsNullOrEmpty())
|
||||
{
|
||||
child.ItemId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken)
|
||||
{
|
||||
await base.UpdateToRepositoryAsync(updateReason, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var localAlternates = GetLocalAlternateVersionIds()
|
||||
.Select(i => LibraryManager.GetItemById(i))
|
||||
.Where(i => i is not null);
|
||||
|
||||
foreach (var item in localAlternates)
|
||||
{
|
||||
item.ImageInfos = ImageInfos;
|
||||
item.Overview = Overview;
|
||||
item.ProductionYear = ProductionYear;
|
||||
item.PremiereDate = PremiereDate;
|
||||
item.CommunityRating = CommunityRating;
|
||||
item.OfficialRating = OfficialRating;
|
||||
item.Genres = Genres;
|
||||
item.ProviderIds = ProviderIds;
|
||||
|
||||
await item.UpdateToRepositoryAsync(ItemUpdateType.MetadataDownload, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<FileSystemMetadata> GetDeletePaths()
|
||||
{
|
||||
if (!IsInMixedFolder)
|
||||
{
|
||||
return new[]
|
||||
{
|
||||
new FileSystemMetadata
|
||||
{
|
||||
FullName = ContainingFolderPath,
|
||||
IsDirectory = true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return base.GetDeletePaths();
|
||||
}
|
||||
|
||||
public virtual MediaStream GetDefaultVideoStream()
|
||||
{
|
||||
if (!DefaultVideoStreamIndex.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return MediaSourceManager.GetMediaStreams(new MediaStreamQuery
|
||||
{
|
||||
ItemId = Id,
|
||||
Index = DefaultVideoStreamIndex.Value
|
||||
}).FirstOrDefault();
|
||||
}
|
||||
|
||||
protected override IEnumerable<(BaseItem Item, MediaSourceType MediaSourceType)> GetAllItemsForMediaSources()
|
||||
{
|
||||
var list = new List<(BaseItem, MediaSourceType)>
|
||||
{
|
||||
(this, MediaSourceType.Default)
|
||||
};
|
||||
|
||||
list.AddRange(GetLinkedAlternateVersions().Select(i => ((BaseItem)i, MediaSourceType.Grouping)));
|
||||
|
||||
if (!string.IsNullOrEmpty(PrimaryVersionId))
|
||||
{
|
||||
if (LibraryManager.GetItemById(PrimaryVersionId) is Video primary)
|
||||
{
|
||||
var existingIds = list.Select(i => i.Item1.Id).ToList();
|
||||
list.Add((primary, MediaSourceType.Grouping));
|
||||
list.AddRange(primary.GetLinkedAlternateVersions().Where(i => !existingIds.Contains(i.Id)).Select(i => ((BaseItem)i, MediaSourceType.Grouping)));
|
||||
}
|
||||
}
|
||||
|
||||
var localAlternates = list
|
||||
.SelectMany(i =>
|
||||
{
|
||||
return i.Item1 is Video video ? video.GetLocalAlternateVersionIds() : Enumerable.Empty<Guid>();
|
||||
})
|
||||
.Select(LibraryManager.GetItemById)
|
||||
.Where(i => i is not null)
|
||||
.ToList();
|
||||
|
||||
list.AddRange(localAlternates.Select(i => (i, MediaSourceType.Default)));
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CS1591
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace MediaBrowser.Controller.Entities
|
||||
{
|
||||
/// <summary>
|
||||
/// Class Year.
|
||||
/// </summary>
|
||||
[Common.RequiresSourceSerialisation]
|
||||
public class Year : BaseItem, IItemByName
|
||||
{
|
||||
[JsonIgnore]
|
||||
public override bool SupportsAncestors => false;
|
||||
|
||||
[JsonIgnore]
|
||||
public override bool SupportsPeople => false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the folder containing the item.
|
||||
/// If the item is a folder, it returns the folder itself.
|
||||
/// </summary>
|
||||
/// <value>The containing folder path.</value>
|
||||
[JsonIgnore]
|
||||
public override string ContainingFolderPath => Path;
|
||||
|
||||
public override bool CanDelete()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public override List<string> GetUserDataKeys()
|
||||
{
|
||||
var list = base.GetUserDataKeys();
|
||||
|
||||
list.Insert(0, "Year-" + Name);
|
||||
return list;
|
||||
}
|
||||
|
||||
public override double GetDefaultPrimaryImageAspectRatio()
|
||||
{
|
||||
double value = 2;
|
||||
value /= 3;
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public override bool IsSaveLocalMetadataEnabled()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public IReadOnlyList<BaseItem> GetTaggedItems(InternalItemsQuery query)
|
||||
{
|
||||
if (!int.TryParse(Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
|
||||
{
|
||||
return new List<BaseItem>();
|
||||
}
|
||||
|
||||
query.Years = new[] { year };
|
||||
|
||||
return LibraryManager.GetItemList(query);
|
||||
}
|
||||
|
||||
public int? GetYearValue()
|
||||
{
|
||||
if (int.TryParse(Name, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year))
|
||||
{
|
||||
return year;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string GetPath(string name)
|
||||
{
|
||||
return GetPath(name, true);
|
||||
}
|
||||
|
||||
public static string GetPath(string name, bool normalizeName)
|
||||
{
|
||||
// Trim the period at the end because windows will have a hard time with that
|
||||
var validName = normalizeName ?
|
||||
FileSystem.GetValidFilename(name).Trim().TrimEnd('.') :
|
||||
name;
|
||||
|
||||
return System.IO.Path.Combine(ConfigurationManager.ApplicationPaths.YearPath, validName);
|
||||
}
|
||||
|
||||
private string GetRebasedPath()
|
||||
{
|
||||
return GetPath(System.IO.Path.GetFileName(Path), false);
|
||||
}
|
||||
|
||||
public override bool RequiresRefresh()
|
||||
{
|
||||
var newPath = GetRebasedPath();
|
||||
if (!string.Equals(Path, newPath, StringComparison.Ordinal))
|
||||
{
|
||||
Logger.LogDebug("{0} path has changed from {1} to {2}", GetType().Name, Path, newPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.RequiresRefresh();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is called before any metadata refresh and returns true if changes were made.
|
||||
/// </summary>
|
||||
/// <param name="replaceAllMetadata">Whether to replace all metadata.</param>
|
||||
/// <returns>true if the item has change, else false.</returns>
|
||||
public override bool BeforeMetadataRefresh(bool replaceAllMetadata)
|
||||
{
|
||||
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
|
||||
|
||||
var newPath = GetRebasedPath();
|
||||
if (!string.Equals(Path, newPath, StringComparison.Ordinal))
|
||||
{
|
||||
Path = newPath;
|
||||
hasChanges = true;
|
||||
}
|
||||
|
||||
return hasChanges;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using MediaBrowser.Controller.Session;
|
||||
|
||||
namespace MediaBrowser.Controller.Events.Authentication;
|
||||
|
||||
/// <summary>
|
||||
/// A class representing an authentication result event.
|
||||
/// </summary>
|
||||
public class AuthenticationRequestEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthenticationRequestEventArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="request">The <see cref="AuthenticationRequest"/>.</param>
|
||||
public AuthenticationRequestEventArgs(AuthenticationRequest request)
|
||||
{
|
||||
Username = request.Username;
|
||||
UserId = request.UserId;
|
||||
App = request.App;
|
||||
AppVersion = request.AppVersion;
|
||||
DeviceId = request.DeviceId;
|
||||
DeviceName = request.DeviceName;
|
||||
RemoteEndPoint = request.RemoteEndPoint;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user name.
|
||||
/// </summary>
|
||||
public string? Username { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user id.
|
||||
/// </summary>
|
||||
public Guid? UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the app.
|
||||
/// </summary>
|
||||
public string? App { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the app version.
|
||||
/// </summary>
|
||||
public string? AppVersion { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the device id.
|
||||
/// </summary>
|
||||
public string? DeviceId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the device name.
|
||||
/// </summary>
|
||||
public string? DeviceName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the remote endpoint.
|
||||
/// </summary>
|
||||
public string? RemoteEndPoint { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using MediaBrowser.Controller.Authentication;
|
||||
using MediaBrowser.Model.Dto;
|
||||
|
||||
namespace MediaBrowser.Controller.Events.Authentication;
|
||||
|
||||
/// <summary>
|
||||
/// A class representing an authentication result event.
|
||||
/// </summary>
|
||||
public class AuthenticationResultEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AuthenticationResultEventArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="result">The <see cref="AuthenticationResult"/>.</param>
|
||||
public AuthenticationResultEventArgs(AuthenticationResult result)
|
||||
{
|
||||
User = result.User;
|
||||
SessionInfo = result.SessionInfo;
|
||||
ServerId = result.ServerId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the user.
|
||||
/// </summary>
|
||||
public UserDto User { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the session information.
|
||||
/// </summary>
|
||||
public SessionInfoDto? SessionInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the server id.
|
||||
/// </summary>
|
||||
public string? ServerId { get; set; }
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user