repo creation with initial code after cloning public repo

This commit is contained in:
2026-02-19 07:36:25 -05:00
commit 460884fea3
2860 changed files with 650825 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
namespace MediaBrowser.Common.Api;
/// <summary>
/// Policies for the API authorization.
/// </summary>
public static class Policies
{
/// <summary>
/// Policy name for requiring first time setup or elevated privileges.
/// </summary>
public const string FirstTimeSetupOrElevated = "FirstTimeSetupOrElevated";
/// <summary>
/// Policy name for requiring elevated privileges.
/// </summary>
public const string RequiresElevation = "RequiresElevation";
/// <summary>
/// Policy name for allowing local access only.
/// </summary>
public const string LocalAccessOnly = "LocalAccessOnly";
/// <summary>
/// Policy name for escaping schedule controls.
/// </summary>
public const string IgnoreParentalControl = "IgnoreParentalControl";
/// <summary>
/// Policy name for requiring download permission.
/// </summary>
public const string Download = "Download";
/// <summary>
/// Policy name for requiring first time setup or default permissions.
/// </summary>
public const string FirstTimeSetupOrDefault = "FirstTimeSetupOrDefault";
/// <summary>
/// Policy name for requiring local access or elevated privileges.
/// </summary>
public const string LocalAccessOrRequiresElevation = "LocalAccessOrRequiresElevation";
/// <summary>
/// Policy name for requiring (anonymous) LAN access.
/// </summary>
public const string AnonymousLanAccessPolicy = "AnonymousLanAccessPolicy";
/// <summary>
/// Policy name for escaping schedule controls or requiring first time setup.
/// </summary>
public const string FirstTimeSetupOrIgnoreParentalControl = "FirstTimeSetupOrIgnoreParentalControl";
/// <summary>
/// Policy name for accessing SyncPlay.
/// </summary>
public const string SyncPlayHasAccess = "SyncPlayHasAccess";
/// <summary>
/// Policy name for creating a SyncPlay group.
/// </summary>
public const string SyncPlayCreateGroup = "SyncPlayCreateGroup";
/// <summary>
/// Policy name for joining a SyncPlay group.
/// </summary>
public const string SyncPlayJoinGroup = "SyncPlayJoinGroup";
/// <summary>
/// Policy name for accessing a SyncPlay group.
/// </summary>
public const string SyncPlayIsInGroup = "SyncPlayIsInGroup";
/// <summary>
/// Policy name for accessing collection management.
/// </summary>
public const string CollectionManagement = "CollectionManagement";
/// <summary>
/// Policy name for accessing LiveTV.
/// </summary>
public const string LiveTvAccess = "LiveTvAccess";
/// <summary>
/// Policy name for managing LiveTV.
/// </summary>
public const string LiveTvManagement = "LiveTvManagement";
/// <summary>
/// Policy name for accessing subtitles management.
/// </summary>
public const string SubtitleManagement = "SubtitleManagement";
/// <summary>
/// Policy name for accessing lyric management.
/// </summary>
public const string LyricManagement = "LyricManagement";
}
@@ -0,0 +1,22 @@
#nullable disable
using System;
namespace MediaBrowser.Common.Configuration
{
/// <summary>
/// Describes a single entry in the application configuration.
/// </summary>
public class ConfigurationStore
{
/// <summary>
/// Gets or sets the unique identifier for the configuration.
/// </summary>
public string Key { get; set; }
/// <summary>
/// Gets or sets the type used to store the data for this configuration entry.
/// </summary>
public Type ConfigurationType { get; set; }
}
}
@@ -0,0 +1,33 @@
using System;
namespace MediaBrowser.Common.Configuration
{
/// <summary>
/// <see cref="EventArgs" /> for the ConfigurationUpdated event.
/// </summary>
public class ConfigurationUpdateEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="ConfigurationUpdateEventArgs"/> class.
/// </summary>
/// <param name="key">The configuration key.</param>
/// <param name="newConfiguration">The new configuration.</param>
public ConfigurationUpdateEventArgs(string key, object newConfiguration)
{
Key = key;
NewConfiguration = newConfiguration;
}
/// <summary>
/// Gets the key.
/// </summary>
/// <value>The key.</value>
public string Key { get; }
/// <summary>
/// Gets the new configuration.
/// </summary>
/// <value>The new configuration.</value>
public object NewConfiguration { get; }
}
}
@@ -0,0 +1,42 @@
using System;
using System.IO;
using MediaBrowser.Model.Configuration;
namespace MediaBrowser.Common.Configuration
{
/// <summary>
/// Class containing extension methods for working with the encoding configuration.
/// </summary>
public static class EncodingConfigurationExtensions
{
/// <summary>
/// Gets the encoding options.
/// </summary>
/// <param name="configurationManager">The configuration manager.</param>
/// <returns>The encoding options.</returns>
public static EncodingOptions GetEncodingOptions(this IConfigurationManager configurationManager)
=> configurationManager.GetConfiguration<EncodingOptions>("encoding");
/// <summary>
/// Retrieves the transcoding temp path from the encoding configuration, falling back to a default if no path
/// is specified in configuration. If the directory does not exist, it will be created.
/// </summary>
/// <param name="configurationManager">The configuration manager.</param>
/// <returns>The transcoding temp path.</returns>
/// <exception cref="UnauthorizedAccessException">If the directory does not exist, and the caller does not have the required permission to create it.</exception>
/// <exception cref="NotSupportedException">If there is a custom path transcoding path specified, but it is invalid.</exception>
/// <exception cref="IOException">If the directory does not exist, and it also could not be created.</exception>
public static string GetTranscodePath(this IConfigurationManager configurationManager)
{
// Get the configured path and fall back to a default
var transcodingTempPath = configurationManager.GetEncodingOptions().TranscodingTempPath;
if (string.IsNullOrEmpty(transcodingTempPath))
{
transcodingTempPath = Path.Combine(configurationManager.CommonApplicationPaths.CachePath, "transcodes");
}
configurationManager.CommonApplicationPaths.CreateAndCheckMarker(transcodingTempPath, "transcode", true);
return transcodingTempPath;
}
}
}
@@ -0,0 +1,113 @@
namespace MediaBrowser.Common.Configuration
{
/// <summary>
/// Interface IApplicationPaths.
/// </summary>
public interface IApplicationPaths
{
/// <summary>
/// Gets the path to the program data folder.
/// </summary>
/// <value>The program data path.</value>
string ProgramDataPath { get; }
/// <summary>
/// Gets the path to the web UI resources folder.
/// </summary>
/// <remarks>
/// This value is not relevant if the server is configured to not host any static web content.
/// </remarks>
string WebPath { get; }
/// <summary>
/// Gets the path to the program system folder.
/// </summary>
/// <value>The program data path.</value>
string ProgramSystemPath { get; }
/// <summary>
/// Gets the folder path to the data directory.
/// </summary>
/// <value>The data directory.</value>
string DataPath { get; }
/// <summary>
/// Gets the image cache path.
/// </summary>
/// <value>The image cache path.</value>
string ImageCachePath { get; }
/// <summary>
/// Gets the path to the plugin directory.
/// </summary>
/// <value>The plugins path.</value>
string PluginsPath { get; }
/// <summary>
/// Gets the path to the plugin configurations directory.
/// </summary>
/// <value>The plugin configurations path.</value>
string PluginConfigurationsPath { get; }
/// <summary>
/// Gets the path to the log directory.
/// </summary>
/// <value>The log directory path.</value>
string LogDirectoryPath { get; }
/// <summary>
/// Gets the path to the application configuration root directory.
/// </summary>
/// <value>The configuration directory path.</value>
string ConfigurationDirectoryPath { get; }
/// <summary>
/// Gets the path to the system configuration file.
/// </summary>
/// <value>The system configuration file path.</value>
string SystemConfigurationFilePath { get; }
/// <summary>
/// Gets the folder path to the cache directory.
/// </summary>
/// <value>The cache directory.</value>
string CachePath { get; }
/// <summary>
/// Gets the folder path to the temp directory within the cache folder.
/// </summary>
/// <value>The temp directory.</value>
string TempDirectory { get; }
/// <summary>
/// Gets the magic string used for virtual path manipulation.
/// </summary>
/// <value>The magic string used for virtual path manipulation.</value>
string VirtualDataPath { get; }
/// <summary>
/// Gets the path used for storing trickplay files.
/// </summary>
/// <value>The trickplay path.</value>
string TrickplayPath { get; }
/// <summary>
/// Gets the path used for storing backup archives.
/// </summary>
/// <value>The backup path.</value>
string BackupPath { get; }
/// <summary>
/// Checks and creates all known base paths.
/// </summary>
void MakeSanityCheckOrThrow();
/// <summary>
/// Checks and creates the given path and adds it with a marker file if non existent.
/// </summary>
/// <param name="path">The path to check.</param>
/// <param name="markerName">The common marker file name.</param>
/// <param name="recursive">Check for other settings paths recursively.</param>
void CreateAndCheckMarker(string path, string markerName, bool recursive = false);
}
}
@@ -0,0 +1,17 @@
using System.Collections.Generic;
namespace MediaBrowser.Common.Configuration
{
/// <summary>
/// Provides an interface to retrieve a configuration store. Classes with this interface are scanned for at
/// application start to dynamically register configuration for various modules/plugins.
/// </summary>
public interface IConfigurationFactory
{
/// <summary>
/// Get the configuration store for this module.
/// </summary>
/// <returns>The configuration store.</returns>
IEnumerable<ConfigurationStore> GetConfigurations();
}
}
@@ -0,0 +1,97 @@
#pragma warning disable CS1591
using System;
using System.Collections.Generic;
using MediaBrowser.Model.Configuration;
namespace MediaBrowser.Common.Configuration
{
public interface IConfigurationManager
{
/// <summary>
/// Occurs when [configuration updating].
/// </summary>
event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdating;
/// <summary>
/// Occurs when [configuration updated].
/// </summary>
event EventHandler<EventArgs> ConfigurationUpdated;
/// <summary>
/// Occurs when [named configuration updated].
/// </summary>
event EventHandler<ConfigurationUpdateEventArgs> NamedConfigurationUpdated;
/// <summary>
/// Gets the application paths.
/// </summary>
/// <value>The application paths.</value>
IApplicationPaths CommonApplicationPaths { get; }
/// <summary>
/// Gets the configuration.
/// </summary>
/// <value>The configuration.</value>
BaseApplicationConfiguration CommonConfiguration { get; }
/// <summary>
/// Saves the configuration.
/// </summary>
void SaveConfiguration();
/// <summary>
/// Replaces the configuration.
/// </summary>
/// <param name="newConfiguration">The new configuration.</param>
void ReplaceConfiguration(BaseApplicationConfiguration newConfiguration);
/// <summary>
/// Manually pre-loads a factory so that it is available pre system initialisation.
/// </summary>
/// <typeparam name="T">Class to register.</typeparam>
void RegisterConfiguration<T>()
where T : IConfigurationFactory;
/// <summary>
/// Gets the configuration.
/// </summary>
/// <param name="key">The key.</param>
/// <returns>System.Object.</returns>
object GetConfiguration(string key);
/// <summary>
/// Gets the array of configuration stores.
/// </summary>
/// <returns>Array of ConfigurationStore.</returns>
ConfigurationStore[] GetConfigurationStores();
/// <summary>
/// Gets the type of the configuration.
/// </summary>
/// <param name="key">The key.</param>
/// <returns>Type.</returns>
Type GetConfigurationType(string key);
/// <summary>
/// Saves the configuration.
/// </summary>
/// <param name="key">The key.</param>
/// <param name="configuration">The configuration.</param>
void SaveConfiguration(string key, object configuration);
/// <summary>
/// Adds the parts.
/// </summary>
/// <param name="factories">The factories.</param>
void AddParts(IEnumerable<IConfigurationFactory> factories);
}
public static class ConfigurationManagerExtensions
{
public static T GetConfiguration<T>(this IConfigurationManager manager, string key)
{
return (T)manager.GetConfiguration(key);
}
}
}
@@ -0,0 +1,15 @@
namespace MediaBrowser.Common.Configuration
{
/// <summary>
/// A configuration store that can be validated.
/// </summary>
public interface IValidatingConfiguration
{
/// <summary>
/// Validation method to be invoked before saving the configuration.
/// </summary>
/// <param name="oldConfig">The old configuration.</param>
/// <param name="newConfig">The new configuration.</param>
void Validate(object oldConfig, object newConfig);
}
}
+89
View File
@@ -0,0 +1,89 @@
#pragma warning disable CS1591
using System;
namespace MediaBrowser.Common
{
public static class Crc32
{
private static readonly uint[] _crcTable =
{
0x00000000, 0x77073096, 0xee0e612c, 0x990951ba,
0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de,
0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940,
0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116,
0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a,
0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818,
0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c,
0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2,
0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086,
0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4,
0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe,
0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252,
0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60,
0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04,
0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e,
0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c,
0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0,
0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6,
0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d
};
public static uint Compute(ReadOnlySpan<byte> bytes)
{
var crc = 0xffffffff;
var len = bytes.Length;
for (var i = 0; i < len; i++)
{
crc = (crc >> 8) ^ _crcTable[(bytes[i] ^ crc) & 0xff];
}
return ~crc;
}
}
}
+64
View File
@@ -0,0 +1,64 @@
using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Common.Events
{
/// <summary>
/// Class EventHelper.
/// </summary>
// TODO: @bond Remove
public static class EventHelper
{
/// <summary>
/// Fires the event.
/// </summary>
/// <param name="handler">The handler.</param>
/// <param name="sender">The sender.</param>
/// <param name="args">The <see cref="EventArgs" /> instance containing the event data.</param>
/// <param name="logger">The logger.</param>
public static void QueueEventIfNotNull(EventHandler? handler, object sender, EventArgs args, ILogger logger)
{
if (handler is not null)
{
Task.Run(() =>
{
try
{
handler(sender, args);
}
catch (Exception ex)
{
logger.LogError(ex, "Error in event handler");
}
});
}
}
/// <summary>
/// Queues the event.
/// </summary>
/// <typeparam name="T">Argument type for the <c>handler</c>.</typeparam>
/// <param name="handler">The handler.</param>
/// <param name="sender">The sender.</param>
/// <param name="args">The args.</param>
/// <param name="logger">The logger.</param>
public static void QueueEventIfNotNull<T>(EventHandler<T>? handler, object sender, T args, ILogger logger)
{
if (handler is not null)
{
Task.Run(() =>
{
try
{
handler(sender, args);
}
catch (Exception ex)
{
logger.LogError(ex, "Error in event handler");
}
});
}
}
}
}
@@ -0,0 +1,37 @@
using System;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
namespace MediaBrowser.Common.Extensions
{
/// <summary>
/// Class BaseExtensions.
/// </summary>
public static partial class BaseExtensions
{
// http://stackoverflow.com/questions/1349023/how-can-i-strip-html-from-text-in-net
[GeneratedRegex(@"<(.|\n)*?>")]
private static partial Regex StripHtmlRegex();
/// <summary>
/// Strips the HTML.
/// </summary>
/// <param name="htmlString">The HTML string.</param>
/// <returns><see cref="string" />.</returns>
public static string StripHtml(this string htmlString)
=> StripHtmlRegex().Replace(htmlString, string.Empty).Trim();
/// <summary>
/// Gets the Md5.
/// </summary>
/// <param name="str">The string.</param>
/// <returns><see cref="Guid" />.</returns>
public static Guid GetMD5(this string str)
{
#pragma warning disable CA5351
return new Guid(MD5.HashData(Encoding.Unicode.GetBytes(str)));
#pragma warning restore CA5351
}
}
}
@@ -0,0 +1,41 @@
using System.Net;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Common.Extensions
{
/// <summary>
/// Static class containing extension methods for <see cref="HttpContext"/>.
/// </summary>
public static class HttpContextExtensions
{
/// <summary>
/// Checks the origin of the HTTP context.
/// </summary>
/// <param name="context">The incoming HTTP context.</param>
/// <returns><c>true</c> if the request is coming from the same machine as is running the server, <c>false</c> otherwise.</returns>
public static bool IsLocal(this HttpContext context)
{
return (context.Connection.LocalIpAddress is null
&& context.Connection.RemoteIpAddress is null)
|| Equals(context.Connection.LocalIpAddress, context.Connection.RemoteIpAddress);
}
/// <summary>
/// Extracts the remote IP address of the caller of the HTTP context.
/// </summary>
/// <param name="context">The HTTP context.</param>
/// <returns>The remote caller IP address.</returns>
public static IPAddress GetNormalizedRemoteIP(this HttpContext context)
{
// Default to the loopback address if no RemoteIpAddress is specified (i.e. during integration tests)
var ip = context.Connection.RemoteIpAddress ?? IPAddress.Loopback;
if (ip.IsIPv4MappedToIPv6)
{
ip = ip.MapToIPv4();
}
return ip;
}
}
}
@@ -0,0 +1,26 @@
using System;
namespace MediaBrowser.Common.Extensions
{
/// <summary>
/// Class MethodNotAllowedException.
/// </summary>
public class MethodNotAllowedException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="MethodNotAllowedException" /> class.
/// </summary>
public MethodNotAllowedException()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="MethodNotAllowedException" /> class.
/// </summary>
/// <param name="message">The message.</param>
public MethodNotAllowedException(string message)
: base(message)
{
}
}
}
@@ -0,0 +1,28 @@
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace MediaBrowser.Common.Extensions
{
/// <summary>
/// Extension methods for <see cref="Process"/>.
/// </summary>
public static class ProcessExtensions
{
/// <summary>
/// Asynchronously wait for the process to exit.
/// </summary>
/// <param name="process">The process to wait for.</param>
/// <param name="timeout">The duration to wait before cancelling waiting for the task.</param>
/// <returns>A task that will complete when the process has exited, cancellation has been requested, or an error occurs.</returns>
/// <exception cref="OperationCanceledException">The timeout ended.</exception>
public static async Task WaitForExitAsync(this Process process, TimeSpan timeout)
{
using (var cancelTokenSource = new CancellationTokenSource(timeout))
{
await process.WaitForExitAsync(cancelTokenSource.Token).ConfigureAwait(false);
}
}
}
}
@@ -0,0 +1,25 @@
#pragma warning disable CS1591
using System;
namespace MediaBrowser.Common.Extensions
{
public class RateLimitExceededException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="RateLimitExceededException" /> class.
/// </summary>
public RateLimitExceededException()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="RateLimitExceededException" /> class.
/// </summary>
/// <param name="message">The message.</param>
public RateLimitExceededException(string message)
: base(message)
{
}
}
}
@@ -0,0 +1,26 @@
using System;
namespace MediaBrowser.Common.Extensions
{
/// <summary>
/// Class ResourceNotFoundException.
/// </summary>
public class ResourceNotFoundException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="ResourceNotFoundException" /> class.
/// </summary>
public ResourceNotFoundException()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ResourceNotFoundException" /> class.
/// </summary>
/// <param name="message">The message.</param>
public ResourceNotFoundException(string message)
: base(message)
{
}
}
}
+39
View File
@@ -0,0 +1,39 @@
using System;
namespace MediaBrowser.Common
{
/// <summary>
/// Represents errors that occur during interaction with FFmpeg.
/// </summary>
public class FfmpegException : Exception
{
/// <summary>
/// Initializes a new instance of the <see cref="FfmpegException"/> class.
/// </summary>
public FfmpegException()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="FfmpegException"/> class with a specified error message.
/// </summary>
/// <param name="message">The message that describes the error.</param>
public FfmpegException(string message) : base(message)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="FfmpegException"/> class with a specified error message and a
/// reference to the inner exception that is the cause of this exception.
/// </summary>
/// <param name="message">The error message that explains the reason for the exception.</param>
/// <param name="innerException">
/// The exception that is the cause of the current exception, or a null reference (Nothing in Visual Basic) if
/// no inner exception is specified.
/// </param>
public FfmpegException(string message, Exception innerException)
: base(message, innerException)
{
}
}
}
+125
View File
@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common
{
/// <summary>
/// Delegate used with GetExports{T}.
/// </summary>
/// <param name="type">Type to create.</param>
/// <returns>New instance of type <param>type</param>.</returns>
public delegate object? CreationDelegateFactory(Type type);
/// <summary>
/// An interface to be implemented by the applications hosting a kernel.
/// </summary>
public interface IApplicationHost
{
/// <summary>
/// Occurs when [has pending restart changed].
/// </summary>
event EventHandler? HasPendingRestartChanged;
/// <summary>
/// Gets the name.
/// </summary>
/// <value>The name.</value>
string Name { get; }
/// <summary>
/// Gets the device identifier.
/// </summary>
/// <value>The device identifier.</value>
string SystemId { get; }
/// <summary>
/// Gets a value indicating whether this instance has pending changes requiring a restart.
/// </summary>
/// <value><c>true</c> if this instance has a pending restart; otherwise, <c>false</c>.</value>
bool HasPendingRestart { get; }
/// <summary>
/// Gets or sets a value indicating whether the application should restart.
/// </summary>
bool ShouldRestart { get; set; }
/// <summary>
/// Gets the application version.
/// </summary>
/// <value>The application version.</value>
Version ApplicationVersion { get; }
/// <summary>
/// Gets or sets the service provider.
/// </summary>
IServiceProvider? ServiceProvider { get; set; }
/// <summary>
/// Gets the application version.
/// </summary>
/// <value>The application version.</value>
string ApplicationVersionString { get; }
/// <summary>
/// Gets the application user agent.
/// </summary>
/// <value>The application user agent.</value>
string ApplicationUserAgent { get; }
/// <summary>
/// Gets the email address for use within a comment section of a user agent field.
/// Presently used to provide contact information to MusicBrainz service.
/// </summary>
string ApplicationUserAgentAddress { get; }
/// <summary>
/// Gets all plugin assemblies which implement a custom rest api.
/// </summary>
/// <returns>An <see cref="IEnumerable{Assembly}"/> containing the plugin assemblies.</returns>
IEnumerable<Assembly> GetApiPluginAssemblies();
/// <summary>
/// Notifies the pending restart.
/// </summary>
void NotifyPendingRestart();
/// <summary>
/// Gets the exports.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="manageLifetime">If set to <c>true</c> [manage lifetime].</param>
/// <returns><see cref="IReadOnlyCollection{T}" />.</returns>
IReadOnlyCollection<T> GetExports<T>(bool manageLifetime = true);
/// <summary>
/// Gets the exports.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="defaultFunc">Delegate function that gets called to create the object.</param>
/// <param name="manageLifetime">If set to <c>true</c> [manage lifetime].</param>
/// <returns><see cref="IReadOnlyCollection{T}" />.</returns>
IReadOnlyCollection<T> GetExports<T>(CreationDelegateFactory defaultFunc, bool manageLifetime = true);
/// <summary>
/// Gets the export types.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <returns>IEnumerable{Type}.</returns>
IEnumerable<Type> GetExportTypes<T>();
/// <summary>
/// Resolves this instance.
/// </summary>
/// <typeparam name="T">The <c>Type</c>.</typeparam>
/// <returns>``0.</returns>
T Resolve<T>();
/// <summary>
/// Initializes this instance.
/// </summary>
/// <param name="serviceCollection">Instance of the <see cref="IServiceCollection"/> interface.</param>
void Init(IServiceCollection serviceCollection);
}
}
@@ -0,0 +1,61 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- ProjectGuid is only included as a requirement for SonarQube analysis -->
<PropertyGroup>
<ProjectGuid>{9142EEFA-7570-41E1-BFCC-468BB571AF2F}</ProjectGuid>
</PropertyGroup>
<PropertyGroup>
<Authors>Jellyfin Contributors</Authors>
<PackageId>Jellyfin.Common</PackageId>
<VersionPrefix>10.12.0</VersionPrefix>
<RepositoryUrl>https://github.com/jellyfin/jellyfin</RepositoryUrl>
<PackageLicenseExpression>GPL-3.0-only</PackageLicenseExpression>
</PropertyGroup>
<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<ProjectReference Include="..\MediaBrowser.Model\MediaBrowser.Model.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\SharedVersion.cs" />
</ItemGroup>
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
</PropertyGroup>
<PropertyGroup Condition=" '$(Stability)'=='Unstable'">
<!-- Include all symbols in the main nupkg until Azure Artifact Feed starts supporting ingesting NuGet symbol packages. -->
<AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder>
</PropertyGroup>
<!-- Code Analyzers -->
<ItemGroup Condition=" '$(Configuration)' == 'Debug' ">
<PackageReference Include="IDisposableAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="SerilogAnalyzer" PrivateAssets="All" />
<PackageReference Include="StyleCop.Analyzers" PrivateAssets="All" />
<PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>Jellyfin.Common.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project>
+133
View File
@@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.NetworkInformation;
using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Http;
namespace MediaBrowser.Common.Net
{
/// <summary>
/// Interface for the NetworkManager class.
/// </summary>
public interface INetworkManager
{
/// <summary>
/// Event triggered on network changes.
/// </summary>
event EventHandler NetworkChanged;
/// <summary>
/// Gets a value indicating whether IPv4 is enabled.
/// </summary>
bool IsIPv4Enabled { get; }
/// <summary>
/// Gets a value indicating whether IPv6 is enabled.
/// </summary>
bool IsIPv6Enabled { get; }
/// <summary>
/// Calculates the list of interfaces to use for Kestrel.
/// </summary>
/// <returns>A IReadOnlyList{IPData} object containing all the interfaces to bind.
/// If all the interfaces are specified, and none are excluded, it returns zero items
/// to represent any address.</returns>
/// <param name="individualInterfaces">When false, return <see cref="IPAddress.Any"/> or <see cref="IPAddress.IPv6Any"/> for all interfaces.</param>
IReadOnlyList<IPData> GetAllBindInterfaces(bool individualInterfaces = false);
/// <summary>
/// Returns a list containing the loopback interfaces.
/// </summary>
/// <returns>IReadOnlyList{IPData}.</returns>
IReadOnlyList<IPData> GetLoopbacks();
/// <summary>
/// Retrieves the bind address to use in system URLs. (Server Discovery, PlayTo, LiveTV, SystemInfo)
/// If no bind addresses are specified, an internal interface address is selected.
/// The priority of selection is as follows:-
///
/// The value contained in the startup parameter --published-server-url.
///
/// If the user specified custom subnet overrides, the correct subnet for the source address.
///
/// If the user specified bind interfaces to use:-
/// The bind interface that contains the source subnet.
/// The first bind interface specified that suits best first the source's endpoint. eg. external or internal.
///
/// If the source is from a public subnet address range and the user hasn't specified any bind addresses:-
/// The first public interface that isn't a loopback and contains the source subnet.
/// The first public interface that isn't a loopback.
/// The first internal interface that isn't a loopback.
///
/// If the source is from a private subnet address range and the user hasn't specified any bind addresses:-
/// The first private interface that contains the source subnet.
/// The first private interface that isn't a loopback.
///
/// If no interfaces meet any of these criteria, then a loopback address is returned.
///
/// Interfaces that have been specifically excluded from binding are not used in any of the calculations.
/// </summary>
/// <param name="source">Source of the request.</param>
/// <param name="port">Optional port returned, if it's part of an override.</param>
/// <returns>IP address to use, or loopback address if all else fails.</returns>
string GetBindAddress(HttpRequest source, out int? port);
/// <summary>
/// Retrieves the bind address to use in system URLs. (Server Discovery, PlayTo, LiveTV, SystemInfo)
/// If no bind addresses are specified, an internal interface address is selected.
/// </summary>
/// <param name="source">IP address of the request.</param>
/// <param name="port">Optional port returned, if it's part of an override.</param>
/// <param name="skipOverrides">Optional boolean denoting if published server overrides should be ignored. Defaults to false.</param>
/// <returns>IP address to use, or loopback address if all else fails.</returns>
string GetBindAddress(IPAddress? source, out int? port, bool skipOverrides = false);
/// <summary>
/// Retrieves the bind address to use in system URLs. (Server Discovery, PlayTo, LiveTV, SystemInfo)
/// If no bind addresses are specified, an internal interface address is selected.
/// (See <see cref="GetBindAddress(IPAddress, out int?, bool)"/>.
/// </summary>
/// <param name="source">Source of the request.</param>
/// <param name="port">Optional port returned, if it's part of an override.</param>
/// <returns>IP address to use, or loopback address if all else fails.</returns>
string GetBindAddress(string source, out int? port);
/// <summary>
/// Returns true if the address is part of the user defined LAN.
/// </summary>
/// <param name="address">IP to check.</param>
/// <returns>True if endpoint is within the LAN range.</returns>
bool IsInLocalNetwork(string address);
/// <summary>
/// Returns true if the address is part of the user defined LAN.
/// </summary>
/// <param name="address">IP to check.</param>
/// <returns>True if endpoint is within the LAN range.</returns>
bool IsInLocalNetwork(IPAddress address);
/// <summary>
/// Attempts to convert the interface name to an IP address.
/// eg. "eth1", or "enp3s5".
/// </summary>
/// <param name="intf">Interface name.</param>
/// <param name="result">Resulting object's IP addresses, if successful.</param>
/// <returns>Success of the operation.</returns>
bool TryParseInterface(string intf, [NotNullWhen(true)] out IReadOnlyList<IPData>? result);
/// <summary>
/// Returns all internal (LAN) bind interface addresses.
/// </summary>
/// <returns>An list of internal (LAN) interfaces addresses.</returns>
IReadOnlyList<IPData> GetInternalBindAddresses();
/// <summary>
/// Checks if <paramref name="remoteIP"/> has access to the server.
/// </summary>
/// <param name="remoteIP">IP address of the client.</param>
/// <returns>The result of evaluating the access policy, <c>Allow</c> if it should be allowed.</returns>
RemoteAccessPolicyResult ShouldAllowServerAccess(IPAddress remoteIP);
}
}
+28
View File
@@ -0,0 +1,28 @@
namespace MediaBrowser.Common.Net
{
/// <summary>
/// Registered http client names.
/// </summary>
public static class NamedClient
{
/// <summary>
/// Gets the value for the default named http client which implements happy eyeballs.
/// </summary>
public const string Default = nameof(Default);
/// <summary>
/// Gets the value for the MusicBrainz named http client.
/// </summary>
public const string MusicBrainz = nameof(MusicBrainz);
/// <summary>
/// Gets the value for the DLNA named http client.
/// </summary>
public const string Dlna = nameof(Dlna);
/// <summary>
/// Non happy eyeballs implementation.
/// </summary>
public const string DirectIp = nameof(DirectIp);
}
}
@@ -0,0 +1,176 @@
#pragma warning disable CA1819 // Properties should not return arrays
using System;
namespace MediaBrowser.Common.Net;
/// <summary>
/// Defines the <see cref="NetworkConfiguration" />.
/// </summary>
public class NetworkConfiguration
{
/// <summary>
/// The default value for <see cref="InternalHttpPort"/>.
/// </summary>
public const int DefaultHttpPort = 8096;
/// <summary>
/// The default value for <see cref="PublicHttpsPort"/> and <see cref="InternalHttpsPort"/>.
/// </summary>
public const int DefaultHttpsPort = 8920;
private string _baseUrl = string.Empty;
/// <summary>
/// Gets or sets a value used to specify the URL prefix that your Jellyfin instance can be accessed at.
/// </summary>
public string BaseUrl
{
get => _baseUrl;
set
{
// Normalize the start of the string
if (string.IsNullOrWhiteSpace(value))
{
// If baseUrl is empty, set an empty prefix string
_baseUrl = string.Empty;
return;
}
if (value[0] != '/')
{
// If baseUrl was not configured with a leading slash, append one for consistency
value = "/" + value;
}
// Normalize the end of the string
if (value[^1] == '/')
{
// If baseUrl was configured with a trailing slash, remove it for consistency
value = value.Remove(value.Length - 1);
}
_baseUrl = value;
}
}
/// <summary>
/// Gets or sets a value indicating whether to use HTTPS.
/// </summary>
/// <remarks>
/// In order for HTTPS to be used, in addition to setting this to true, valid values must also be
/// provided for <see cref="CertificatePath"/> and <see cref="CertificatePassword"/>.
/// </remarks>
public bool EnableHttps { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the server should force connections over HTTPS.
/// </summary>
public bool RequireHttps { get; set; }
/// <summary>
/// Gets or sets the filesystem path of an X.509 certificate to use for SSL.
/// </summary>
public string CertificatePath { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the password required to access the X.509 certificate data in the file specified by <see cref="CertificatePath"/>.
/// </summary>
public string CertificatePassword { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the internal HTTP server port.
/// </summary>
/// <value>The HTTP server port.</value>
public int InternalHttpPort { get; set; } = DefaultHttpPort;
/// <summary>
/// Gets or sets the internal HTTPS server port.
/// </summary>
/// <value>The HTTPS server port.</value>
public int InternalHttpsPort { get; set; } = DefaultHttpsPort;
/// <summary>
/// Gets or sets the public HTTP port.
/// </summary>
/// <value>The public HTTP port.</value>
public int PublicHttpPort { get; set; } = DefaultHttpPort;
/// <summary>
/// Gets or sets the public HTTPS port.
/// </summary>
/// <value>The public HTTPS port.</value>
public int PublicHttpsPort { get; set; } = DefaultHttpsPort;
/// <summary>
/// Gets or sets a value indicating whether Autodiscovery is enabled.
/// </summary>
public bool AutoDiscovery { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether to enable automatic port forwarding.
/// </summary>
[Obsolete("No longer supported")]
public bool EnableUPnP { get; set; }
/// <summary>
/// Gets or sets a value indicating whether IPv6 is enabled.
/// </summary>
public bool EnableIPv4 { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether IPv6 is enabled.
/// </summary>
public bool EnableIPv6 { get; set; }
/// <summary>
/// Gets or sets a value indicating whether access from outside of the LAN is permitted.
/// </summary>
public bool EnableRemoteAccess { get; set; } = true;
/// <summary>
/// Gets or sets the subnets that are deemed to make up the LAN.
/// </summary>
public string[] LocalNetworkSubnets { get; set; } = Array.Empty<string>();
/// <summary>
/// Gets or sets the interface addresses which Jellyfin will bind to. If empty, all interfaces will be used.
/// </summary>
public string[] LocalNetworkAddresses { get; set; } = Array.Empty<string>();
/// <summary>
/// Gets or sets the known proxies.
/// </summary>
public string[] KnownProxies { get; set; } = Array.Empty<string>();
/// <summary>
/// Gets or sets a value indicating whether address names that match <see cref="VirtualInterfaceNames"/> should be ignored for the purposes of binding.
/// </summary>
public bool IgnoreVirtualInterfaces { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating the interface name prefixes that should be ignored. The list can be comma separated and values are case-insensitive. <seealso cref="IgnoreVirtualInterfaces"/>.
/// </summary>
public string[] VirtualInterfaceNames { get; set; } = new string[] { "veth" };
/// <summary>
/// Gets or sets a value indicating whether the published server uri is based on information in HTTP requests.
/// </summary>
public bool EnablePublishedServerUriByRequest { get; set; } = false;
/// <summary>
/// Gets or sets the PublishedServerUriBySubnet
/// Gets or sets PublishedServerUri to advertise for specific subnets.
/// </summary>
public string[] PublishedServerUriBySubnet { get; set; } = Array.Empty<string>();
/// <summary>
/// Gets or sets the filter for remote IP connectivity. Used in conjunction with <seealso cref="IsRemoteIPFilterBlacklist"/>.
/// </summary>
public string[] RemoteIPFilter { get; set; } = Array.Empty<string>();
/// <summary>
/// Gets or sets a value indicating whether <seealso cref="RemoteIPFilter"/> contains a blacklist or a whitelist. Default is a whitelist.
/// </summary>
public bool IsRemoteIPFilterBlacklist { get; set; }
}
@@ -0,0 +1,19 @@
using MediaBrowser.Common.Configuration;
namespace MediaBrowser.Common.Net;
/// <summary>
/// Defines the <see cref="NetworkConfigurationExtensions" />.
/// </summary>
public static class NetworkConfigurationExtensions
{
/// <summary>
/// Retrieves the network configuration.
/// </summary>
/// <param name="config">The <see cref="IConfigurationManager"/>.</param>
/// <returns>The <see cref="NetworkConfiguration"/>.</returns>
public static NetworkConfiguration GetNetworkConfiguration(this IConfigurationManager config)
{
return config.GetConfiguration<NetworkConfiguration>(NetworkConfigurationStore.StoreKey);
}
}
@@ -0,0 +1,22 @@
using System.Collections.Generic;
using MediaBrowser.Common.Configuration;
namespace MediaBrowser.Common.Net;
/// <summary>
/// Defines the <see cref="NetworkConfigurationFactory" />.
/// </summary>
public class NetworkConfigurationFactory : IConfigurationFactory
{
/// <summary>
/// The GetConfigurations.
/// </summary>
/// <returns>The <see cref="IEnumerable{ConfigurationStore}"/>.</returns>
public IEnumerable<ConfigurationStore> GetConfigurations()
{
return new[]
{
new NetworkConfigurationStore()
};
}
}
@@ -0,0 +1,23 @@
using MediaBrowser.Common.Configuration;
namespace MediaBrowser.Common.Net;
/// <summary>
/// A configuration that stores network related settings.
/// </summary>
public class NetworkConfigurationStore : ConfigurationStore
{
/// <summary>
/// The name of the configuration in the storage.
/// </summary>
public const string StoreKey = "network";
/// <summary>
/// Initializes a new instance of the <see cref="NetworkConfigurationStore"/> class.
/// </summary>
public NetworkConfigurationStore()
{
ConfigurationType = typeof(NetworkConfiguration);
Key = StoreKey;
}
}
@@ -0,0 +1,79 @@
using System.Net;
namespace MediaBrowser.Common.Net;
/// <summary>
/// Networking constants.
/// </summary>
public static class NetworkConstants
{
/// <summary>
/// IPv4 mask bytes.
/// </summary>
public const int IPv4MaskBytes = 4;
/// <summary>
/// IPv6 mask bytes.
/// </summary>
public const int IPv6MaskBytes = 16;
/// <summary>
/// Minimum IPv4 prefix size.
/// </summary>
public const int MinimumIPv4PrefixSize = 32;
/// <summary>
/// Minimum IPv6 prefix size.
/// </summary>
public const int MinimumIPv6PrefixSize = 128;
/// <summary>
/// Whole IPv4 address space.
/// </summary>
public static readonly IPNetwork IPv4Any = new IPNetwork(IPAddress.Any, 0);
/// <summary>
/// Whole IPv6 address space.
/// </summary>
public static readonly IPNetwork IPv6Any = new IPNetwork(IPAddress.IPv6Any, 0);
/// <summary>
/// IPv4 Loopback as defined in RFC 5735.
/// </summary>
public static readonly IPNetwork IPv4RFC5735Loopback = new IPNetwork(IPAddress.Loopback, 8);
/// <summary>
/// IPv4 private class A as defined in RFC 1918.
/// </summary>
public static readonly IPNetwork IPv4RFC1918PrivateClassA = new IPNetwork(IPAddress.Parse("10.0.0.0"), 8);
/// <summary>
/// IPv4 private class B as defined in RFC 1918.
/// </summary>
public static readonly IPNetwork IPv4RFC1918PrivateClassB = new IPNetwork(IPAddress.Parse("172.16.0.0"), 12);
/// <summary>
/// IPv4 private class C as defined in RFC 1918.
/// </summary>
public static readonly IPNetwork IPv4RFC1918PrivateClassC = new IPNetwork(IPAddress.Parse("192.168.0.0"), 16);
/// <summary>
/// IPv4 Link-Local as defined in RFC 3927.
/// </summary>
public static readonly IPNetwork IPv4RFC3927LinkLocal = new IPNetwork(IPAddress.Parse("169.254.0.0"), 16);
/// <summary>
/// IPv6 loopback as defined in RFC 4291.
/// </summary>
public static readonly IPNetwork IPv6RFC4291Loopback = new IPNetwork(IPAddress.IPv6Loopback, 128);
/// <summary>
/// IPv6 site local as defined in RFC 4291.
/// </summary>
public static readonly IPNetwork IPv6RFC4291SiteLocal = new IPNetwork(IPAddress.Parse("fe80::"), 10);
/// <summary>
/// IPv6 unique local as defined in RFC 4193.
/// </summary>
public static readonly IPNetwork IPv6RFC4193UniqueLocal = new IPNetwork(IPAddress.Parse("fc00::"), 7);
}
+360
View File
@@ -0,0 +1,360 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Net;
using System.Net.Sockets;
using System.Text.RegularExpressions;
using Jellyfin.Extensions;
using MediaBrowser.Model.Net;
namespace MediaBrowser.Common.Net;
/// <summary>
/// Defines the <see cref="NetworkUtils" />.
/// </summary>
public static partial class NetworkUtils
{
// Use regular expression as CheckHostName isn't RFC5892 compliant.
// Modified from gSkinner's expression at https://stackoverflow.com/questions/11809631/fully-qualified-domain-name-validation
[GeneratedRegex(@"(?im)^(?!:\/\/)(?=.{1,255}$)((.{1,63}\.){0,127}(?![0-9]*$)[a-z0-9-]+\.?)(:(\d){1,5}){0,1}$", RegexOptions.IgnoreCase, "en-US")]
private static partial Regex FqdnGeneratedRegex();
/// <summary>
/// Returns true if the IPAddress contains an IP6 Local link address.
/// </summary>
/// <param name="address">IPAddress object to check.</param>
/// <returns>True if it is a local link address.</returns>
/// <remarks>
/// See https://stackoverflow.com/questions/6459928/explain-the-instance-properties-of-system-net-ipaddress
/// it appears that the IPAddress.IsIPv6LinkLocal is out of date.
/// </remarks>
public static bool IsIPv6LinkLocal(IPAddress address)
{
ArgumentNullException.ThrowIfNull(address);
if (address.IsIPv4MappedToIPv6)
{
address = address.MapToIPv4();
}
if (address.AddressFamily != AddressFamily.InterNetworkV6)
{
return false;
}
// GetAddressBytes
Span<byte> octet = stackalloc byte[16];
address.TryWriteBytes(octet, out _);
uint word = (uint)(octet[0] << 8) + octet[1];
return word >= 0xfe80 && word <= 0xfebf; // fe80::/10 :Local link.
}
/// <summary>
/// Convert a subnet mask in CIDR notation to a dotted decimal string value. IPv4 only.
/// </summary>
/// <param name="cidr">Subnet mask in CIDR notation.</param>
/// <param name="family">IPv4 or IPv6 family.</param>
/// <returns>String value of the subnet mask in dotted decimal notation.</returns>
public static IPAddress CidrToMask(byte cidr, AddressFamily family)
{
uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize) - cidr);
addr = ((addr & 0xff000000) >> 24)
| ((addr & 0x00ff0000) >> 8)
| ((addr & 0x0000ff00) << 8)
| ((addr & 0x000000ff) << 24);
return new IPAddress(addr);
}
/// <summary>
/// Convert a subnet mask in CIDR notation to a dotted decimal string value. IPv4 only.
/// </summary>
/// <param name="cidr">Subnet mask in CIDR notation.</param>
/// <param name="family">IPv4 or IPv6 family.</param>
/// <returns>String value of the subnet mask in dotted decimal notation.</returns>
public static IPAddress CidrToMask(int cidr, AddressFamily family)
{
uint addr = 0xFFFFFFFF << ((family == AddressFamily.InterNetwork ? NetworkConstants.MinimumIPv4PrefixSize : NetworkConstants.MinimumIPv6PrefixSize) - cidr);
addr = ((addr & 0xff000000) >> 24)
| ((addr & 0x00ff0000) >> 8)
| ((addr & 0x0000ff00) << 8)
| ((addr & 0x000000ff) << 24);
return new IPAddress(addr);
}
/// <summary>
/// Convert a subnet mask to a CIDR. IPv4 only.
/// https://stackoverflow.com/questions/36954345/get-cidr-from-netmask.
/// </summary>
/// <param name="mask">Subnet mask.</param>
/// <returns>Byte CIDR representing the mask.</returns>
public static byte MaskToCidr(IPAddress mask)
{
ArgumentNullException.ThrowIfNull(mask);
byte cidrnet = 0;
if (mask.Equals(IPAddress.Any))
{
return cidrnet;
}
// GetAddressBytes
Span<byte> bytes = stackalloc byte[mask.AddressFamily == AddressFamily.InterNetwork ? NetworkConstants.IPv4MaskBytes : NetworkConstants.IPv6MaskBytes];
if (!mask.TryWriteBytes(bytes, out var bytesWritten))
{
Console.WriteLine("Unable to write address bytes, only {0} bytes written.", bytesWritten.ToString(CultureInfo.InvariantCulture));
}
var zeroed = false;
for (var i = 0; i < bytes.Length; i++)
{
for (int v = bytes[i]; (v & 0xFF) != 0; v <<= 1)
{
if (zeroed)
{
// Invalid netmask.
return (byte)~cidrnet;
}
if ((v & 0x80) == 0)
{
zeroed = true;
}
else
{
cidrnet++;
}
}
}
return cidrnet;
}
/// <summary>
/// Converts an IPAddress into a string.
/// IPv6 addresses are returned in [ ], with their scope removed.
/// </summary>
/// <param name="address">Address to convert.</param>
/// <returns>URI safe conversion of the address.</returns>
public static string FormatIPString(IPAddress? address)
{
if (address is null)
{
return string.Empty;
}
var str = address.ToString();
if (address.AddressFamily == AddressFamily.InterNetworkV6)
{
int i = str.IndexOf('%', StringComparison.Ordinal);
if (i != -1)
{
str = str.Substring(0, i);
}
return $"[{str}]";
}
return str;
}
/// <summary>
/// Try parsing an array of strings into <see cref="IPNetwork"/> objects, respecting exclusions.
/// Elements without a subnet mask will be represented as <see cref="IPNetwork"/> with a single IP.
/// </summary>
/// <param name="values">Input string array to be parsed.</param>
/// <param name="result">Collection of <see cref="IPNetwork"/>.</param>
/// <param name="negated">Boolean signaling if negated or not negated values should be parsed.</param>
/// <returns><c>True</c> if parsing was successful.</returns>
public static bool TryParseToSubnets(string[] values, [NotNullWhen(true)] out IReadOnlyList<IPData>? result, bool negated = false)
{
if (values is null || values.Length == 0)
{
result = null;
return false;
}
List<IPData>? tmpResult = null;
for (int a = 0; a < values.Length; a++)
{
if (TryParseToSubnet(values[a], out var innerResult, negated))
{
(tmpResult ??= new()).Add(innerResult);
}
}
result = tmpResult;
return result is not null;
}
/// <summary>
/// Try parsing a string into an <see cref="IPData"/>, respecting exclusions.
/// Inputs without a subnet mask will be represented as <see cref="IPData"/> with a single IP.
/// </summary>
/// <param name="value">Input string to be parsed.</param>
/// <param name="result">An <see cref="IPData"/>.</param>
/// <param name="negated">Boolean signaling if negated or not negated values should be parsed.</param>
/// <returns><c>True</c> if parsing was successful.</returns>
public static bool TryParseToSubnet(ReadOnlySpan<char> value, [NotNullWhen(true)] out IPData? result, bool negated = false)
{
// If multiple IP addresses are in a comma-separated string, the individual addresses may contain leading and/or trailing whitespace
value = value.Trim();
bool isAddressNegated = false;
if (value.StartsWith('!'))
{
isAddressNegated = true;
value = value[1..]; // Remove leading '!' character
}
if (isAddressNegated != negated)
{
result = default;
return false;
}
var index = value.IndexOf('/');
if (index != -1)
{
if (IPAddress.TryParse(value[..index], out var address) && IPNetwork.TryParse(value, out var subnet))
{
result = new IPData(address, subnet);
return true;
}
}
else if (IPAddress.TryParse(value, out var address))
{
if (address.AddressFamily == AddressFamily.InterNetwork)
{
result = address.Equals(IPAddress.Any) ? new IPData(IPAddress.Any, NetworkConstants.IPv4Any) : new IPData(address, new IPNetwork(address, NetworkConstants.MinimumIPv4PrefixSize));
return true;
}
else if (address.AddressFamily == AddressFamily.InterNetworkV6)
{
result = address.Equals(IPAddress.IPv6Any) ? new IPData(IPAddress.IPv6Any, NetworkConstants.IPv6Any) : new IPData(address, new IPNetwork(address, NetworkConstants.MinimumIPv6PrefixSize));
return true;
}
}
result = default;
return false;
}
/// <summary>
/// Attempts to parse a host span.
/// </summary>
/// <param name="host">Host name to parse.</param>
/// <param name="addresses">Object representing the span, if it has successfully been parsed.</param>
/// <param name="isIPv4Enabled"><c>true</c> if IPv4 is enabled.</param>
/// <param name="isIPv6Enabled"><c>true</c> if IPv6 is enabled.</param>
/// <returns><c>true</c> if the parsing is successful, <c>false</c> if not.</returns>
public static bool TryParseHost(ReadOnlySpan<char> host, [NotNullWhen(true)] out IPAddress[]? addresses, bool isIPv4Enabled = true, bool isIPv6Enabled = false)
{
host = host.Trim();
if (host.IsEmpty)
{
addresses = null;
return false;
}
// See if it's an IPv6 with port address e.g. [::1] or [::1]:120.
if (host[0] == '[')
{
int i = host.IndexOf(']');
if (i != -1)
{
return TryParseHost(host[1..(i - 1)], out addresses);
}
addresses = Array.Empty<IPAddress>();
return false;
}
var hosts = new List<string>();
foreach (var splitSpan in host.Split(':'))
{
hosts.Add(splitSpan.ToString());
}
if (hosts.Count <= 2)
{
var firstPart = hosts[0];
// Is hostname or hostname:port
if (FqdnGeneratedRegex().IsMatch(firstPart))
{
try
{
// .NET automatically filters only supported returned addresses based on OS support.
addresses = Dns.GetHostAddresses(firstPart);
return true;
}
catch (SocketException)
{
// Ignore socket errors, as the result value will just be an empty array.
}
}
// Is an IPv4 or IPv4:port
if (IPAddress.TryParse(firstPart.AsSpan().LeftPart('/'), out var address))
{
if (((address.AddressFamily == AddressFamily.InterNetwork) && (!isIPv4Enabled && isIPv6Enabled))
|| ((address.AddressFamily == AddressFamily.InterNetworkV6) && (isIPv4Enabled && !isIPv6Enabled)))
{
addresses = Array.Empty<IPAddress>();
return false;
}
addresses = new[] { address };
// Host name is an IPv4 address, so fake resolve.
return true;
}
}
else if (hosts.Count > 0 && hosts.Count <= 9) // 8 octets + port
{
if (IPAddress.TryParse(host.LeftPart('/'), out var address))
{
addresses = new[] { address };
return true;
}
}
addresses = Array.Empty<IPAddress>();
return false;
}
/// <summary>
/// Gets the broadcast address for a <see cref="IPNetwork"/>.
/// </summary>
/// <param name="network">The <see cref="IPNetwork"/>.</param>
/// <returns>The broadcast address.</returns>
public static IPAddress GetBroadcastAddress(IPNetwork network)
{
var addressBytes = network.BaseAddress.GetAddressBytes();
uint ipAddress = BitConverter.ToUInt32(addressBytes, 0);
uint ipMaskV4 = BitConverter.ToUInt32(CidrToMask(network.PrefixLength, AddressFamily.InterNetwork).GetAddressBytes(), 0);
uint broadCastIPAddress = ipAddress | ~ipMaskV4;
return new IPAddress(BitConverter.GetBytes(broadCastIPAddress));
}
/// <summary>
/// Check if a subnet contains an address. This method also handles IPv4 mapped to IPv6 addresses.
/// </summary>
/// <param name="network">The <see cref="IPNetwork"/>.</param>
/// <param name="address">The <see cref="IPAddress"/>.</param>
/// <returns>Whether the supplied IP is in the supplied network.</returns>
public static bool SubnetContainsAddress(IPNetwork network, IPAddress address)
{
ArgumentNullException.ThrowIfNull(address);
if (address.IsIPv4MappedToIPv6)
{
address = address.MapToIPv4();
}
return network.Contains(address);
}
}
@@ -0,0 +1,29 @@
using System;
namespace MediaBrowser.Common.Net;
/// <summary>
/// Result of <see cref="INetworkManager.ShouldAllowServerAccess" />.
/// </summary>
public enum RemoteAccessPolicyResult
{
/// <summary>
/// The connection should be allowed.
/// </summary>
Allow,
/// <summary>
/// The connection should be rejected since it is not from a local IP and remote access is disabled.
/// </summary>
RejectDueToRemoteAccessDisabled,
/// <summary>
/// The connection should be rejected since it is from a blocklisted IP.
/// </summary>
RejectDueToIPBlocklist,
/// <summary>
/// The connection should be rejected since it is from a remote IP that is not in the allowlist.
/// </summary>
RejectDueToNotAllowlistedRemoteIP,
}
+94
View File
@@ -0,0 +1,94 @@
#nullable disable
using System;
using System.IO;
using System.Reflection;
using MediaBrowser.Model.Plugins;
namespace MediaBrowser.Common.Plugins
{
/// <summary>
/// Provides a common base class for all plugins.
/// </summary>
public abstract class BasePlugin : IPlugin, IPluginAssembly
{
/// <summary>
/// Gets the name of the plugin.
/// </summary>
/// <value>The name.</value>
public abstract string Name { get; }
/// <summary>
/// Gets the description.
/// </summary>
/// <value>The description.</value>
public virtual string Description => string.Empty;
/// <summary>
/// Gets the unique id.
/// </summary>
/// <value>The unique id.</value>
public virtual Guid Id { get; private set; }
/// <summary>
/// Gets the plugin version.
/// </summary>
/// <value>The version.</value>
public Version Version { get; private set; }
/// <summary>
/// Gets the path to the assembly file.
/// </summary>
/// <value>The assembly file path.</value>
public string AssemblyFilePath { get; private set; }
/// <summary>
/// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed.
/// </summary>
/// <value>The data folder path.</value>
public string DataFolderPath { get; private set; }
/// <summary>
/// Gets a value indicating whether the plugin can be uninstalled.
/// </summary>
public bool CanUninstall => !Path.GetDirectoryName(AssemblyFilePath)
.Equals(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), StringComparison.Ordinal);
/// <summary>
/// Gets the plugin info.
/// </summary>
/// <returns>PluginInfo.</returns>
public virtual PluginInfo GetPluginInfo()
{
var info = new PluginInfo(
Name,
Version,
Description,
Id,
CanUninstall);
return info;
}
/// <summary>
/// Called just before the plugin is uninstalled from the server.
/// </summary>
public virtual void OnUninstalling()
{
}
/// <inheritdoc />
public void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion)
{
AssemblyFilePath = assemblyFilePath;
DataFolderPath = dataFolderPath;
Version = assemblyVersion;
}
/// <inheritdoc />
public void SetId(Guid assemblyId)
{
Id = assemblyId;
}
}
}
@@ -0,0 +1,200 @@
#nullable disable
#pragma warning disable SA1649 // File name should match first type name
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Serialization;
namespace MediaBrowser.Common.Plugins
{
/// <summary>
/// Provides a common base class for all plugins.
/// </summary>
/// <typeparam name="TConfigurationType">The type of the T configuration type.</typeparam>
public abstract class BasePlugin<TConfigurationType> : BasePlugin, IHasPluginConfiguration
where TConfigurationType : BasePluginConfiguration
{
/// <summary>
/// The configuration sync lock.
/// </summary>
private readonly Lock _configurationSyncLock = new();
/// <summary>
/// The configuration save lock.
/// </summary>
private readonly Lock _configurationSaveLock = new();
/// <summary>
/// The configuration.
/// </summary>
private TConfigurationType _configuration;
/// <summary>
/// Initializes a new instance of the <see cref="BasePlugin{TConfigurationType}" /> class.
/// </summary>
/// <param name="applicationPaths">The application paths.</param>
/// <param name="xmlSerializer">The XML serializer.</param>
protected BasePlugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer)
{
ApplicationPaths = applicationPaths;
XmlSerializer = xmlSerializer;
var assembly = GetType().Assembly;
var assemblyName = assembly.GetName();
var assemblyFilePath = assembly.Location;
var dataFolderPath = Path.Combine(ApplicationPaths.PluginsPath, Path.GetFileNameWithoutExtension(assemblyFilePath));
if (Version is not null && !Directory.Exists(dataFolderPath))
{
// Try again with the version number appended to the folder name.
dataFolderPath += "_" + Version;
}
SetAttributes(assemblyFilePath, dataFolderPath, assemblyName.Version);
var idAttributes = assembly.GetCustomAttributes(typeof(GuidAttribute), true);
if (idAttributes.Length > 0)
{
var attribute = (GuidAttribute)idAttributes[0];
var assemblyId = new Guid(attribute.Value);
SetId(assemblyId);
}
}
/// <summary>
/// Gets the application paths.
/// </summary>
/// <value>The application paths.</value>
protected IApplicationPaths ApplicationPaths { get; private set; }
/// <summary>
/// Gets the XML serializer.
/// </summary>
/// <value>The XML serializer.</value>
protected IXmlSerializer XmlSerializer { get; private set; }
/// <summary>
/// Gets the type of configuration this plugin uses.
/// </summary>
/// <value>The type of the configuration.</value>
public Type ConfigurationType => typeof(TConfigurationType);
/// <summary>
/// Gets or sets the event handler that is triggered when this configuration changes.
/// </summary>
public EventHandler<BasePluginConfiguration> ConfigurationChanged { get; set; }
/// <summary>
/// Gets the name the assembly file.
/// </summary>
/// <value>The name of the assembly file.</value>
protected string AssemblyFileName => Path.GetFileName(AssemblyFilePath);
/// <summary>
/// Gets or sets the plugin configuration.
/// </summary>
/// <value>The configuration.</value>
public TConfigurationType Configuration
{
get
{
// Lazy load
if (_configuration is null)
{
lock (_configurationSyncLock)
{
_configuration ??= LoadConfiguration();
}
}
return _configuration;
}
protected set => _configuration = value;
}
/// <summary>
/// Gets the name of the configuration file. Subclasses should override.
/// </summary>
/// <value>The name of the configuration file.</value>
public virtual string ConfigurationFileName => Path.ChangeExtension(AssemblyFileName, ".xml");
/// <summary>
/// Gets the full path to the configuration file.
/// </summary>
/// <value>The configuration file path.</value>
public string ConfigurationFilePath => Path.Combine(ApplicationPaths.PluginConfigurationsPath, ConfigurationFileName);
/// <summary>
/// Gets the plugin configuration.
/// </summary>
/// <value>The configuration.</value>
BasePluginConfiguration IHasPluginConfiguration.Configuration => Configuration;
/// <summary>
/// Saves the current configuration to the file system.
/// </summary>
/// <param name="config">Configuration to save.</param>
public virtual void SaveConfiguration(TConfigurationType config)
{
lock (_configurationSaveLock)
{
var folder = Path.GetDirectoryName(ConfigurationFilePath);
Directory.CreateDirectory(folder);
XmlSerializer.SerializeToFile(config, ConfigurationFilePath);
}
}
/// <summary>
/// Saves the current configuration to the file system.
/// </summary>
public virtual void SaveConfiguration()
{
SaveConfiguration(Configuration);
}
/// <inheritdoc />
public virtual void UpdateConfiguration(BasePluginConfiguration configuration)
{
ArgumentNullException.ThrowIfNull(configuration);
Configuration = (TConfigurationType)configuration;
SaveConfiguration(Configuration);
ConfigurationChanged?.Invoke(this, configuration);
}
/// <inheritdoc />
public override PluginInfo GetPluginInfo()
{
var info = base.GetPluginInfo();
info.ConfigurationFileName = ConfigurationFileName;
return info;
}
private TConfigurationType LoadConfiguration()
{
var path = ConfigurationFilePath;
try
{
return (TConfigurationType)XmlSerializer.DeserializeFromFile(typeof(TConfigurationType), path);
}
catch
{
var config = Activator.CreateInstance<TConfigurationType>();
SaveConfiguration(config);
return config;
}
}
}
}
@@ -0,0 +1,27 @@
using System;
using MediaBrowser.Model.Plugins;
namespace MediaBrowser.Common.Plugins
{
/// <summary>
/// Defines the <see cref="IHasPluginConfiguration" />.
/// </summary>
public interface IHasPluginConfiguration
{
/// <summary>
/// Gets the type of configuration this plugin uses.
/// </summary>
Type ConfigurationType { get; }
/// <summary>
/// Gets the plugin's configuration.
/// </summary>
BasePluginConfiguration Configuration { get; }
/// <summary>
/// Completely overwrites the current configuration with a new copy.
/// </summary>
/// <param name="configuration">The configuration.</param>
void UpdateConfiguration(BasePluginConfiguration configuration);
}
}
+59
View File
@@ -0,0 +1,59 @@
#nullable disable
using System;
using MediaBrowser.Model.Plugins;
namespace MediaBrowser.Common.Plugins
{
/// <summary>
/// Defines the <see cref="IPlugin" />.
/// </summary>
public interface IPlugin
{
/// <summary>
/// Gets the name of the plugin.
/// </summary>
string Name { get; }
/// <summary>
/// Gets the Description.
/// </summary>
string Description { get; }
/// <summary>
/// Gets the unique id.
/// </summary>
Guid Id { get; }
/// <summary>
/// Gets the plugin version.
/// </summary>
Version Version { get; }
/// <summary>
/// Gets the path to the assembly file.
/// </summary>
string AssemblyFilePath { get; }
/// <summary>
/// Gets a value indicating whether the plugin can be uninstalled.
/// </summary>
bool CanUninstall { get; }
/// <summary>
/// Gets the full path to the data folder, where the plugin can store any miscellaneous files needed.
/// </summary>
string DataFolderPath { get; }
/// <summary>
/// Gets the <see cref="PluginInfo"/>.
/// </summary>
/// <returns>PluginInfo.</returns>
PluginInfo GetPluginInfo();
/// <summary>
/// Called when just before the plugin is uninstalled from the server.
/// </summary>
void OnUninstalling();
}
}
@@ -0,0 +1,13 @@
#pragma warning disable CS1591
using System;
namespace MediaBrowser.Common.Plugins
{
public interface IPluginAssembly
{
void SetAttributes(string assemblyFilePath, string dataFolderPath, Version assemblyVersion);
void SetId(Guid assemblyId);
}
}
@@ -0,0 +1,96 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using MediaBrowser.Model.Plugins;
using MediaBrowser.Model.Updates;
using Microsoft.Extensions.DependencyInjection;
namespace MediaBrowser.Common.Plugins
{
/// <summary>
/// Defines the <see cref="IPluginManager" />.
/// </summary>
public interface IPluginManager
{
/// <summary>
/// Gets the Plugins.
/// </summary>
IReadOnlyList<LocalPlugin> Plugins { get; }
/// <summary>
/// Creates the plugins.
/// </summary>
void CreatePlugins();
/// <summary>
/// Returns all the assemblies.
/// </summary>
/// <returns>An IEnumerable{Assembly}.</returns>
IEnumerable<Assembly> LoadAssemblies();
/// <summary>
/// Registers the plugin's services with the DI.
/// Note: DI is not yet instantiated yet.
/// </summary>
/// <param name="serviceCollection">A <see cref="ServiceCollection"/> instance.</param>
void RegisterServices(IServiceCollection serviceCollection);
/// <summary>
/// Saves the manifest back to disk.
/// </summary>
/// <param name="manifest">The <see cref="PluginManifest"/> to save.</param>
/// <param name="path">The path where to save the manifest.</param>
/// <returns>True if successful.</returns>
bool SaveManifest(PluginManifest manifest, string path);
/// <summary>
/// Generates a manifest from repository data.
/// </summary>
/// <param name="packageInfo">The <see cref="PackageInfo"/> used to generate a manifest.</param>
/// <param name="version">Version to be installed.</param>
/// <param name="path">The path where to save the manifest.</param>
/// <param name="status">Initial status of the plugin.</param>
/// <returns>True if successful.</returns>
Task<bool> PopulateManifest(PackageInfo packageInfo, Version version, string path, PluginStatus status);
/// <summary>
/// Imports plugin details from a folder.
/// </summary>
/// <param name="folder">Folder of the plugin.</param>
void ImportPluginFrom(string folder);
/// <summary>
/// Disable the plugin.
/// </summary>
/// <param name="assembly">The <see cref="Assembly"/> of the plug to disable.</param>
void FailPlugin(Assembly assembly);
/// <summary>
/// Disable the plugin.
/// </summary>
/// <param name="plugin">The <see cref="LocalPlugin"/> of the plug to disable.</param>
void DisablePlugin(LocalPlugin plugin);
/// <summary>
/// Enables the plugin, disabling all other versions.
/// </summary>
/// <param name="plugin">The <see cref="LocalPlugin"/> of the plug to disable.</param>
void EnablePlugin(LocalPlugin plugin);
/// <summary>
/// Attempts to find the plugin with and id of <paramref name="id"/>.
/// </summary>
/// <param name="id">Id of plugin.</param>
/// <param name="version">The version of the plugin to locate.</param>
/// <returns>A <see cref="LocalPlugin"/> if located, or null if not.</returns>
LocalPlugin? GetPlugin(Guid id, Version? version = null);
/// <summary>
/// Removes the plugin.
/// </summary>
/// <param name="plugin">The plugin.</param>
/// <returns>Outcome of the operation.</returns>
bool RemovePlugin(LocalPlugin plugin);
}
}
+139
View File
@@ -0,0 +1,139 @@
using System;
using System.Collections.Generic;
using MediaBrowser.Model.Plugins;
namespace MediaBrowser.Common.Plugins
{
/// <summary>
/// Local plugin class.
/// </summary>
public class LocalPlugin : IEquatable<LocalPlugin>
{
private readonly bool _supported;
private Version? _version;
/// <summary>
/// Initializes a new instance of the <see cref="LocalPlugin"/> class.
/// </summary>
/// <param name="path">The plugin path.</param>
/// <param name="isSupported"><b>True</b> if Jellyfin supports this version of the plugin.</param>
/// <param name="manifest">The manifest record for this plugin, or null if one does not exist.</param>
public LocalPlugin(string path, bool isSupported, PluginManifest manifest)
{
Path = path;
DllFiles = Array.Empty<string>();
_supported = isSupported;
Manifest = manifest;
}
/// <summary>
/// Gets the plugin id.
/// </summary>
public Guid Id => Manifest.Id;
/// <summary>
/// Gets the plugin name.
/// </summary>
public string Name => Manifest.Name;
/// <summary>
/// Gets the plugin version.
/// </summary>
public Version Version
{
get
{
if (_version is null)
{
_version = Version.Parse(Manifest.Version);
}
return _version;
}
}
/// <summary>
/// Gets the plugin path.
/// </summary>
public string Path { get; }
/// <summary>
/// Gets or sets the list of dll files for this plugin.
/// </summary>
public IReadOnlyList<string> DllFiles { get; set; }
/// <summary>
/// Gets or sets the instance of this plugin.
/// </summary>
public IPlugin? Instance { get; set; }
/// <summary>
/// Gets a value indicating whether Jellyfin supports this version of the plugin, and it's enabled.
/// </summary>
public bool IsEnabledAndSupported => _supported && Manifest.Status >= PluginStatus.Active;
/// <summary>
/// Gets a value indicating whether the plugin has a manifest.
/// </summary>
public PluginManifest Manifest { get; }
/// <summary>
/// Compare two <see cref="LocalPlugin"/>.
/// </summary>
/// <param name="a">The first item.</param>
/// <param name="b">The second item.</param>
/// <returns>Comparison result.</returns>
public static int Compare(LocalPlugin a, LocalPlugin b)
{
if (a is null || b is null)
{
throw new ArgumentNullException(a is null ? nameof(a) : nameof(b));
}
var compare = string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase);
// Id is not equal but name is.
if (!a.Id.Equals(b.Id) && compare == 0)
{
compare = a.Id.CompareTo(b.Id);
}
return compare == 0 ? a.Version.CompareTo(b.Version) : compare;
}
/// <summary>
/// Returns the plugin information.
/// </summary>
/// <returns>A <see cref="PluginInfo"/> instance containing the information.</returns>
public PluginInfo GetPluginInfo()
{
var inst = Instance?.GetPluginInfo() ?? new PluginInfo(Manifest.Name, Version, Manifest.Description, Manifest.Id, true);
inst.Status = Manifest.Status;
inst.HasImage = !string.IsNullOrEmpty(Manifest.ImagePath);
return inst;
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
return obj is LocalPlugin other && this.Equals(other);
}
/// <inheritdoc />
public override int GetHashCode()
{
return Name.GetHashCode(StringComparison.OrdinalIgnoreCase);
}
/// <inheritdoc />
public bool Equals(LocalPlugin? other)
{
if (other is null)
{
return false;
}
return Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase) && Id.Equals(other.Id) && Version.Equals(other.Version);
}
}
}
@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using MediaBrowser.Model.Plugins;
namespace MediaBrowser.Common.Plugins
{
/// <summary>
/// Defines a Plugin manifest file.
/// </summary>
public class PluginManifest
{
/// <summary>
/// Initializes a new instance of the <see cref="PluginManifest"/> class.
/// </summary>
public PluginManifest()
{
Category = string.Empty;
Changelog = string.Empty;
Description = string.Empty;
Id = Guid.Empty;
Name = string.Empty;
Owner = string.Empty;
Overview = string.Empty;
TargetAbi = string.Empty;
Version = string.Empty;
Assemblies = Array.Empty<string>();
}
/// <summary>
/// Gets or sets the category of the plugin.
/// </summary>
[JsonPropertyName("category")]
public string Category { get; set; }
/// <summary>
/// Gets or sets the changelog information.
/// </summary>
[JsonPropertyName("changelog")]
public string Changelog { get; set; }
/// <summary>
/// Gets or sets the description of the plugin.
/// </summary>
[JsonPropertyName("description")]
public string Description { get; set; }
/// <summary>
/// Gets or sets the Global Unique Identifier for the plugin.
/// </summary>
[JsonPropertyName("guid")]
public Guid Id { get; set; }
/// <summary>
/// Gets or sets the Name of the plugin.
/// </summary>
[JsonPropertyName("name")]
public string Name { get; set; }
/// <summary>
/// Gets or sets an overview of the plugin.
/// </summary>
[JsonPropertyName("overview")]
public string Overview { get; set; }
/// <summary>
/// Gets or sets the owner of the plugin.
/// </summary>
[JsonPropertyName("owner")]
public string Owner { get; set; }
/// <summary>
/// Gets or sets the compatibility version for the plugin.
/// </summary>
[JsonPropertyName("targetAbi")]
public string TargetAbi { get; set; }
/// <summary>
/// Gets or sets the timestamp of the plugin.
/// </summary>
[JsonPropertyName("timestamp")]
public DateTime Timestamp { get; set; }
/// <summary>
/// Gets or sets the Version number of the plugin.
/// </summary>
[JsonPropertyName("version")]
public string Version { get; set; }
/// <summary>
/// Gets or sets a value indicating the operational status of this plugin.
/// </summary>
[JsonPropertyName("status")]
public PluginStatus Status { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this plugin should automatically update.
/// </summary>
[JsonPropertyName("autoUpdate")]
public bool AutoUpdate { get; set; } = true; // DO NOT MOVE THIS INTO THE CONSTRUCTOR.
/// <summary>
/// Gets or sets the ImagePath
/// Gets or sets a value indicating whether this plugin has an image.
/// Image must be located in the local plugin folder.
/// </summary>
[JsonPropertyName("imagePath")]
public string? ImagePath { get; set; }
/// <summary>
/// Gets or sets the collection of assemblies that should be loaded.
/// Paths are considered relative to the plugin folder.
/// </summary>
[JsonPropertyName("assemblies")]
public IReadOnlyList<string> Assemblies { get; set; }
}
}
@@ -0,0 +1,21 @@
using System.Reflection;
using System.Resources;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("MediaBrowser.Common")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("Jellyfin Project")]
[assembly: AssemblyProduct("Jellyfin Server")]
[assembly: AssemblyCopyright("Copyright © 2019 Jellyfin Contributors. Code released under the GNU General Public License")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: NeutralResourcesLanguage("en")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
@@ -0,0 +1,123 @@
using System;
using System.Diagnostics.CodeAnalysis;
namespace MediaBrowser.Common.Providers
{
/// <summary>
/// Parsers for provider ids.
/// </summary>
public static class ProviderIdParsers
{
private const int ImdbMinNumbers = 7;
private const int ImdbMaxNumbers = 8;
private const string ImdbPrefix = "tt";
/// <summary>
/// Parses an IMDb id from a string.
/// </summary>
/// <param name="text">The text to parse.</param>
/// <param name="imdbId">The parsed IMDb id.</param>
/// <returns>True if parsing was successful, false otherwise.</returns>
public static bool TryFindImdbId(ReadOnlySpan<char> text, out ReadOnlySpan<char> imdbId)
{
// IMDb id is at least 9 chars (tt + 7 numbers)
while (text.Length >= 2 + ImdbMinNumbers)
{
var ttPos = text.IndexOf(ImdbPrefix);
if (ttPos == -1)
{
imdbId = default;
return false;
}
text = text.Slice(ttPos);
var i = 2;
var limit = Math.Min(text.Length, ImdbMaxNumbers + 2);
for (; i < limit; i++)
{
var c = text[i];
if (!IsDigit(c))
{
break;
}
}
// Skip if more than 8 digits + 2 chars for tt
if (i <= ImdbMaxNumbers + 2 && i >= ImdbMinNumbers + 2)
{
imdbId = text.Slice(0, i);
return true;
}
text = text.Slice(i);
}
imdbId = default;
return false;
}
/// <summary>
/// Parses an TMDb id from a movie url.
/// </summary>
/// <param name="text">The text with the url to parse.</param>
/// <param name="tmdbId">The parsed TMDb id.</param>
/// <returns>True if parsing was successful, false otherwise.</returns>
public static bool TryFindTmdbMovieId(ReadOnlySpan<char> text, out ReadOnlySpan<char> tmdbId)
=> TryFindProviderId(text, "themoviedb.org/movie/", out tmdbId);
/// <summary>
/// Parses an TMDb id from a series url.
/// </summary>
/// <param name="text">The text with the url to parse.</param>
/// <param name="tmdbId">The parsed TMDb id.</param>
/// <returns>True if parsing was successful, false otherwise.</returns>
public static bool TryFindTmdbSeriesId(ReadOnlySpan<char> text, out ReadOnlySpan<char> tmdbId)
=> TryFindProviderId(text, "themoviedb.org/tv/", out tmdbId);
/// <summary>
/// Parses an TVDb id from a url.
/// </summary>
/// <param name="text">The text with the url to parse.</param>
/// <param name="tvdbId">The parsed TVDb id.</param>
/// <returns>True if parsing was successful, false otherwise.</returns>
public static bool TryFindTvdbId(ReadOnlySpan<char> text, out ReadOnlySpan<char> tvdbId)
=> TryFindProviderId(text, "thetvdb.com/?tab=series&id=", out tvdbId);
private static bool TryFindProviderId(ReadOnlySpan<char> text, ReadOnlySpan<char> searchString, [NotNullWhen(true)] out ReadOnlySpan<char> providerId)
{
var searchPos = text.IndexOf(searchString);
if (searchPos == -1)
{
providerId = default;
return false;
}
text = text.Slice(searchPos + searchString.Length);
int i = 0;
for (; i < text.Length; i++)
{
var c = text[i];
if (!IsDigit(c))
{
break;
}
}
if (i >= 1)
{
providerId = text.Slice(0, i);
return true;
}
providerId = default;
return false;
}
private static bool IsDigit(char c)
{
return c >= '0' && c <= '9';
}
}
}
@@ -0,0 +1,21 @@
#pragma warning disable CS1591
using System.Collections.Generic;
using MediaBrowser.Common.Configuration;
using MediaBrowser.Model.Providers;
namespace MediaBrowser.Common.Providers
{
public class SubtitleConfigurationFactory : IConfigurationFactory
{
/// <inheritdoc />
public IEnumerable<ConfigurationStore> GetConfigurations()
{
yield return new ConfigurationStore()
{
Key = "subtitles",
ConfigurationType = typeof(SubtitleOptions)
};
}
}
}
@@ -0,0 +1,11 @@
using System;
namespace MediaBrowser.Common;
/// <summary>
/// Marks a BaseItem as needing custom serialisation from the Data field of the db.
/// </summary>
[System.AttributeUsage(System.AttributeTargets.Class, Inherited = true, AllowMultiple = false)]
public sealed class RequiresSourceSerialisationAttribute : System.Attribute
{
}
@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MediaBrowser.Common.Plugins;
using MediaBrowser.Model.Updates;
namespace MediaBrowser.Common.Updates
{
/// <summary>
/// Defines the <see cref="IInstallationManager" />.
/// </summary>
public interface IInstallationManager : IDisposable
{
/// <summary>
/// Gets the completed installations.
/// </summary>
IEnumerable<InstallationInfo> CompletedInstallations { get; }
/// <summary>
/// Parses a plugin manifest at the supplied URL.
/// </summary>
/// <param name="manifestName">Name of the repository.</param>
/// <param name="manifest">The URL to query.</param>
/// <param name="filterIncompatible">Filter out incompatible plugins.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{IReadOnlyList{PackageInfo}}.</returns>
Task<PackageInfo[]> GetPackages(string manifestName, string manifest, bool filterIncompatible, CancellationToken cancellationToken = default);
/// <summary>
/// Gets all available packages that are supported by this version.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>Task{IReadOnlyList{PackageInfo}}.</returns>
Task<IReadOnlyList<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken = default);
/// <summary>
/// Returns all plugins matching the requirements.
/// </summary>
/// <param name="availablePackages">The available packages.</param>
/// <param name="name">The name of the plugin.</param>
/// <param name="id">The id of the plugin.</param>
/// <param name="specificVersion">The version of the plugin.</param>
/// <returns>All plugins matching the requirements.</returns>
IEnumerable<PackageInfo> FilterPackages(
IEnumerable<PackageInfo> availablePackages,
string? name = null,
Guid id = default,
Version? specificVersion = null);
/// <summary>
/// Returns all compatible versions ordered from newest to oldest.
/// </summary>
/// <param name="availablePackages">The available packages.</param>
/// <param name="name">The name.</param>
/// <param name="id">The id of the plugin.</param>
/// <param name="minVersion">The minimum required version of the plugin.</param>
/// <param name="specificVersion">The specific version of the plugin to install.</param>
/// <returns>All compatible versions ordered from newest to oldest.</returns>
IEnumerable<InstallationInfo> GetCompatibleVersions(
IEnumerable<PackageInfo> availablePackages,
string? name = null,
Guid id = default,
Version? minVersion = null,
Version? specificVersion = null);
/// <summary>
/// Returns the available compatible plugin updates.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The available plugin updates.</returns>
Task<IEnumerable<InstallationInfo>> GetAvailablePluginUpdates(CancellationToken cancellationToken = default);
/// <summary>
/// Installs the package.
/// </summary>
/// <param name="package">The package.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns><see cref="Task" />.</returns>
Task InstallPackage(InstallationInfo package, CancellationToken cancellationToken = default);
/// <summary>
/// Uninstalls a plugin.
/// </summary>
/// <param name="plugin">The plugin.</param>
void UninstallPlugin(LocalPlugin plugin);
/// <summary>
/// Cancels the installation.
/// </summary>
/// <param name="id">The id of the package that is being installed.</param>
/// <returns>Returns true if the install was cancelled.</returns>
bool CancelInstallation(Guid id);
}
}
@@ -0,0 +1,23 @@
#nullable disable
using System;
using MediaBrowser.Model.Updates;
namespace MediaBrowser.Common.Updates
{
/// <summary>
/// Defines the <see cref="InstallationEventArgs" />.
/// </summary>
public class InstallationEventArgs : EventArgs
{
/// <summary>
/// Gets or sets the <see cref="InstallationInfo"/>.
/// </summary>
public InstallationInfo InstallationInfo { get; set; }
/// <summary>
/// Gets or sets the <see cref="VersionInfo"/>.
/// </summary>
public VersionInfo VersionInfo { get; set; }
}
}
@@ -0,0 +1,12 @@
#nullable disable
#pragma warning disable CS1591
using System;
namespace MediaBrowser.Common.Updates
{
public class InstallationFailedEventArgs : InstallationEventArgs
{
public Exception Exception { get; set; }
}
}
@@ -0,0 +1,4 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v10.0", FrameworkDisplayName = ".NET 10.0")]
@@ -0,0 +1,27 @@
is_global = true
build_property.TargetFramework = net10.0
build_property.TargetFramework = net10.0
build_property.TargetFrameworkIdentifier = .NETCoreApp
build_property.TargetFrameworkIdentifier = .NETCoreApp
build_property.TargetFrameworkVersion = v10.0
build_property.TargetFrameworkVersion = v10.0
build_property.TargetPlatformMinVersion =
build_property.TargetPlatformMinVersion =
build_property.UsingMicrosoftNETSdkWeb =
build_property.UsingMicrosoftNETSdkWeb =
build_property.ProjectTypeGuids =
build_property.ProjectTypeGuids =
build_property.InvariantGlobalization =
build_property.InvariantGlobalization =
build_property.PlatformNeutralAssembly =
build_property.PlatformNeutralAssembly =
build_property.EnforceExtendedAnalyzerRules =
build_property.EnforceExtendedAnalyzerRules =
build_property._SupportedPlatformList = Linux,macOS,Windows
build_property._SupportedPlatformList = Linux,macOS,Windows
build_property.RootNamespace = MediaBrowser.Common
build_property.ProjectDir = /srv/common_drive/Projects/pgsql-jellyfin/MediaBrowser.Common/
build_property.EnableComHosting =
build_property.EnableGeneratedComInterfaceComImportInterop =
build_property.EffectiveAnalysisLevelStyle = 10.0
build_property.EnableCodeStyleSeverity =
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<RestoreSuccess Condition=" '$(RestoreSuccess)' == '' ">True</RestoreSuccess>
<RestoreTool Condition=" '$(RestoreTool)' == '' ">NuGet</RestoreTool>
<ProjectAssetsFile Condition=" '$(ProjectAssetsFile)' == '' ">$(MSBuildThisFileDirectory)project.assets.json</ProjectAssetsFile>
<NuGetPackageRoot Condition=" '$(NuGetPackageRoot)' == '' ">/home/wjones/.nuget/packages/</NuGetPackageRoot>
<NuGetPackageFolders Condition=" '$(NuGetPackageFolders)' == '' ">/home/wjones/.nuget/packages/</NuGetPackageFolders>
<NuGetProjectStyle Condition=" '$(NuGetProjectStyle)' == '' ">PackageReference</NuGetProjectStyle>
<NuGetToolVersion Condition=" '$(NuGetToolVersion)' == '' ">7.0.0</NuGetToolVersion>
</PropertyGroup>
<ItemGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<SourceRoot Include="/home/wjones/.nuget/packages/" />
</ItemGroup>
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<Import Project="$(NuGetPackageRoot)microsoft.entityframeworkcore/10.0.3/buildTransitive/net10.0/Microsoft.EntityFrameworkCore.props" Condition="Exists('$(NuGetPackageRoot)microsoft.entityframeworkcore/10.0.3/buildTransitive/net10.0/Microsoft.EntityFrameworkCore.props')" />
<Import Project="$(NuGetPackageRoot)microsoft.codeanalysis.bannedapianalyzers/4.14.0/buildTransitive/Microsoft.CodeAnalysis.BannedApiAnalyzers.props" Condition="Exists('$(NuGetPackageRoot)microsoft.codeanalysis.bannedapianalyzers/4.14.0/buildTransitive/Microsoft.CodeAnalysis.BannedApiAnalyzers.props')" />
</ImportGroup>
<PropertyGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<PkgStyleCop_Analyzers_Unstable Condition=" '$(PkgStyleCop_Analyzers_Unstable)' == '' ">/home/wjones/.nuget/packages/stylecop.analyzers.unstable/1.2.0.556</PkgStyleCop_Analyzers_Unstable>
<PkgSmartAnalyzers_MultithreadingAnalyzer Condition=" '$(PkgSmartAnalyzers_MultithreadingAnalyzer)' == '' ">/home/wjones/.nuget/packages/smartanalyzers.multithreadinganalyzer/1.1.31</PkgSmartAnalyzers_MultithreadingAnalyzer>
<PkgSerilogAnalyzer Condition=" '$(PkgSerilogAnalyzer)' == '' ">/home/wjones/.nuget/packages/seriloganalyzer/0.15.0</PkgSerilogAnalyzer>
<PkgMicrosoft_CodeAnalysis_BannedApiAnalyzers Condition=" '$(PkgMicrosoft_CodeAnalysis_BannedApiAnalyzers)' == '' ">/home/wjones/.nuget/packages/microsoft.codeanalysis.bannedapianalyzers/4.14.0</PkgMicrosoft_CodeAnalysis_BannedApiAnalyzers>
<PkgIDisposableAnalyzers Condition=" '$(PkgIDisposableAnalyzers)' == '' ">/home/wjones/.nuget/packages/idisposableanalyzers/4.0.8</PkgIDisposableAnalyzers>
</PropertyGroup>
</Project>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Condition=" '$(ExcludeRestorePackageImports)' != 'true' ">
<Import Project="$(NuGetPackageRoot)microsoft.codeanalysis.bannedapianalyzers/4.14.0/buildTransitive/Microsoft.CodeAnalysis.BannedApiAnalyzers.targets" Condition="Exists('$(NuGetPackageRoot)microsoft.codeanalysis.bannedapianalyzers/4.14.0/buildTransitive/Microsoft.CodeAnalysis.BannedApiAnalyzers.targets')" />
</ImportGroup>
</Project>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,25 @@
{
"version": 2,
"dgSpecHash": "SvTROcOUqzw=",
"success": true,
"projectFilePath": "/srv/common_drive/Projects/pgsql-jellyfin/MediaBrowser.Common/MediaBrowser.Common.csproj",
"expectedPackageFiles": [
"/home/wjones/.nuget/packages/diacritics/4.1.4/diacritics.4.1.4.nupkg.sha512",
"/home/wjones/.nuget/packages/icu4n/60.1.0-alpha.356/icu4n.60.1.0-alpha.356.nupkg.sha512",
"/home/wjones/.nuget/packages/icu4n.transliterator/60.1.0-alpha.356/icu4n.transliterator.60.1.0-alpha.356.nupkg.sha512",
"/home/wjones/.nuget/packages/idisposableanalyzers/4.0.8/idisposableanalyzers.4.0.8.nupkg.sha512",
"/home/wjones/.nuget/packages/j2n/2.0.0/j2n.2.0.0.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.codeanalysis.bannedapianalyzers/4.14.0/microsoft.codeanalysis.bannedapianalyzers.4.14.0.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.entityframeworkcore/10.0.3/microsoft.entityframeworkcore.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.entityframeworkcore.abstractions/10.0.3/microsoft.entityframeworkcore.abstractions.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.entityframeworkcore.analyzers/10.0.3/microsoft.entityframeworkcore.analyzers.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/microsoft.entityframeworkcore.relational/10.0.3/microsoft.entityframeworkcore.relational.10.0.3.nupkg.sha512",
"/home/wjones/.nuget/packages/polly/8.6.5/polly.8.6.5.nupkg.sha512",
"/home/wjones/.nuget/packages/polly.core/8.6.5/polly.core.8.6.5.nupkg.sha512",
"/home/wjones/.nuget/packages/seriloganalyzer/0.15.0/seriloganalyzer.0.15.0.nupkg.sha512",
"/home/wjones/.nuget/packages/smartanalyzers.multithreadinganalyzer/1.1.31/smartanalyzers.multithreadinganalyzer.1.1.31.nupkg.sha512",
"/home/wjones/.nuget/packages/stylecop.analyzers/1.2.0-beta.556/stylecop.analyzers.1.2.0-beta.556.nupkg.sha512",
"/home/wjones/.nuget/packages/stylecop.analyzers.unstable/1.2.0.556/stylecop.analyzers.unstable.1.2.0.556.nupkg.sha512"
],
"logs": []
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
17714532047800000
@@ -0,0 +1 @@
17715044198300000