diff options
62 files changed, 777 insertions, 1047 deletions
diff --git a/BDInfo/BDInfo.csproj b/BDInfo/BDInfo.csproj index b2c752d0c..9dbaa9e2f 100644 --- a/BDInfo/BDInfo.csproj +++ b/BDInfo/BDInfo.csproj @@ -11,6 +11,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> </Project> diff --git a/DvdLib/DvdLib.csproj b/DvdLib/DvdLib.csproj index b2c752d0c..9dbaa9e2f 100644 --- a/DvdLib/DvdLib.csproj +++ b/DvdLib/DvdLib.csproj @@ -11,6 +11,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> </Project> diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj index 4c07087c5..34b49120b 100644 --- a/Emby.Dlna/Emby.Dlna.csproj +++ b/Emby.Dlna/Emby.Dlna.csproj @@ -14,6 +14,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> <ItemGroup> diff --git a/Emby.Dlna/PlayTo/SsdpHttpClient.cs b/Emby.Dlna/PlayTo/SsdpHttpClient.cs index 22aaa6885..217ea3a4b 100644 --- a/Emby.Dlna/PlayTo/SsdpHttpClient.cs +++ b/Emby.Dlna/PlayTo/SsdpHttpClient.cs @@ -73,9 +73,6 @@ namespace Emby.Dlna.PlayTo UserAgent = USERAGENT, LogErrorResponseBody = true, BufferContent = false, - - // The periodic requests may keep some devices awake - LogRequestAsDebug = true }; options.RequestHeaders["HOST"] = ip + ":" + port.ToString(_usCulture); @@ -98,9 +95,6 @@ namespace Emby.Dlna.PlayTo LogErrorResponseBody = true, BufferContent = false, - // The periodic requests may keep some devices awake - LogRequestAsDebug = true, - CancellationToken = cancellationToken }; @@ -135,13 +129,9 @@ namespace Emby.Dlna.PlayTo { Url = url, UserAgent = USERAGENT, - LogRequest = logRequest || _config.GetDlnaConfiguration().EnableDebugLog, LogErrorResponseBody = true, BufferContent = false, - // The periodic requests may keep some devices awake - LogRequestAsDebug = true, - CancellationToken = cancellationToken }; diff --git a/Emby.Drawing/Emby.Drawing.csproj b/Emby.Drawing/Emby.Drawing.csproj index 9f97baf77..2e539f2c7 100644 --- a/Emby.Drawing/Emby.Drawing.csproj +++ b/Emby.Drawing/Emby.Drawing.csproj @@ -3,6 +3,8 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> <ItemGroup> @@ -15,4 +17,9 @@ <Compile Include="..\SharedVersion.cs" /> </ItemGroup> + <PropertyGroup> + <!-- We need at least C# 7.1 for the "default literal" feature--> + <LangVersion>latest</LangVersion> + </PropertyGroup> + </Project> diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs index a7d95eb20..ce8089e59 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/Emby.Drawing/ImageProcessor.cs @@ -22,42 +22,47 @@ using Microsoft.Extensions.Logging; namespace Emby.Drawing { /// <summary> - /// Class ImageProcessor + /// Class ImageProcessor. /// </summary> public class ImageProcessor : IImageProcessor, IDisposable { - /// <summary> - /// The us culture - /// </summary> - protected readonly CultureInfo UsCulture = new CultureInfo("en-US"); + // Increment this when there's a change requiring caches to be invalidated + private const string Version = "3"; - /// <summary> - /// Gets the list of currently registered image processors - /// Image processors are specialized metadata providers that run after the normal ones - /// </summary> - /// <value>The image enhancers.</value> - public IImageEnhancer[] ImageEnhancers { get; private set; } + private static readonly HashSet<string> _transparentImageTypes + = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" }; /// <summary> /// The _logger /// </summary> private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; private readonly IServerApplicationPaths _appPaths; private IImageEncoder _imageEncoder; private readonly Func<ILibraryManager> _libraryManager; private readonly Func<IMediaEncoder> _mediaEncoder; + private readonly Dictionary<string, LockInfo> _locks = new Dictionary<string, LockInfo>(); + private bool _disposed = false; + + /// <summary> + /// + /// </summary> + /// <param name="logger"></param> + /// <param name="appPaths"></param> + /// <param name="fileSystem"></param> + /// <param name="imageEncoder"></param> + /// <param name="libraryManager"></param> + /// <param name="mediaEncoder"></param> public ImageProcessor( - ILoggerFactory loggerFactory, + ILogger<ImageProcessor> logger, IServerApplicationPaths appPaths, IFileSystem fileSystem, IImageEncoder imageEncoder, Func<ILibraryManager> libraryManager, Func<IMediaEncoder> mediaEncoder) { - _logger = loggerFactory.CreateLogger(nameof(ImageProcessor)); + _logger = logger; _fileSystem = fileSystem; _imageEncoder = imageEncoder; _libraryManager = libraryManager; @@ -69,20 +74,11 @@ namespace Emby.Drawing ImageHelper.ImageProcessor = this; } - public IImageEncoder ImageEncoder - { - get => _imageEncoder; - set - { - if (value == null) - { - throw new ArgumentNullException(nameof(value)); - } + private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images"); - _imageEncoder = value; - } - } + private string EnhancedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "enhanced-images"); + /// <inheritdoc /> public IReadOnlyCollection<string> SupportedInputFormats => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { @@ -115,18 +111,20 @@ namespace Emby.Drawing "wbmp" }; + /// <inheritdoc /> + public IReadOnlyCollection<IImageEnhancer> ImageEnhancers { get; set; } + /// <inheritdoc /> public bool SupportsImageCollageCreation => _imageEncoder.SupportsImageCollageCreation; - private string ResizedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "resized-images"); - - private string EnhancedImageCachePath => Path.Combine(_appPaths.ImageCachePath, "enhanced-images"); - - public void AddParts(IEnumerable<IImageEnhancer> enhancers) + /// <inheritdoc /> + public IImageEncoder ImageEncoder { - ImageEnhancers = enhancers.ToArray(); + get => _imageEncoder; + set => _imageEncoder = value ?? throw new ArgumentNullException(nameof(value)); } + /// <inheritdoc /> public async Task ProcessImage(ImageProcessingOptions options, Stream toStream) { var file = await ProcessImage(options).ConfigureAwait(false); @@ -137,15 +135,15 @@ namespace Emby.Drawing } } + /// <inheritdoc /> public IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats() => _imageEncoder.SupportedOutputFormats; - private static readonly HashSet<string> TransparentImageTypes - = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" }; - + /// <inheritdoc /> public bool SupportsTransparency(string path) - => TransparentImageTypes.Contains(Path.GetExtension(path)); + => _transparentImageTypes.Contains(Path.GetExtension(path)); + /// <inheritdoc /> public async Task<(string path, string mimeType, DateTime dateModified)> ProcessImage(ImageProcessingOptions options) { if (options == null) @@ -187,9 +185,9 @@ namespace Emby.Drawing } dateModified = supportedImageInfo.dateModified; - bool requiresTransparency = TransparentImageTypes.Contains(Path.GetExtension(originalImagePath)); + bool requiresTransparency = _transparentImageTypes.Contains(Path.GetExtension(originalImagePath)); - if (options.Enhancers.Length > 0) + if (options.Enhancers.Count > 0) { if (item == null) { @@ -279,7 +277,7 @@ namespace Emby.Drawing } } - private ImageFormat GetOutputFormat(ImageFormat[] clientSupportedFormats, bool requiresTransparency) + private ImageFormat GetOutputFormat(IReadOnlyCollection<ImageFormat> clientSupportedFormats, bool requiresTransparency) { var serverFormats = GetSupportedImageOutputFormats(); @@ -321,11 +319,6 @@ namespace Emby.Drawing } /// <summary> - /// Increment this when there's a change requiring caches to be invalidated - /// </summary> - private const string Version = "3"; - - /// <summary> /// Gets the cache file path based on a set of parameters /// </summary> private string GetCacheFilePath(string originalPath, ImageDimensions outputSize, int quality, DateTime dateModified, ImageFormat format, bool addPlayedIndicator, double percentPlayed, int? unwatchedCount, int? blur, string backgroundColor, string foregroundLayer) @@ -372,9 +365,11 @@ namespace Emby.Drawing return GetCachePath(ResizedImageCachePath, filename, "." + format.ToString().ToLowerInvariant()); } + /// <inheritdoc /> public ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info) => GetImageDimensions(item, info, true); + /// <inheritdoc /> public ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info, bool updateItem) { int width = info.Width; @@ -400,26 +395,19 @@ namespace Emby.Drawing return size; } - /// <summary> - /// Gets the size of the image. - /// </summary> + /// <inheritdoc /> public ImageDimensions GetImageDimensions(string path) => _imageEncoder.GetImageSize(path); - /// <summary> - /// Gets the image cache tag. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="image">The image.</param> - /// <returns>Guid.</returns> - /// <exception cref="ArgumentNullException">item</exception> + /// <inheritdoc /> public string GetImageCacheTag(BaseItem item, ItemImageInfo image) { - var supportedEnhancers = GetSupportedEnhancers(item, image.Type); + var supportedEnhancers = GetSupportedEnhancers(item, image.Type).ToArray(); return GetImageCacheTag(item, image, supportedEnhancers); } + /// <inheritdoc /> public string GetImageCacheTag(BaseItem item, ChapterInfo chapter) { try @@ -437,22 +425,15 @@ namespace Emby.Drawing } } - /// <summary> - /// Gets the image cache tag. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="image">The image.</param> - /// <param name="imageEnhancers">The image enhancers.</param> - /// <returns>Guid.</returns> - /// <exception cref="ArgumentNullException">item</exception> - public string GetImageCacheTag(BaseItem item, ItemImageInfo image, IImageEnhancer[] imageEnhancers) + /// <inheritdoc /> + public string GetImageCacheTag(BaseItem item, ItemImageInfo image, IReadOnlyCollection<IImageEnhancer> imageEnhancers) { string originalImagePath = image.Path; DateTime dateModified = image.DateModified; ImageType imageType = image.Type; // Optimization - if (imageEnhancers.Length == 0) + if (imageEnhancers.Count == 0) { return (originalImagePath + dateModified.Ticks).GetMD5().ToString("N", CultureInfo.InvariantCulture); } @@ -480,7 +461,7 @@ namespace Emby.Drawing { try { - string filename = (originalImagePath + dateModified.Ticks.ToString(UsCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture); + string filename = (originalImagePath + dateModified.Ticks.ToString(CultureInfo.InvariantCulture)).GetMD5().ToString("N", CultureInfo.InvariantCulture); string cacheExtension = _mediaEncoder().SupportsEncoder("libwebp") ? ".webp" : ".png"; var outputPath = Path.Combine(_appPaths.ImageCachePath, "converted-images", filename + cacheExtension); @@ -507,16 +488,10 @@ namespace Emby.Drawing return (originalImagePath, dateModified); } - /// <summary> - /// Gets the enhanced image. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="imageType">Type of the image.</param> - /// <param name="imageIndex">Index of the image.</param> - /// <returns>Task{System.String}.</returns> + /// <inheritdoc /> public async Task<string> GetEnhancedImage(BaseItem item, ImageType imageType, int imageIndex) { - var enhancers = GetSupportedEnhancers(item, imageType); + var enhancers = GetSupportedEnhancers(item, imageType).ToArray(); ItemImageInfo imageInfo = item.GetImageInfo(imageType, imageIndex); @@ -532,7 +507,7 @@ namespace Emby.Drawing bool inputImageSupportsTransparency, BaseItem item, int imageIndex, - IImageEnhancer[] enhancers, + IReadOnlyCollection<IImageEnhancer> enhancers, CancellationToken cancellationToken) { var originalImagePath = image.Path; @@ -573,6 +548,7 @@ namespace Emby.Drawing /// <param name="imageIndex">Index of the image.</param> /// <param name="supportedEnhancers">The supported enhancers.</param> /// <param name="cacheGuid">The cache unique identifier.</param> + /// <param name="cancellationToken">The cancellation token.</param> /// <returns>Task<System.String>.</returns> /// <exception cref="ArgumentNullException"> /// originalImagePath @@ -584,9 +560,9 @@ namespace Emby.Drawing BaseItem item, ImageType imageType, int imageIndex, - IImageEnhancer[] supportedEnhancers, + IReadOnlyCollection<IImageEnhancer> supportedEnhancers, string cacheGuid, - CancellationToken cancellationToken) + CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(originalImagePath)) { @@ -680,6 +656,7 @@ namespace Emby.Drawing { throw new ArgumentNullException(nameof(path)); } + if (string.IsNullOrEmpty(uniqueName)) { throw new ArgumentNullException(nameof(uniqueName)); @@ -722,6 +699,7 @@ namespace Emby.Drawing return Path.Combine(path, prefix, filename); } + /// <inheritdoc /> public void CreateImageCollage(ImageCollageOptions options) { _logger.LogInformation("Creating image collage and saving to {Path}", options.OutputPath); @@ -731,38 +709,25 @@ namespace Emby.Drawing _logger.LogInformation("Completed creation of image collage and saved to {Path}", options.OutputPath); } - public IImageEnhancer[] GetSupportedEnhancers(BaseItem item, ImageType imageType) + /// <inheritdoc /> + public IEnumerable<IImageEnhancer> GetSupportedEnhancers(BaseItem item, ImageType imageType) { - List<IImageEnhancer> list = null; - foreach (var i in ImageEnhancers) { - try - { - if (i.Supports(item, imageType)) - { - if (list == null) - { - list = new List<IImageEnhancer>(); - } - list.Add(i); - } - } - catch (Exception ex) + if (i.Supports(item, imageType)) { - _logger.LogError(ex, "Error in image enhancer: {0}", i.GetType().Name); + yield return i; } } - - return list == null ? Array.Empty<IImageEnhancer>() : list.ToArray(); } - private Dictionary<string, LockInfo> _locks = new Dictionary<string, LockInfo>(); + private class LockInfo { public SemaphoreSlim Lock = new SemaphoreSlim(1, 1); public int Count = 1; } + private LockInfo GetLock(string key) { lock (_locks) @@ -795,7 +760,7 @@ namespace Emby.Drawing } } - private bool _disposed; + /// <inheritdoc /> public void Dispose() { _disposed = true; diff --git a/Emby.Drawing/NullImageEncoder.cs b/Emby.Drawing/NullImageEncoder.cs index fc4a5af9f..5af7f1622 100644 --- a/Emby.Drawing/NullImageEncoder.cs +++ b/Emby.Drawing/NullImageEncoder.cs @@ -5,36 +5,40 @@ using MediaBrowser.Model.Drawing; namespace Emby.Drawing { + /// <summary> + /// A fallback implementation of <see cref="IImageEncoder" />. + /// </summary> public class NullImageEncoder : IImageEncoder { + /// <inheritdoc /> public IReadOnlyCollection<string> SupportedInputFormats => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "png", "jpeg", "jpg" }; + /// <inheritdoc /> public IReadOnlyCollection<ImageFormat> SupportedOutputFormats => new HashSet<ImageFormat>() { ImageFormat.Jpg, ImageFormat.Png }; - public void CropWhiteSpace(string inputPath, string outputPath) - { - throw new NotImplementedException(); - } - - public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) - { - throw new NotImplementedException(); - } - - public void CreateImageCollage(ImageCollageOptions options) - { - throw new NotImplementedException(); - } - + /// <inheritdoc /> public string Name => "Null Image Encoder"; + /// <inheritdoc /> public bool SupportsImageCollageCreation => false; + /// <inheritdoc /> public bool SupportsImageEncoding => false; + /// <inheritdoc /> public ImageDimensions GetImageSize(string path) + => throw new NotImplementedException(); + + /// <inheritdoc /> + public string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat selectedOutputFormat) + { + throw new NotImplementedException(); + } + + /// <inheritdoc /> + public void CreateImageCollage(ImageCollageOptions options) { throw new NotImplementedException(); } diff --git a/Emby.IsoMounting/IsoMounter/IsoMounter.csproj b/Emby.IsoMounting/IsoMounter/IsoMounter.csproj index dafa51cd5..0778b987b 100644 --- a/Emby.IsoMounting/IsoMounter/IsoMounter.csproj +++ b/Emby.IsoMounting/IsoMounter/IsoMounter.csproj @@ -12,6 +12,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> </Project> diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index 9e2a4950f..0b1ce2fce 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -23,7 +23,7 @@ <!-- Code analysers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.3" /> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.4" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" /> </ItemGroup> diff --git a/Emby.Notifications/Emby.Notifications.csproj b/Emby.Notifications/Emby.Notifications.csproj index 5c68e48c8..cbd3bde4f 100644 --- a/Emby.Notifications/Emby.Notifications.csproj +++ b/Emby.Notifications/Emby.Notifications.csproj @@ -3,6 +3,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> <ItemGroup> diff --git a/Emby.Photos/Emby.Photos.csproj b/Emby.Photos/Emby.Photos.csproj index c9830abc5..db73cb521 100644 --- a/Emby.Photos/Emby.Photos.csproj +++ b/Emby.Photos/Emby.Photos.csproj @@ -10,12 +10,13 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="TagLibSharp" Version="2.2.0-beta" /> + <PackageReference Include="TagLibSharp" Version="2.2.0" /> </ItemGroup> <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> </Project> diff --git a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs index 3e44c9c0a..fb4ffd74b 100644 --- a/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs +++ b/Emby.Server.Implementations/Activity/ActivityLogEntryPoint.cs @@ -346,7 +346,7 @@ namespace Emby.Server.Implementations.Activity }); } - private void OnPluginUpdated(object sender, GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>> e) + private void OnPluginUpdated(object sender, GenericEventArgs<(IPlugin, PackageVersionInfo)> e) { CreateLogEntry(new ActivityLogEntry { diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 09847b2f8..68b02d4a2 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -121,6 +121,10 @@ namespace Emby.Server.Implementations /// </summary> public abstract class ApplicationHost : IServerApplicationHost, IDisposable { + private SqliteUserRepository _userRepository; + + private SqliteDisplayPreferencesRepository _displayPreferencesRepository; + /// <summary> /// Gets a value indicating whether this instance can self restart. /// </summary> @@ -291,8 +295,6 @@ namespace Emby.Server.Implementations /// <value>The user data repository.</value> private IUserDataManager UserDataManager { get; set; } - private IUserRepository UserRepository { get; set; } - internal SqliteItemRepository ItemRepository { get; set; } private INotificationManager NotificationManager { get; set; } @@ -510,13 +512,8 @@ namespace Emby.Server.Implementations return AllConcreteTypes.Where(i => currentType.IsAssignableFrom(i)); } - /// <summary> - /// Gets the exports. - /// </summary> - /// <typeparam name="T">The type</typeparam> - /// <param name="manageLifetime">if set to <c>true</c> [manage lifetime].</param> - /// <returns>IEnumerable{``0}.</returns> - public IEnumerable<T> GetExports<T>(bool manageLifetime = true) + /// <inheritdoc /> + public IReadOnlyCollection<T> GetExports<T>(bool manageLifetime = true) { var parts = GetExportTypes<T>() .Select(CreateInstanceSafe) @@ -538,6 +535,7 @@ namespace Emby.Server.Implementations /// <summary> /// Runs the startup tasks. /// </summary> + /// <returns><see cref="Task" />.</returns> public async Task RunStartupTasksAsync() { Logger.LogInformation("Running startup tasks"); @@ -550,7 +548,7 @@ namespace Emby.Server.Implementations Logger.LogInformation("ServerId: {0}", SystemId); - var entryPoints = GetExports<IServerEntryPoint>().ToList(); + var entryPoints = GetExports<IServerEntryPoint>(); var stopWatch = new Stopwatch(); stopWatch.Start(); @@ -757,8 +755,12 @@ namespace Emby.Server.Implementations UserDataManager = new UserDataManager(LoggerFactory, ServerConfigurationManager, () => UserManager); serviceCollection.AddSingleton(UserDataManager); - var displayPreferencesRepo = new SqliteDisplayPreferencesRepository(LoggerFactory, JsonSerializer, ApplicationPaths, FileSystemManager); - serviceCollection.AddSingleton<IDisplayPreferencesRepository>(displayPreferencesRepo); + _displayPreferencesRepository = new SqliteDisplayPreferencesRepository( + LoggerFactory.CreateLogger<SqliteDisplayPreferencesRepository>(), + JsonSerializer, + ApplicationPaths, + FileSystemManager); + serviceCollection.AddSingleton<IDisplayPreferencesRepository>(_displayPreferencesRepository); ItemRepository = new SqliteItemRepository(ServerConfigurationManager, this, JsonSerializer, LoggerFactory, LocalizationManager); serviceCollection.AddSingleton<IItemRepository>(ItemRepository); @@ -766,9 +768,9 @@ namespace Emby.Server.Implementations AuthenticationRepository = GetAuthenticationRepository(); serviceCollection.AddSingleton(AuthenticationRepository); - UserRepository = GetUserRepository(); + _userRepository = GetUserRepository(); - UserManager = new UserManager(LoggerFactory, ServerConfigurationManager, UserRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, this, JsonSerializer, FileSystemManager); + UserManager = new UserManager(LoggerFactory, ServerConfigurationManager, _userRepository, XmlSerializer, NetworkManager, () => ImageProcessor, () => DtoService, this, JsonSerializer, FileSystemManager); serviceCollection.AddSingleton(UserManager); LibraryManager = new LibraryManager(this, LoggerFactory, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => LibraryMonitor, FileSystemManager, () => ProviderManager, () => UserViewManager); @@ -801,7 +803,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(HttpServer); - ImageProcessor = GetImageProcessor(); + ImageProcessor = new ImageProcessor(LoggerFactory.CreateLogger<ImageProcessor>(), ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder); serviceCollection.AddSingleton(ImageProcessor); TVSeriesManager = new TVSeriesManager(UserManager, UserDataManager, LibraryManager, ServerConfigurationManager); @@ -884,7 +886,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(typeof(IResourceFileManager), typeof(ResourceFileManager)); - displayPreferencesRepo.Initialize(); + _displayPreferencesRepository.Initialize(); var userDataRepo = new SqliteUserDataRepository(LoggerFactory, ApplicationPaths); @@ -953,18 +955,16 @@ namespace Emby.Server.Implementations } } - private IImageProcessor GetImageProcessor() - { - return new ImageProcessor(LoggerFactory, ServerConfigurationManager.ApplicationPaths, FileSystemManager, ImageEncoder, () => LibraryManager, () => MediaEncoder); - } - /// <summary> /// Gets the user repository. /// </summary> - /// <returns>Task{IUserRepository}.</returns> - private IUserRepository GetUserRepository() + /// <returns><see cref="Task{SqliteUserRepository}" />.</returns> + private SqliteUserRepository GetUserRepository() { - var repo = new SqliteUserRepository(LoggerFactory, ApplicationPaths, JsonSerializer); + var repo = new SqliteUserRepository( + LoggerFactory.CreateLogger<SqliteUserRepository>(), + ApplicationPaths, + JsonSerializer); repo.Initialize(); @@ -1087,7 +1087,7 @@ namespace Emby.Server.Implementations GetExports<IMetadataSaver>(), GetExports<IExternalId>()); - ImageProcessor.AddParts(GetExports<IImageEnhancer>()); + ImageProcessor.ImageEnhancers = GetExports<IImageEnhancer>(); LiveTvManager.AddParts(GetExports<ILiveTvService>(), GetExports<ITunerHost>(), GetExports<IListingsProvider>()); @@ -1528,8 +1528,6 @@ namespace Emby.Server.Implementations { Url = Url, LogErrorResponseBody = false, - LogErrors = false, - LogRequest = false, BufferContent = false, CancellationToken = cancellationToken }).ConfigureAwait(false)) @@ -1681,8 +1679,8 @@ namespace Emby.Server.Implementations private async Task<bool> IsIpAddressValidAsync(IPAddress address, CancellationToken cancellationToken) { - if (address.Equals(IPAddress.Loopback) || - address.Equals(IPAddress.IPv6Loopback)) + if (address.Equals(IPAddress.Loopback) + || address.Equals(IPAddress.IPv6Loopback)) { return true; } @@ -1695,12 +1693,6 @@ namespace Emby.Server.Implementations return cachedResult; } -#if DEBUG - const bool LogPing = true; -#else - const bool LogPing = false; -#endif - try { using (var response = await HttpClient.SendAsync( @@ -1708,8 +1700,6 @@ namespace Emby.Server.Implementations { Url = apiUrl, LogErrorResponseBody = false, - LogErrors = LogPing, - LogRequest = LogPing, BufferContent = false, CancellationToken = cancellationToken }, HttpMethod.Post).ConfigureAwait(false)) @@ -1910,10 +1900,12 @@ namespace Emby.Server.Implementations } } - UserRepository.Dispose(); + _userRepository?.Dispose(); + _displayPreferencesRepository.Dispose(); } - UserRepository = null; + _userRepository = null; + _displayPreferencesRepository = null; _disposed = true; } diff --git a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs index 010ad6384..a5bb47afb 100644 --- a/Emby.Server.Implementations/Data/BaseSqliteRepository.cs +++ b/Emby.Server.Implementations/Data/BaseSqliteRepository.cs @@ -124,7 +124,7 @@ namespace Emby.Server.Implementations.Data } WriteConnection.Execute("PRAGMA temp_store=" + (int)TempStore); - + // Configuration and pragmas can affect VACUUM so it needs to be last. WriteConnection.Execute("VACUUM"); @@ -218,7 +218,7 @@ namespace Emby.Server.Implementations.Data WriteLock.Wait(); try { - WriteConnection.Dispose(); + WriteConnection?.Dispose(); } finally { diff --git a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs index 87096e72f..77f5d9479 100644 --- a/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteDisplayPreferencesRepository.cs @@ -21,8 +21,8 @@ namespace Emby.Server.Implementations.Data { private readonly IFileSystem _fileSystem; - public SqliteDisplayPreferencesRepository(ILoggerFactory loggerFactory, IJsonSerializer jsonSerializer, IApplicationPaths appPaths, IFileSystem fileSystem) - : base(loggerFactory.CreateLogger(nameof(SqliteDisplayPreferencesRepository))) + public SqliteDisplayPreferencesRepository(ILogger<SqliteDisplayPreferencesRepository> logger, IJsonSerializer jsonSerializer, IApplicationPaths appPaths, IFileSystem fileSystem) + : base(logger) { _jsonSerializer = jsonSerializer; _fileSystem = fileSystem; diff --git a/Emby.Server.Implementations/Data/SqliteUserRepository.cs b/Emby.Server.Implementations/Data/SqliteUserRepository.cs index de2354eef..d6d250fb3 100644 --- a/Emby.Server.Implementations/Data/SqliteUserRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserRepository.cs @@ -18,10 +18,10 @@ namespace Emby.Server.Implementations.Data private readonly IJsonSerializer _jsonSerializer; public SqliteUserRepository( - ILoggerFactory loggerFactory, + ILogger<SqliteUserRepository> logger, IServerApplicationPaths appPaths, IJsonSerializer jsonSerializer) - : base(loggerFactory.CreateLogger(nameof(SqliteUserRepository))) + : base(logger) { _jsonSerializer = jsonSerializer; diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 6e7aa1313..1a7f10634 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -1364,7 +1364,7 @@ namespace Emby.Server.Implementations.Dto return null; } - var supportedEnhancers = _imageProcessor.GetSupportedEnhancers(item, ImageType.Primary); + var supportedEnhancers = _imageProcessor.GetSupportedEnhancers(item, ImageType.Primary).ToArray(); ImageDimensions size; diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index f07f8e3bf..c78d96d4a 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -32,7 +32,7 @@ <PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="2.2.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.2.0" /> - <PackageReference Include="ServiceStack.Text.Core" Version="5.5.0" /> + <PackageReference Include="ServiceStack.Text.Core" Version="5.6.0" /> <PackageReference Include="sharpcompress" Version="0.23.0" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="1.0.0" /> </ItemGroup> @@ -47,13 +47,14 @@ <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Release' "> - <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <PropertyGroup> + <!-- We need at least C# 7.3 to compare tuples--> + <LangVersion>latest</LangVersion> </PropertyGroup> <!-- Code analysers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.3" /> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.4" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" /> </ItemGroup> diff --git a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs index a933b53f5..0dd4d4ca5 100644 --- a/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs +++ b/Emby.Server.Implementations/HttpClientManager/HttpClientManager.cs @@ -5,9 +5,6 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Extensions; @@ -20,7 +17,7 @@ using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.HttpClientManager { /// <summary> - /// Class HttpClientManager + /// Class HttpClientManager. /// </summary> public class HttpClientManager : IHttpClient { @@ -45,19 +42,9 @@ namespace Emby.Server.Implementations.HttpClientManager IFileSystem fileSystem, Func<string> defaultUserAgentFn) { - if (appPaths == null) - { - throw new ArgumentNullException(nameof(appPaths)); - } - - if (logger == null) - { - throw new ArgumentNullException(nameof(logger)); - } - - _logger = logger; + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _fileSystem = fileSystem; - _appPaths = appPaths; + _appPaths = appPaths ?? throw new ArgumentNullException(nameof(appPaths)); _defaultUserAgentFn = defaultUserAgentFn; } @@ -118,7 +105,7 @@ namespace Emby.Server.Implementations.HttpClientManager request.Headers.Add(HeaderNames.Connection, "Keep-Alive"); } - //request.Headers.Add(HeaderNames.CacheControl, "no-cache"); + // request.Headers.Add(HeaderNames.CacheControl, "no-cache"); /* if (!string.IsNullOrWhiteSpace(userInfo)) @@ -196,7 +183,7 @@ namespace Emby.Server.Implementations.HttpClientManager } var url = options.Url; - var urlHash = url.ToLowerInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture); + var urlHash = url.ToUpperInvariant().GetMD5().ToString("N", CultureInfo.InvariantCulture); var responseCachePath = Path.Combine(_appPaths.CachePath, "httpclient", urlHash); @@ -239,7 +226,13 @@ namespace Emby.Server.Implementations.HttpClientManager { Directory.CreateDirectory(Path.GetDirectoryName(responseCachePath)); - using (var fileStream = _fileSystem.GetFileStream(responseCachePath, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.None, true)) + using (var fileStream = new FileStream( + responseCachePath, + FileMode.Create, + FileAccess.Write, + FileShare.None, + StreamDefaults.DefaultFileStreamBufferSize, + true)) { await response.Content.CopyToAsync(fileStream).ConfigureAwait(false); @@ -278,16 +271,11 @@ namespace Emby.Server.Implementations.HttpClientManager } } - if (options.LogRequest) - { - _logger.LogDebug("HttpClientManager {0}: {1}", httpMethod.ToString(), options.Url); - } - options.CancellationToken.ThrowIfCancellationRequested(); var response = await client.SendAsync( httpWebRequest, - options.BufferContent ? HttpCompletionOption.ResponseContentRead : HttpCompletionOption.ResponseHeadersRead, + options.BufferContent || options.CacheMode == CacheMode.Unconditional ? HttpCompletionOption.ResponseContentRead : HttpCompletionOption.ResponseHeadersRead, options.CancellationToken).ConfigureAwait(false); await EnsureSuccessStatusCode(response, options).ConfigureAwait(false); @@ -308,138 +296,6 @@ namespace Emby.Server.Implementations.HttpClientManager public Task<HttpResponseInfo> Post(HttpRequestOptions options) => SendAsync(options, HttpMethod.Post); - /// <summary> - /// Downloads the contents of a given url into a temporary location - /// </summary> - /// <param name="options">The options.</param> - /// <returns>Task{System.String}.</returns> - public async Task<string> GetTempFile(HttpRequestOptions options) - { - var response = await GetTempFileResponse(options).ConfigureAwait(false); - return response.TempFilePath; - } - - public async Task<HttpResponseInfo> GetTempFileResponse(HttpRequestOptions options) - { - ValidateParams(options); - - Directory.CreateDirectory(_appPaths.TempDirectory); - - var tempFile = Path.Combine(_appPaths.TempDirectory, Guid.NewGuid() + ".tmp"); - - if (options.Progress == null) - { - throw new ArgumentException("Options did not have a Progress value.", nameof(options)); - } - - options.CancellationToken.ThrowIfCancellationRequested(); - - var httpWebRequest = GetRequestMessage(options, HttpMethod.Get); - - options.Progress.Report(0); - - if (options.LogRequest) - { - _logger.LogDebug("HttpClientManager.GetTempFileResponse url: {0}", options.Url); - } - - var client = GetHttpClient(options.Url); - - try - { - options.CancellationToken.ThrowIfCancellationRequested(); - - using (var response = (await client.SendAsync(httpWebRequest, options.CancellationToken).ConfigureAwait(false))) - { - await EnsureSuccessStatusCode(response, options).ConfigureAwait(false); - - options.CancellationToken.ThrowIfCancellationRequested(); - - using (var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) - using (var fs = _fileSystem.GetFileStream(tempFile, FileOpenMode.Create, FileAccessMode.Write, FileShareMode.Read, true)) - { - await stream.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, options.CancellationToken).ConfigureAwait(false); - } - - options.Progress.Report(100); - - var responseInfo = new HttpResponseInfo(response.Headers, response.Content.Headers) - { - TempFilePath = tempFile, - StatusCode = response.StatusCode, - ContentType = response.Content.Headers.ContentType?.MediaType, - ContentLength = response.Content.Headers.ContentLength - }; - - return responseInfo; - } - } - catch (Exception ex) - { - if (File.Exists(tempFile)) - { - File.Delete(tempFile); - } - - throw GetException(ex, options); - } - } - - private Exception GetException(Exception ex, HttpRequestOptions options) - { - if (ex is HttpException) - { - return ex; - } - - var webException = ex as WebException - ?? ex.InnerException as WebException; - - if (webException != null) - { - if (options.LogErrors) - { - _logger.LogError(webException, "Error {Status} getting response from {Url}", webException.Status, options.Url); - } - - var exception = new HttpException(webException.Message, webException); - - using (var response = webException.Response as HttpWebResponse) - { - if (response != null) - { - exception.StatusCode = response.StatusCode; - } - } - - if (!exception.StatusCode.HasValue) - { - if (webException.Status == WebExceptionStatus.NameResolutionFailure || - webException.Status == WebExceptionStatus.ConnectFailure) - { - exception.IsTimedOut = true; - } - } - - return exception; - } - - var operationCanceledException = ex as OperationCanceledException - ?? ex.InnerException as OperationCanceledException; - - if (operationCanceledException != null) - { - return GetCancellationException(options, options.CancellationToken, operationCanceledException); - } - - if (options.LogErrors) - { - _logger.LogError(ex, "Error getting response from {Url}", options.Url); - } - - return ex; - } - private void ValidateParams(HttpRequestOptions options) { if (string.IsNullOrEmpty(options.Url)) @@ -471,35 +327,6 @@ namespace Emby.Server.Implementations.HttpClientManager return url; } - /// <summary> - /// Throws the cancellation exception. - /// </summary> - /// <param name="options">The options.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <param name="exception">The exception.</param> - /// <returns>Exception.</returns> - private Exception GetCancellationException(HttpRequestOptions options, CancellationToken cancellationToken, OperationCanceledException exception) - { - // If the HttpClient's timeout is reached, it will cancel the Task internally - if (!cancellationToken.IsCancellationRequested) - { - var msg = string.Format("Connection to {0} timed out", options.Url); - - if (options.LogErrors) - { - _logger.LogError(msg); - } - - // Throw an HttpException so that the caller doesn't think it was cancelled by user code - return new HttpException(msg, exception) - { - IsTimedOut = true - }; - } - - return exception; - } - private async Task EnsureSuccessStatusCode(HttpResponseMessage response, HttpRequestOptions options) { if (response.IsSuccessStatusCode) @@ -507,8 +334,11 @@ namespace Emby.Server.Implementations.HttpClientManager return; } - var msg = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - _logger.LogError("HTTP request failed with message: {Message}", msg); + if (options.LogErrorResponseBody) + { + var msg = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + _logger.LogError("HTTP request failed with message: {Message}", msg); + } throw new HttpException(response.ReasonPhrase) { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index db016ec70..ed254accb 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -256,7 +256,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var uri = new Uri(GetApiUrl(info)); - using (var manager = new HdHomerunManager(_socketFactory, Logger)) + using (var manager = new HdHomerunManager(Logger)) { // Legacy HdHomeruns are IPv4 only var ipInfo = IPAddress.Parse(uri.Host); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index 6e79441da..c19552428 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -1,12 +1,13 @@ using System; +using System.Buffers; using System.Collections.Generic; using System.Net; +using System.Net.Sockets; using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Controller.LiveTv; -using MediaBrowser.Model.Net; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun @@ -78,35 +79,34 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun public class HdHomerunManager : IDisposable { - public static int HdHomeRunPort = 65001; + public const int HdHomeRunPort = 65001; // Message constants - private static byte GetSetName = 3; - private static byte GetSetValue = 4; - private static byte GetSetLockkey = 21; - private static ushort GetSetRequest = 4; - private static ushort GetSetReply = 5; + private const byte GetSetName = 3; + private const byte GetSetValue = 4; + private const byte GetSetLockkey = 21; + private const ushort GetSetRequest = 4; + private const ushort GetSetReply = 5; + + private readonly ILogger _logger; private uint? _lockkey = null; private int _activeTuner = -1; - private readonly ISocketFactory _socketFactory; - private IPAddress _remoteIp; + private IPEndPoint _remoteEndPoint; - private ILogger _logger; - private ISocket _currentTcpSocket; + private TcpClient _tcpClient; - public HdHomerunManager(ISocketFactory socketFactory, ILogger logger) + public HdHomerunManager(ILogger logger) { - _socketFactory = socketFactory; _logger = logger; } public void Dispose() { - using (var socket = _currentTcpSocket) + using (var socket = _tcpClient) { if (socket != null) { - _currentTcpSocket = null; + _tcpClient = null; var task = StopStreaming(socket); Task.WaitAll(task); @@ -116,35 +116,38 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken) { - using (var socket = _socketFactory.CreateTcpSocket(remoteIp, HdHomeRunPort)) + using (var client = new TcpClient(new IPEndPoint(remoteIp, HdHomeRunPort))) + using (var stream = client.GetStream()) { - return await CheckTunerAvailability(socket, remoteIp, tuner, cancellationToken).ConfigureAwait(false); + return await CheckTunerAvailability(stream, tuner, cancellationToken).ConfigureAwait(false); } } - private static async Task<bool> CheckTunerAvailability(ISocket socket, IPAddress remoteIp, int tuner, CancellationToken cancellationToken) + private static async Task<bool> CheckTunerAvailability(NetworkStream stream, int tuner, CancellationToken cancellationToken) { - var ipEndPoint = new IPEndPoint(remoteIp, HdHomeRunPort); - var lockkeyMsg = CreateGetMessage(tuner, "lockkey"); - await socket.SendToAsync(lockkeyMsg, 0, lockkeyMsg.Length, ipEndPoint, cancellationToken); + await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false); - var receiveBuffer = new byte[8192]; - var response = await socket.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false); + byte[] buffer = ArrayPool<byte>.Shared.Rent(8192); + try + { + int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); - ParseReturnMessage(response.Buffer, response.ReceivedBytes, out string returnVal); + ParseReturnMessage(buffer, receivedBytes, out string returnVal); - return string.Equals(returnVal, "none", StringComparison.OrdinalIgnoreCase); + return string.Equals(returnVal, "none", StringComparison.OrdinalIgnoreCase); + } + finally + { + ArrayPool<byte>.Shared.Return(buffer); + } } public async Task StartStreaming(IPAddress remoteIp, IPAddress localIp, int localPort, IHdHomerunChannelCommands commands, int numTuners, CancellationToken cancellationToken) { - _remoteIp = remoteIp; + _remoteEndPoint = new IPEndPoint(remoteIp, HdHomeRunPort); - var tcpClient = _socketFactory.CreateTcpSocket(_remoteIp, HdHomeRunPort); - _currentTcpSocket = tcpClient; - - var receiveBuffer = new byte[8192]; + _tcpClient = new TcpClient(_remoteEndPoint); if (!_lockkey.HasValue) { @@ -153,51 +156,61 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } var lockKeyValue = _lockkey.Value; + var stream = _tcpClient.GetStream(); - var ipEndPoint = new IPEndPoint(_remoteIp, HdHomeRunPort); - - for (int i = 0; i < numTuners; ++i) + byte[] buffer = ArrayPool<byte>.Shared.Rent(8192); + try { - if (!await CheckTunerAvailability(tcpClient, _remoteIp, i, cancellationToken).ConfigureAwait(false)) - continue; - - _activeTuner = i; - var lockKeyString = string.Format("{0:d}", lockKeyValue); - var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null); - await tcpClient.SendToAsync(lockkeyMsg, 0, lockkeyMsg.Length, ipEndPoint, cancellationToken).ConfigureAwait(false); - var response = await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false); - // parse response to make sure it worked - if (!ParseReturnMessage(response.Buffer, response.ReceivedBytes, out var returnVal)) - continue; - - var commandList = commands.GetCommands(); - foreach (Tuple<string, string> command in commandList) + for (int i = 0; i < numTuners; ++i) { - var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue); - await tcpClient.SendToAsync(channelMsg, 0, channelMsg.Length, ipEndPoint, cancellationToken).ConfigureAwait(false); - response = await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false); + if (!await CheckTunerAvailability(stream, i, cancellationToken).ConfigureAwait(false)) + { + continue; + } + + _activeTuner = i; + var lockKeyString = string.Format("{0:d}", lockKeyValue); + var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null); + await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false); + int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); // parse response to make sure it worked - if (!ParseReturnMessage(response.Buffer, response.ReceivedBytes, out returnVal)) + if (!ParseReturnMessage(buffer, receivedBytes, out var returnVal)) { - await ReleaseLockkey(tcpClient, lockKeyValue).ConfigureAwait(false); continue; } - } + var commandList = commands.GetCommands(); + foreach (Tuple<string, string> command in commandList) + { + var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue); + await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false); + receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + // parse response to make sure it worked + if (!ParseReturnMessage(buffer, receivedBytes, out returnVal)) + { + await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false); + continue; + } + } - var targetValue = string.Format("rtp://{0}:{1}", localIp, localPort); - var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue); + var targetValue = string.Format("rtp://{0}:{1}", localIp, localPort); + var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue); - await tcpClient.SendToAsync(targetMsg, 0, targetMsg.Length, ipEndPoint, cancellationToken).ConfigureAwait(false); - response = await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false); - // parse response to make sure it worked - if (!ParseReturnMessage(response.Buffer, response.ReceivedBytes, out returnVal)) - { - await ReleaseLockkey(tcpClient, lockKeyValue).ConfigureAwait(false); - continue; - } + await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false); + receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + // parse response to make sure it worked + if (!ParseReturnMessage(buffer, receivedBytes, out returnVal)) + { + await ReleaseLockkey(_tcpClient, lockKeyValue).ConfigureAwait(false); + continue; + } - return; + return; + } + } + finally + { + ArrayPool<byte>.Shared.Return(buffer); } _activeTuner = -1; @@ -207,53 +220,70 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun public async Task ChangeChannel(IHdHomerunChannelCommands commands, CancellationToken cancellationToken) { if (!_lockkey.HasValue) + { return; + } - using (var tcpClient = _socketFactory.CreateTcpSocket(_remoteIp, HdHomeRunPort)) + using (var tcpClient = new TcpClient(_remoteEndPoint)) + using (var stream = tcpClient.GetStream()) { var commandList = commands.GetCommands(); - var receiveBuffer = new byte[8192]; - - foreach (Tuple<string, string> command in commandList) + byte[] buffer = ArrayPool<byte>.Shared.Rent(8192); + try { - var channelMsg = CreateSetMessage(_activeTuner, command.Item1, command.Item2, _lockkey); - await tcpClient.SendToAsync(channelMsg, 0, channelMsg.Length, new IPEndPoint(_remoteIp, HdHomeRunPort), cancellationToken).ConfigureAwait(false); - var response = await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false); - // parse response to make sure it worked - if (!ParseReturnMessage(response.Buffer, response.ReceivedBytes, out string returnVal)) + foreach (Tuple<string, string> command in commandList) { - return; + var channelMsg = CreateSetMessage(_activeTuner, command.Item1, command.Item2, _lockkey); + await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false); + int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); + // parse response to make sure it worked + if (!ParseReturnMessage(buffer, receivedBytes, out string returnVal)) + { + return; + } } } + finally + { + ArrayPool<byte>.Shared.Return(buffer); + } } } - public Task StopStreaming(ISocket socket) + public Task StopStreaming(TcpClient client) { var lockKey = _lockkey; if (!lockKey.HasValue) + { return Task.CompletedTask; + } - return ReleaseLockkey(socket, lockKey.Value); + return ReleaseLockkey(client, lockKey.Value); } - private async Task ReleaseLockkey(ISocket tcpClient, uint lockKeyValue) + private async Task ReleaseLockkey(TcpClient client, uint lockKeyValue) { _logger.LogInformation("HdHomerunManager.ReleaseLockkey {0}", lockKeyValue); - var ipEndPoint = new IPEndPoint(_remoteIp, HdHomeRunPort); + var stream = client.GetStream(); var releaseTarget = CreateSetMessage(_activeTuner, "target", "none", lockKeyValue); - await tcpClient.SendToAsync(releaseTarget, 0, releaseTarget.Length, ipEndPoint, CancellationToken.None).ConfigureAwait(false); - - var receiveBuffer = new byte[8192]; + await stream.WriteAsync(releaseTarget, 0, releaseTarget.Length, CancellationToken.None).ConfigureAwait(false); - await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, CancellationToken.None).ConfigureAwait(false); - var releaseKeyMsg = CreateSetMessage(_activeTuner, "lockkey", "none", lockKeyValue); - _lockkey = null; - await tcpClient.SendToAsync(releaseKeyMsg, 0, releaseKeyMsg.Length, ipEndPoint, CancellationToken.None).ConfigureAwait(false); - await tcpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, CancellationToken.None).ConfigureAwait(false); + var buffer = ArrayPool<byte>.Shared.Rent(8192); + try + { + await stream.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None).ConfigureAwait(false); + var releaseKeyMsg = CreateSetMessage(_activeTuner, "lockkey", "none", lockKeyValue); + _lockkey = null; + await stream.WriteAsync(releaseKeyMsg, 0, releaseKeyMsg.Length, CancellationToken.None).ConfigureAwait(false); + await stream.ReadAsync(buffer, 0, buffer.Length, CancellationToken.None).ConfigureAwait(false); + } + finally + { + ArrayPool<byte>.Shared.Return(buffer); + } } private static byte[] CreateGetMessage(int tuner, string name) @@ -270,7 +300,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun // calculate crc and insert at the end of the message var crcBytes = BitConverter.GetBytes(HdHomerunCrc.GetCrc32(message, messageLength - 4)); if (flipEndian) + { Array.Reverse(crcBytes); + } + Buffer.BlockCopy(crcBytes, 0, message, offset, 4); return message; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index ec708cf20..1d79a5f96 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -49,13 +49,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun EnableStreamSharing = true; } - private static Socket CreateSocket(AddressFamily addressFamily, SocketType socketType, ProtocolType protocolType) - { - var socket = new Socket(addressFamily, SocketType.Stream, ProtocolType.Tcp); - - return socket; - } - public override async Task Open(CancellationToken openCancellationToken) { LiveStreamCancellationTokenSource.Token.ThrowIfCancellationRequested(); @@ -71,13 +64,13 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var remoteAddress = IPAddress.Parse(uri.Host); IPAddress localAddress = null; - using (var tcpSocket = CreateSocket(remoteAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp)) + using (var tcpClient = new TcpClient()) { try { - tcpSocket.Connect(new IPEndPoint(remoteAddress, HdHomerunManager.HdHomeRunPort)); - localAddress = ((IPEndPoint)tcpSocket.LocalEndPoint).Address; - tcpSocket.Close(); + await tcpClient.ConnectAsync(remoteAddress, HdHomerunManager.HdHomeRunPort).ConfigureAwait(false); + localAddress = ((IPEndPoint)tcpClient.Client.RemoteEndPoint).Address; + tcpClient.Close(); } catch (Exception ex) { @@ -87,7 +80,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } var udpClient = _socketFactory.CreateUdpSocket(localPort); - var hdHomerunManager = new HdHomerunManager(_socketFactory, Logger); + var hdHomerunManager = new HdHomerunManager(Logger); try { @@ -103,6 +96,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { Logger.LogError(ex, "Error opening live stream:"); } + throw; } } @@ -199,126 +193,5 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } } } - - public class UdpClientStream : Stream - { - private static int RtpHeaderBytes = 12; - private static int PacketSize = 1316; - private readonly MediaBrowser.Model.Net.ISocket _udpClient; - bool disposed; - - public UdpClientStream(MediaBrowser.Model.Net.ISocket udpClient) : base() - { - _udpClient = udpClient; - } - - public override async Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) - { - if (buffer == null) - throw new ArgumentNullException(nameof(buffer)); - - if (offset + count < 0) - throw new ArgumentOutOfRangeException(nameof(offset), "offset + count must not be negative"); - - if (offset + count > buffer.Length) - throw new ArgumentException("offset + count must not be greater than the length of buffer"); - - if (disposed) - throw new ObjectDisposedException(nameof(UdpClientStream)); - - // This will always receive a 1328 packet size (PacketSize + RtpHeaderSize) - // The RTP header will be stripped so see how many reads we need to make to fill the buffer. - int numReads = count / PacketSize; - int totalBytesRead = 0; - byte[] receiveBuffer = new byte[81920]; - - for (int i = 0; i < numReads; ++i) - { - var data = await _udpClient.ReceiveAsync(receiveBuffer, 0, receiveBuffer.Length, cancellationToken).ConfigureAwait(false); - - var bytesRead = data.ReceivedBytes - RtpHeaderBytes; - - // remove rtp header - Buffer.BlockCopy(data.Buffer, RtpHeaderBytes, buffer, offset, bytesRead); - offset += bytesRead; - totalBytesRead += bytesRead; - } - return totalBytesRead; - } - - public override int Read(byte[] buffer, int offset, int count) - { - if (buffer == null) - throw new ArgumentNullException(nameof(buffer)); - - if (offset + count < 0) - throw new ArgumentOutOfRangeException("offset + count must not be negative", "offset+count"); - - if (offset + count > buffer.Length) - throw new ArgumentException("offset + count must not be greater than the length of buffer"); - - if (disposed) - throw new ObjectDisposedException(nameof(UdpClientStream)); - - // This will always receive a 1328 packet size (PacketSize + RtpHeaderSize) - // The RTP header will be stripped so see how many reads we need to make to fill the buffer. - int numReads = count / PacketSize; - int totalBytesRead = 0; - byte[] receiveBuffer = new byte[81920]; - - for (int i = 0; i < numReads; ++i) - { - var receivedBytes = _udpClient.Receive(receiveBuffer, 0, receiveBuffer.Length); - - var bytesRead = receivedBytes - RtpHeaderBytes; - - // remove rtp header - Buffer.BlockCopy(receiveBuffer, RtpHeaderBytes, buffer, offset, bytesRead); - offset += bytesRead; - totalBytesRead += bytesRead; - } - return totalBytesRead; - } - - protected override void Dispose(bool disposing) - { - disposed = true; - } - - public override bool CanRead => throw new NotImplementedException(); - - public override bool CanSeek => throw new NotImplementedException(); - - public override bool CanWrite => throw new NotImplementedException(); - - public override long Length => throw new NotImplementedException(); - - public override long Position - { - get => throw new NotImplementedException(); - - set => throw new NotImplementedException(); - } - - public override void Flush() - { - throw new NotImplementedException(); - } - - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotImplementedException(); - } - - public override void SetLength(long value) - { - throw new NotImplementedException(); - } - - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotImplementedException(); - } - } } } diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs index 42ffa4e22..cb53ce50c 100644 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ b/Emby.Server.Implementations/Net/SocketFactory.cs @@ -2,54 +2,12 @@ using System; using System.IO; using System.Net; using System.Net.Sockets; -using Emby.Server.Implementations.Networking; using MediaBrowser.Model.Net; namespace Emby.Server.Implementations.Net { public class SocketFactory : ISocketFactory { - // THIS IS A LINKED FILE - SHARED AMONGST MULTIPLE PLATFORMS - // Be careful to check any changes compile and work for all platform projects it is shared in. - - // Not entirely happy with this. Would have liked to have done something more generic/reusable, - // but that wasn't really the point so kept to YAGNI principal for now, even if the - // interfaces are a bit ugly, specific and make assumptions. - - public ISocket CreateTcpSocket(IPAddress remoteAddress, int remotePort) - { - if (remotePort < 0) - { - throw new ArgumentException("remotePort cannot be less than zero.", nameof(remotePort)); - } - - var addressFamily = remoteAddress.AddressFamily == AddressFamily.InterNetwork - ? AddressFamily.InterNetwork - : AddressFamily.InterNetworkV6; - - var retVal = new Socket(addressFamily, System.Net.Sockets.SocketType.Stream, System.Net.Sockets.ProtocolType.Tcp); - - try - { - retVal.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - } - catch (SocketException) - { - // This is not supported on all operating systems (qnap) - } - - try - { - return new UdpSocket(retVal, new IPEndPoint(remoteAddress, remotePort)); - } - catch - { - retVal?.Dispose(); - - throw; - } - } - /// <summary> /// Creates a new UDP acceptSocket and binds it to the specified local port. /// </summary> diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodingTempTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodingTempTask.cs new file mode 100644 index 000000000..ad9b56535 --- /dev/null +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodingTempTask.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.Tasks; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.ScheduledTasks.Tasks +{ + /// <summary> + /// Deletes all transcoding temp files + /// </summary> + public class DeleteTranscodingTempTask : IScheduledTask, IConfigurableScheduledTask + { + /// <summary> + /// Gets or sets the application paths. + /// </summary> + /// <value>The application paths.</value> + protected ServerApplicationPaths ApplicationPaths { get; set; } + + + private readonly ILogger _logger; + + private readonly IFileSystem _fileSystem; + + /// <summary> + /// Initializes a new instance of the <see cref="DeleteTranscodingTempTask" /> class. + /// </summary> + public DeleteTranscodingTempTask(ServerApplicationPaths appPaths, ILogger logger, IFileSystem fileSystem) + { + ApplicationPaths = appPaths; + _logger = logger; + _fileSystem = fileSystem; + } + + /// <summary> + /// Creates the triggers that define when the task will run + /// </summary> + /// <returns>IEnumerable{BaseTaskTrigger}.</returns> + public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() => new List<TaskTriggerInfo>(); + + /// <summary> + /// Returns the task to be executed + /// </summary> + /// <param name="cancellationToken">The cancellation token.</param> + /// <param name="progress">The progress.</param> + /// <returns>Task.</returns> + public Task Execute(CancellationToken cancellationToken, IProgress<double> progress) + { + var minDateModified = DateTime.UtcNow.AddDays(-1); + progress.Report(50); + + try + { + DeleteTempFilesFromDirectory(cancellationToken, ApplicationPaths.TranscodingTempPath, minDateModified, progress); + } + catch (DirectoryNotFoundException) + { + // No biggie here. Nothing to delete + } + + return Task.CompletedTask; + } + + + /// <summary> + /// Deletes the transcoded temp files from directory with a last write time less than a given date + /// </summary> + /// <param name="cancellationToken">The task cancellation token.</param> + /// <param name="directory">The directory.</param> + /// <param name="minDateModified">The min date modified.</param> + /// <param name="progress">The progress.</param> + private void DeleteTempFilesFromDirectory(CancellationToken cancellationToken, string directory, DateTime minDateModified, IProgress<double> progress) + { + var filesToDelete = _fileSystem.GetFiles(directory, true) + .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified) + .ToList(); + + var index = 0; + + foreach (var file in filesToDelete) + { + double percent = index; + percent /= filesToDelete.Count; + + progress.Report(100 * percent); + + cancellationToken.ThrowIfCancellationRequested(); + + DeleteFile(file.FullName); + + index++; + } + + DeleteEmptyFolders(directory); + + progress.Report(100); + } + + private void DeleteEmptyFolders(string parent) + { + foreach (var directory in _fileSystem.GetDirectoryPaths(parent)) + { + DeleteEmptyFolders(directory); + if (!_fileSystem.GetFileSystemEntryPaths(directory).Any()) + { + try + { + Directory.Delete(directory, false); + } + catch (UnauthorizedAccessException ex) + { + _logger.LogError(ex, "Error deleting directory {path}", directory); + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting directory {path}", directory); + } + } + } + } + + private void DeleteFile(string path) + { + try + { + _fileSystem.DeleteFile(path); + } + catch (UnauthorizedAccessException ex) + { + _logger.LogError(ex, "Error deleting file {path}", path); + } + catch (IOException ex) + { + _logger.LogError(ex, "Error deleting file {path}", path); + } + } + + public string Name => "Transcoding temp cleanup"; + + public string Description => "Deletes transcoding temp files older than 24 hours."; + + public string Category => "Maintenance"; + + public string Key => "DeleteTranscodingTempFiles"; + + public bool IsHidden => false; + + public bool IsEnabled => false; + + public bool IsLogged => true; + } +} diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs index c6431c311..7afeba9dd 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs @@ -1,4 +1,3 @@ -using MediaBrowser.Common; using MediaBrowser.Common.Updates; using MediaBrowser.Model.Net; using System; @@ -25,13 +24,10 @@ namespace Emby.Server.Implementations.ScheduledTasks private readonly IInstallationManager _installationManager; - private readonly IApplicationHost _appHost; - - public PluginUpdateTask(ILogger logger, IInstallationManager installationManager, IApplicationHost appHost) + public PluginUpdateTask(ILogger logger, IInstallationManager installationManager) { _logger = logger; _installationManager = installationManager; - _appHost = appHost; } /// <summary> @@ -40,14 +36,11 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <returns>IEnumerable{BaseTaskTrigger}.</returns> public IEnumerable<TaskTriggerInfo> GetDefaultTriggers() { - return new[] { - - // At startup - new TaskTriggerInfo {Type = TaskTriggerInfo.TriggerStartup}, - - // Every so often - new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks} - }; + // At startup + yield return new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerStartup }; + + // Every so often + yield return new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }; } /// <summary> @@ -72,7 +65,7 @@ namespace Emby.Server.Implementations.ScheduledTasks try { - await _installationManager.InstallPackage(package, true, new SimpleProgress<double>(), cancellationToken).ConfigureAwait(false); + await _installationManager.InstallPackage(package, cancellationToken).ConfigureAwait(false); } catch (OperationCanceledException) { @@ -94,8 +87,7 @@ namespace Emby.Server.Implementations.ScheduledTasks // Update progress lock (progress) { - numComplete++; - progress.Report(90.0 * numComplete / packagesToInstall.Count + 10); + progress.Report((90.0 * ++numComplete / packagesToInstall.Count) + 10); } } diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 6833c20c3..2f84b91ec 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -3,13 +3,15 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net.Http; +using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Extensions; using MediaBrowser.Common.Net; using MediaBrowser.Common.Plugins; -using MediaBrowser.Common.Progress; using MediaBrowser.Common.Updates; using MediaBrowser.Controller.Configuration; using MediaBrowser.Model.Events; @@ -33,7 +35,7 @@ namespace Emby.Server.Implementations.Updates /// <summary> /// The current installations /// </summary> - public List<Tuple<InstallationInfo, CancellationTokenSource>> CurrentInstallations { get; set; } + private List<(InstallationInfo info, CancellationTokenSource token)> _currentInstallations { get; set; } /// <summary> /// The completed installations @@ -48,48 +50,14 @@ namespace Emby.Server.Implementations.Updates public event EventHandler<GenericEventArgs<IPlugin>> PluginUninstalled; /// <summary> - /// Called when [plugin uninstalled]. - /// </summary> - /// <param name="plugin">The plugin.</param> - private void OnPluginUninstalled(IPlugin plugin) - { - PluginUninstalled?.Invoke(this, new GenericEventArgs<IPlugin> { Argument = plugin }); - } - - /// <summary> /// Occurs when [plugin updated]. /// </summary> - public event EventHandler<GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>>> PluginUpdated; - /// <summary> - /// Called when [plugin updated]. - /// </summary> - /// <param name="plugin">The plugin.</param> - /// <param name="newVersion">The new version.</param> - private void OnPluginUpdated(IPlugin plugin, PackageVersionInfo newVersion) - { - _logger.LogInformation("Plugin updated: {0} {1} {2}", newVersion.name, newVersion.versionStr ?? string.Empty, newVersion.classification); - - PluginUpdated?.Invoke(this, new GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>> { Argument = new Tuple<IPlugin, PackageVersionInfo>(plugin, newVersion) }); - - _applicationHost.NotifyPendingRestart(); - } + public event EventHandler<GenericEventArgs<(IPlugin, PackageVersionInfo)>> PluginUpdated; /// <summary> /// Occurs when [plugin updated]. /// </summary> public event EventHandler<GenericEventArgs<PackageVersionInfo>> PluginInstalled; - /// <summary> - /// Called when [plugin installed]. - /// </summary> - /// <param name="package">The package.</param> - private void OnPluginInstalled(PackageVersionInfo package) - { - _logger.LogInformation("New plugin installed: {0} {1} {2}", package.name, package.versionStr ?? string.Empty, package.classification); - - PluginInstalled?.Invoke(this, new GenericEventArgs<PackageVersionInfo> { Argument = package }); - - _applicationHost.NotifyPendingRestart(); - } /// <summary> /// The _logger @@ -111,7 +79,7 @@ namespace Emby.Server.Implementations.Updates private readonly IZipClient _zipClient; public InstallationManager( - ILoggerFactory loggerFactory, + ILogger<InstallationManager> logger, IApplicationHost appHost, IApplicationPaths appPaths, IHttpClient httpClient, @@ -120,15 +88,15 @@ namespace Emby.Server.Implementations.Updates IFileSystem fileSystem, IZipClient zipClient) { - if (loggerFactory == null) + if (logger == null) { - throw new ArgumentNullException(nameof(loggerFactory)); + throw new ArgumentNullException(nameof(logger)); } - CurrentInstallations = new List<Tuple<InstallationInfo, CancellationTokenSource>>(); + _currentInstallations = new List<(InstallationInfo, CancellationTokenSource)>(); _completedInstallationsInternal = new ConcurrentBag<InstallationInfo>(); - _logger = loggerFactory.CreateLogger(nameof(InstallationManager)); + _logger = logger; _applicationHost = appHost; _appPaths = appPaths; _httpClient = httpClient; @@ -138,21 +106,12 @@ namespace Emby.Server.Implementations.Updates _zipClient = zipClient; } - private static Version GetPackageVersion(PackageVersionInfo version) - { - return new Version(ValueOrDefault(version.versionStr, "0.0.0.1")); - } - - private static string ValueOrDefault(string str, string def) - { - return string.IsNullOrEmpty(str) ? def : str; - } - /// <summary> /// Gets all available packages. /// </summary> /// <returns>Task{List{PackageInfo}}.</returns> - public async Task<List<PackageInfo>> GetAvailablePackages(CancellationToken cancellationToken, + public async Task<List<PackageInfo>> GetAvailablePackages( + CancellationToken cancellationToken, bool withRegistration = true, string packageType = null, Version applicationVersion = null) @@ -168,26 +127,21 @@ namespace Emby.Server.Implementations.Updates /// <returns>Task{List{PackageInfo}}.</returns> public async Task<List<PackageInfo>> GetAvailablePackagesWithoutRegistrationInfo(CancellationToken cancellationToken) { - using (var response = await _httpClient.SendAsync(new HttpRequestOptions - { - Url = "https://repo.jellyfin.org/releases/plugin/manifest.json", - CancellationToken = cancellationToken, - Progress = new SimpleProgress<double>(), - CacheLength = GetCacheLength() - }, "GET").ConfigureAwait(false)) - { - using (var stream = response.Content) + using (var response = await _httpClient.SendAsync( + new HttpRequestOptions { - return FilterPackages(await _jsonSerializer.DeserializeFromStreamAsync<PackageInfo[]>(stream).ConfigureAwait(false)); - } + Url = "https://repo.jellyfin.org/releases/plugin/manifest.json", + CancellationToken = cancellationToken, + CacheMode = CacheMode.Unconditional, + CacheLength = GetCacheLength() + }, + HttpMethod.Get).ConfigureAwait(false)) + using (Stream stream = response.Content) + { + return FilterPackages(await _jsonSerializer.DeserializeFromStreamAsync<PackageInfo[]>(stream).ConfigureAwait(false)); } } - private PackageVersionClass GetSystemUpdateLevel() - { - return _applicationHost.SystemUpdateLevel; - } - private static TimeSpan GetCacheLength() { return TimeSpan.FromMinutes(3); @@ -211,7 +165,7 @@ namespace Emby.Server.Implementations.Updates } package.versions = versions - .OrderByDescending(GetPackageVersion) + .OrderByDescending(x => x.Version) .ToArray(); if (package.versions.Length == 0) @@ -294,7 +248,7 @@ namespace Emby.Server.Implementations.Updates return null; } - return package.versions.FirstOrDefault(v => GetPackageVersion(v).Equals(version) && v.classification == classification); + return package.versions.FirstOrDefault(v => v.Version == version && v.classification == classification); } /// <summary> @@ -325,13 +279,8 @@ namespace Emby.Server.Implementations.Updates var package = availablePackages.FirstOrDefault(p => string.Equals(p.guid, guid ?? "none", StringComparison.OrdinalIgnoreCase)) ?? availablePackages.FirstOrDefault(p => p.name.Equals(name, StringComparison.OrdinalIgnoreCase)); - if (package == null) - { - return null; - } - - return package.versions - .OrderByDescending(GetPackageVersion) + return package?.versions + .OrderByDescending(x => x.Version) .FirstOrDefault(v => v.classification <= classification && IsPackageVersionUpToDate(v, currentServerVersion)); } @@ -346,40 +295,26 @@ namespace Emby.Server.Implementations.Updates { var catalog = await GetAvailablePackagesWithoutRegistrationInfo(cancellationToken).ConfigureAwait(false); - var systemUpdateLevel = GetSystemUpdateLevel(); + var systemUpdateLevel = _applicationHost.SystemUpdateLevel; // Figure out what needs to be installed return _applicationHost.Plugins.Select(p => { var latestPluginInfo = GetLatestCompatibleVersion(catalog, p.Name, p.Id.ToString(), applicationVersion, systemUpdateLevel); - return latestPluginInfo != null && GetPackageVersion(latestPluginInfo) > p.Version ? latestPluginInfo : null; - + return latestPluginInfo != null && latestPluginInfo.Version > p.Version ? latestPluginInfo : null; }).Where(i => i != null) .Where(p => !string.IsNullOrEmpty(p.sourceUrl) && !CompletedInstallations.Any(i => string.Equals(i.AssemblyGuid, p.guid, StringComparison.OrdinalIgnoreCase))); } - /// <summary> - /// Installs the package. - /// </summary> - /// <param name="package">The package.</param> - /// <param name="isPlugin">if set to <c>true</c> [is plugin].</param> - /// <param name="progress">The progress.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException">package</exception> - public async Task InstallPackage(PackageVersionInfo package, bool isPlugin, IProgress<double> progress, CancellationToken cancellationToken) + /// <inheritdoc /> + public async Task InstallPackage(PackageVersionInfo package, CancellationToken cancellationToken) { if (package == null) { throw new ArgumentNullException(nameof(package)); } - if (progress == null) - { - throw new ArgumentNullException(nameof(progress)); - } - var installationInfo = new InstallationInfo { Id = Guid.NewGuid(), @@ -391,24 +326,14 @@ namespace Emby.Server.Implementations.Updates var innerCancellationTokenSource = new CancellationTokenSource(); - var tuple = new Tuple<InstallationInfo, CancellationTokenSource>(installationInfo, innerCancellationTokenSource); + var tuple = (installationInfo, innerCancellationTokenSource); // Add it to the in-progress list - lock (CurrentInstallations) + lock (_currentInstallations) { - CurrentInstallations.Add(tuple); + _currentInstallations.Add(tuple); } - var innerProgress = new ActionableProgress<double>(); - - // Whenever the progress updates, update the outer progress object and InstallationInfo - innerProgress.RegisterAction(percent => - { - progress.Report(percent); - - installationInfo.PercentComplete = percent; - }); - var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, innerCancellationTokenSource.Token).Token; var installationEventArgs = new InstallationEventArgs @@ -421,11 +346,11 @@ namespace Emby.Server.Implementations.Updates try { - await InstallPackageInternal(package, isPlugin, innerProgress, linkedToken).ConfigureAwait(false); + await InstallPackageInternal(package, linkedToken).ConfigureAwait(false); - lock (CurrentInstallations) + lock (_currentInstallations) { - CurrentInstallations.Remove(tuple); + _currentInstallations.Remove(tuple); } _completedInstallationsInternal.Add(installationInfo); @@ -434,9 +359,9 @@ namespace Emby.Server.Implementations.Updates } catch (OperationCanceledException) { - lock (CurrentInstallations) + lock (_currentInstallations) { - CurrentInstallations.Remove(tuple); + _currentInstallations.Remove(tuple); } _logger.LogInformation("Package installation cancelled: {0} {1}", package.name, package.versionStr); @@ -449,9 +374,9 @@ namespace Emby.Server.Implementations.Updates { _logger.LogError(ex, "Package installation failed"); - lock (CurrentInstallations) + lock (_currentInstallations) { - CurrentInstallations.Remove(tuple); + _currentInstallations.Remove(tuple); } PackageInstallationFailed?.Invoke(this, new InstallationFailedEventArgs @@ -473,110 +398,85 @@ namespace Emby.Server.Implementations.Updates /// Installs the package internal. /// </summary> /// <param name="package">The package.</param> - /// <param name="isPlugin">if set to <c>true</c> [is plugin].</param> - /// <param name="progress">The progress.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - private async Task InstallPackageInternal(PackageVersionInfo package, bool isPlugin, IProgress<double> progress, CancellationToken cancellationToken) + /// <returns><see cref="Task" />.</returns> + private async Task InstallPackageInternal(PackageVersionInfo package, CancellationToken cancellationToken) { - IPlugin plugin = null; - - if (isPlugin) - { - // Set last update time if we were installed before - plugin = _applicationHost.Plugins.FirstOrDefault(p => string.Equals(p.Id.ToString(), package.guid, StringComparison.OrdinalIgnoreCase)) + // Set last update time if we were installed before + IPlugin plugin = _applicationHost.Plugins.FirstOrDefault(p => string.Equals(p.Id.ToString(), package.guid, StringComparison.OrdinalIgnoreCase)) ?? _applicationHost.Plugins.FirstOrDefault(p => p.Name.Equals(package.name, StringComparison.OrdinalIgnoreCase)); - } - - string targetPath = plugin == null ? null : plugin.AssemblyFilePath; // Do the install - await PerformPackageInstallation(progress, targetPath, package, cancellationToken).ConfigureAwait(false); + await PerformPackageInstallation(package, cancellationToken).ConfigureAwait(false); // Do plugin-specific processing - if (isPlugin) + if (plugin == null) { - if (plugin == null) - { - OnPluginInstalled(package); - } - else - { - OnPluginUpdated(plugin, package); - } + _logger.LogInformation("New plugin installed: {0} {1} {2}", package.name, package.versionStr ?? string.Empty, package.classification); + + PluginInstalled?.Invoke(this, new GenericEventArgs<PackageVersionInfo>(package)); + } + else + { + _logger.LogInformation("Plugin updated: {0} {1} {2}", package.name, package.versionStr ?? string.Empty, package.classification); + + PluginUpdated?.Invoke(this, new GenericEventArgs<(IPlugin, PackageVersionInfo)>((plugin, package))); } + + _applicationHost.NotifyPendingRestart(); } - private async Task PerformPackageInstallation(IProgress<double> progress, string target, PackageVersionInfo package, CancellationToken cancellationToken) + private async Task PerformPackageInstallation(PackageVersionInfo package, CancellationToken cancellationToken) { - // TODO: Remove the `string target` argument as it is not used any longer - var extension = Path.GetExtension(package.targetFilename); - var isArchive = string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase); - - if (!isArchive) + if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase)) { _logger.LogError("Only zip packages are supported. {Filename} is not a zip archive.", package.targetFilename); return; } // Always override the passed-in target (which is a file) and figure it out again - target = Path.Combine(_appPaths.PluginsPath, package.name); - _logger.LogDebug("Installing plugin to {Filename}.", target); + string targetDir = Path.Combine(_appPaths.PluginsPath, package.name); - // Download to temporary file so that, if interrupted, it won't destroy the existing installation - _logger.LogDebug("Downloading ZIP."); - var tempFile = await _httpClient.GetTempFile(new HttpRequestOptions - { - Url = package.sourceUrl, - CancellationToken = cancellationToken, - Progress = progress - - }).ConfigureAwait(false); - - cancellationToken.ThrowIfCancellationRequested(); - - // TODO: Validate with a checksum, *properly* - - // Check if the target directory already exists, and remove it if so - if (Directory.Exists(target)) - { - _logger.LogDebug("Deleting existing plugin at {Filename}.", target); - Directory.Delete(target, true); - } +// CA5351: Do Not Use Broken Cryptographic Algorithms +#pragma warning disable CA5351 + using (var res = await _httpClient.SendAsync( + new HttpRequestOptions + { + Url = package.sourceUrl, + CancellationToken = cancellationToken, + // We need it to be buffered for setting the position + BufferContent = true + }, + HttpMethod.Get).ConfigureAwait(false)) + using (var stream = res.Content) + using (var md5 = MD5.Create()) + { + cancellationToken.ThrowIfCancellationRequested(); + + var hash = HexHelper.ToHexString(md5.ComputeHash(stream)); + if (!string.Equals(package.checksum, hash, StringComparison.OrdinalIgnoreCase)) + { + _logger.LogDebug("{0}, {1}", package.checksum, hash); + throw new InvalidDataException($"The checksums didn't match while installing {package.name}."); + } - // Success - move it to the real target - try - { - _logger.LogDebug("Extracting ZIP {TempFile} to {Filename}.", tempFile, target); - using (var stream = File.OpenRead(tempFile)) + if (Directory.Exists(targetDir)) { - _zipClient.ExtractAllFromZip(stream, target, true); + Directory.Delete(targetDir); } - } - catch (IOException ex) - { - _logger.LogError(ex, "Error attempting to extract {TempFile} to {TargetFile}", tempFile, target); - throw; - } - try - { - _logger.LogDebug("Deleting temporary file {Filename}.", tempFile); - _fileSystem.DeleteFile(tempFile); - } - catch (IOException ex) - { - // Don't fail because of this - _logger.LogError(ex, "Error deleting temp file {TempFile}", tempFile); + stream.Position = 0; + _zipClient.ExtractAllFromZip(stream, targetDir, true); } + +#pragma warning restore CA5351 } /// <summary> /// Uninstalls a plugin /// </summary> /// <param name="plugin">The plugin.</param> - /// <exception cref="ArgumentException"></exception> public void UninstallPlugin(IPlugin plugin) { plugin.OnUninstalling(); @@ -622,11 +522,34 @@ namespace Emby.Server.Implementations.Updates _config.SaveConfiguration(); } - OnPluginUninstalled(plugin); + PluginUninstalled?.Invoke(this, new GenericEventArgs<IPlugin> { Argument = plugin }); _applicationHost.NotifyPendingRestart(); } + /// <inheritdoc/> + public bool CancelInstallation(Guid id) + { + lock (_currentInstallations) + { + var install = _currentInstallations.Find(x => x.Item1.Id == id); + if (install == default((InstallationInfo, CancellationTokenSource))) + { + return false; + } + + install.Item2.Cancel(); + _currentInstallations.Remove(install); + return true; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + /// <summary> /// Releases unmanaged and - optionally - managed resources. /// </summary> @@ -635,21 +558,16 @@ namespace Emby.Server.Implementations.Updates { if (dispose) { - lock (CurrentInstallations) + lock (_currentInstallations) { - foreach (var tuple in CurrentInstallations) + foreach (var tuple in _currentInstallations) { tuple.Item2.Dispose(); } - CurrentInstallations.Clear(); + _currentInstallations.Clear(); } } } - - public void Dispose() - { - Dispose(true); - } } } diff --git a/Emby.XmlTv/Emby.XmlTv/Emby.XmlTv.csproj b/Emby.XmlTv/Emby.XmlTv/Emby.XmlTv.csproj index 0225be2c2..04f558173 100644 --- a/Emby.XmlTv/Emby.XmlTv/Emby.XmlTv.csproj +++ b/Emby.XmlTv/Emby.XmlTv/Emby.XmlTv.csproj @@ -3,6 +3,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> <ItemGroup> diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index f023bc55d..396bdd4b7 100644 --- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -3,6 +3,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> <ItemGroup> diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index b9b0cc382..8b4b61e29 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -9,8 +9,21 @@ using Microsoft.Extensions.Logging; namespace Jellyfin.Server { + /// <summary> + /// Implementation of the abstract <see cref="ApplicationHost" /> class. + /// </summary> public class CoreAppHost : ApplicationHost { + /// <summary> + /// Initializes a new instance of the <see cref="CoreAppHost" /> class. + /// </summary> + /// <param name="applicationPaths">The <see cref="ServerApplicationPaths" /> to be used by the <see cref="CoreAppHost" />.</param> + /// <param name="loggerFactory">The <see cref="ILoggerFactory" /> to be used by the <see cref="CoreAppHost" />.</param> + /// <param name="options">The <see cref="StartupOptions" /> to be used by the <see cref="CoreAppHost" />.</param> + /// <param name="fileSystem">The <see cref="IFileSystem" /> to be used by the <see cref="CoreAppHost" />.</param> + /// <param name="imageEncoder">The <see cref="IImageEncoder" /> to be used by the <see cref="CoreAppHost" />.</param> + /// <param name="networkManager">The <see cref="INetworkManager" /> to be used by the <see cref="CoreAppHost" />.</param> + /// <param name="configuration">The <see cref="IConfiguration" /> to be used by the <see cref="CoreAppHost" />.</param> public CoreAppHost( ServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, @@ -30,15 +43,19 @@ namespace Jellyfin.Server { } + /// <inheritdoc /> public override bool CanSelfRestart => StartupOptions.RestartPath != null; + /// <inheritdoc /> protected override void RestartInternal() => Program.Restart(); + /// <inheritdoc /> protected override IEnumerable<Assembly> GetAssembliesWithPartsInternal() { yield return typeof(CoreAppHost).Assembly; } + /// <inheritdoc /> protected override void ShutdownInternal() => Program.Shutdown(); } } diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 641b3f182..ec7c026e5 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -9,10 +9,8 @@ </PropertyGroup> <PropertyGroup> - <!-- We need C# 7.1 for async main--> + <!-- We need at least C# 7.1 for async main--> <LangVersion>latest</LangVersion> - <!-- Disable documentation warnings (for now) --> - <NoWarn>SA1600;SA1601;SA1629;CS1591</NoWarn> <TreatWarningsAsErrors>true</TreatWarningsAsErrors> </PropertyGroup> @@ -26,7 +24,7 @@ <!-- Code analysers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> - <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.3" /> + <PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.4" /> <PackageReference Include="StyleCop.Analyzers" Version="1.1.118" /> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" /> </ItemGroup> @@ -36,7 +34,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="CommandLineParser" Version="2.5.0" /> + <PackageReference Include="CommandLineParser" Version="2.6.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="2.2.4" /> <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0" /> <PackageReference Include="Serilog.AspNetCore" Version="2.1.1" /> diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 952990493..5e4e36a34 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -28,6 +28,9 @@ using ILogger = Microsoft.Extensions.Logging.ILogger; namespace Jellyfin.Server { + /// <summary> + /// Class containing the entry point of the application. + /// </summary> public static class Program { private static readonly CancellationTokenSource _tokenSource = new CancellationTokenSource(); @@ -35,6 +38,11 @@ namespace Jellyfin.Server private static ILogger _logger; private static bool _restartOnShutdown; + /// <summary> + /// The entry point of the application. + /// </summary> + /// <param name="args">The command line arguments passed.</param> + /// <returns><see cref="Task" />.</returns> public static Task Main(string[] args) { // For backwards compatibility. @@ -53,7 +61,10 @@ namespace Jellyfin.Server .MapResult(StartApp, _ => Task.CompletedTask); } - public static void Shutdown() + /// <summary> + /// Shuts down the application. + /// </summary> + internal static void Shutdown() { if (!_tokenSource.IsCancellationRequested) { @@ -61,7 +72,10 @@ namespace Jellyfin.Server } } - public static void Restart() + /// <summary> + /// Restarts the application. + /// </summary> + internal static void Restart() { _restartOnShutdown = true; @@ -171,11 +185,12 @@ namespace Jellyfin.Server /// <summary> /// Create the data, config and log paths from the variety of inputs(command line args, /// environment variables) or decide on what default to use. For Windows it's %AppPath% - /// for everything else the XDG approach is followed: - /// https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html + /// for everything else the + /// <a href="https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html">XDG approach</a> + /// is followed. /// </summary> - /// <param name="options">StartupOptions</param> - /// <returns>ServerApplicationPaths</returns> + /// <param name="options">The <see cref="StartupOptions" /> for this instance.</param> + /// <returns><see cref="ServerApplicationPaths" />.</returns> private static ServerApplicationPaths CreateApplicationPaths(StartupOptions options) { // dataDir diff --git a/Jellyfin.Server/StartupOptions.cs b/Jellyfin.Server/StartupOptions.cs index 8296d414e..bb0adaf63 100644 --- a/Jellyfin.Server/StartupOptions.cs +++ b/Jellyfin.Server/StartupOptions.cs @@ -8,36 +8,62 @@ namespace Jellyfin.Server /// </summary> public class StartupOptions : IStartupOptions { + /// <summary> + /// Gets or sets the path to the data directory. + /// </summary> + /// <value>The path to the data directory.</value> [Option('d', "datadir", Required = false, HelpText = "Path to use for the data folder (database files, etc.).")] public string DataDir { get; set; } + /// <summary> + /// Gets or sets the path to the web directory. + /// </summary> + /// <value>The path to the web directory.</value> [Option('w', "webdir", Required = false, HelpText = "Path to the Jellyfin web UI resources.")] public string WebDir { get; set; } + /// <summary> + /// Gets or sets the path to the cache directory. + /// </summary> + /// <value>The path to the cache directory.</value> [Option('C', "cachedir", Required = false, HelpText = "Path to use for caching.")] public string CacheDir { get; set; } + /// <summary> + /// Gets or sets the path to the config directory. + /// </summary> + /// <value>The path to the config directory.</value> [Option('c', "configdir", Required = false, HelpText = "Path to use for configuration data (user settings and pictures).")] public string ConfigDir { get; set; } + /// <summary> + /// Gets or sets the path to the log directory. + /// </summary> + /// <value>The path to the log directory.</value> [Option('l', "logdir", Required = false, HelpText = "Path to use for writing log files.")] public string LogDir { get; set; } + /// <inheritdoc /> [Option("ffmpeg", Required = false, HelpText = "Path to external FFmpeg executable to use in place of default found in PATH.")] public string FFmpegPath { get; set; } + /// <inheritdoc /> [Option("service", Required = false, HelpText = "Run as headless service.")] public bool IsService { get; set; } + /// <inheritdoc /> [Option("noautorunwebapp", Required = false, HelpText = "Run headless if startup wizard is complete.")] public bool NoAutoRunWebApp { get; set; } + /// <inheritdoc /> [Option("package-name", Required = false, HelpText = "Used when packaging Jellyfin (example, synology).")] public string PackageName { get; set; } + /// <inheritdoc /> [Option("restartpath", Required = false, HelpText = "Path to restart script.")] public string RestartPath { get; set; } + /// <inheritdoc /> [Option("restartargs", Required = false, HelpText = "Arguments for restart script.")] public string RestartArgs { get; set; } } diff --git a/MediaBrowser.Api/Images/ImageService.cs b/MediaBrowser.Api/Images/ImageService.cs index 23c7339d2..6d3037b24 100644 --- a/MediaBrowser.Api/Images/ImageService.cs +++ b/MediaBrowser.Api/Images/ImageService.cs @@ -550,14 +550,14 @@ namespace MediaBrowser.Api.Images } IImageEnhancer[] supportedImageEnhancers; - if (_imageProcessor.ImageEnhancers.Length > 0) + if (_imageProcessor.ImageEnhancers.Count > 0) { if (item == null) { item = _libraryManager.GetItemById(itemId); } - supportedImageEnhancers = request.EnableImageEnhancers ? _imageProcessor.GetSupportedEnhancers(item, request.Type) : Array.Empty<IImageEnhancer>(); + supportedImageEnhancers = request.EnableImageEnhancers ? _imageProcessor.GetSupportedEnhancers(item, request.Type).ToArray() : Array.Empty<IImageEnhancer>(); } else { @@ -606,8 +606,8 @@ namespace MediaBrowser.Api.Images ImageRequest request, ItemImageInfo image, bool cropwhitespace, - ImageFormat[] supportedFormats, - IImageEnhancer[] enhancers, + IReadOnlyCollection<ImageFormat> supportedFormats, + IReadOnlyCollection<IImageEnhancer> enhancers, TimeSpan? cacheDuration, IDictionary<string, string> headers, bool isHeadRequest) diff --git a/MediaBrowser.Api/MediaBrowser.Api.csproj b/MediaBrowser.Api/MediaBrowser.Api.csproj index ba29c656b..f653270a6 100644 --- a/MediaBrowser.Api/MediaBrowser.Api.csproj +++ b/MediaBrowser.Api/MediaBrowser.Api.csproj @@ -12,6 +12,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> </Project> diff --git a/MediaBrowser.Api/PackageService.cs b/MediaBrowser.Api/PackageService.cs index fbb876dea..baa6f7bb9 100644 --- a/MediaBrowser.Api/PackageService.cs +++ b/MediaBrowser.Api/PackageService.cs @@ -197,7 +197,7 @@ namespace MediaBrowser.Api throw new ResourceNotFoundException(string.Format("Package not found: {0}", request.Name)); } - await _installationManager.InstallPackage(package, true, new SimpleProgress<double>(), CancellationToken.None); + await _installationManager.InstallPackage(package); } /// <summary> @@ -206,13 +206,7 @@ namespace MediaBrowser.Api /// <param name="request">The request.</param> public void Delete(CancelPackageInstallation request) { - var info = _installationManager.CurrentInstallations.FirstOrDefault(i => i.Item1.Id.Equals(request.Id)); - - if (info != null) - { - info.Item2.Cancel(); - } + _installationManager.CancelInstallation(new Guid(request.Id)); } } - } diff --git a/MediaBrowser.Api/Playback/BaseStreamingService.cs b/MediaBrowser.Api/Playback/BaseStreamingService.cs index 77b733dc9..1f78f5afc 100644 --- a/MediaBrowser.Api/Playback/BaseStreamingService.cs +++ b/MediaBrowser.Api/Playback/BaseStreamingService.cs @@ -690,8 +690,8 @@ namespace MediaBrowser.Api.Playback request.AudioCodec = EncodingHelper.InferAudioCodec(url); } - var enableDlnaHeaders = !string.IsNullOrWhiteSpace(request.Params) /*|| - string.Equals(Request.Headers.Get("GetContentFeatures.DLNA.ORG"), "1", StringComparison.OrdinalIgnoreCase)*/; + var enableDlnaHeaders = !string.IsNullOrWhiteSpace(request.Params) || + string.Equals(GetHeader("GetContentFeatures.DLNA.ORG"), "1", StringComparison.OrdinalIgnoreCase); var state = new StreamState(MediaSourceManager, TranscodingJobType) { @@ -1016,11 +1016,6 @@ namespace MediaBrowser.Api.Playback ).FirstOrDefault() ?? string.Empty; } - - foreach (var item in responseHeaders) - { - Request.Response.Headers.Add(item.Key, item.Value); - } } private void AddTimeSeekResponseHeaders(StreamState state, IDictionary<string, string> responseHeaders) diff --git a/MediaBrowser.Common/Extensions/HexHelper.cs b/MediaBrowser.Common/Extensions/HexHelper.cs new file mode 100644 index 000000000..3d80d94ac --- /dev/null +++ b/MediaBrowser.Common/Extensions/HexHelper.cs @@ -0,0 +1,22 @@ +using System; +using System.Globalization; + +namespace MediaBrowser.Common.Extensions +{ + public static class HexHelper + { + public static byte[] FromHexString(string str) + { + byte[] bytes = new byte[str.Length / 2]; + for (int i = 0; i < str.Length; i += 2) + { + bytes[i / 2] = byte.Parse(str.Substring(i, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture); + } + + return bytes; + } + + public static string ToHexString(byte[] bytes) + => BitConverter.ToString(bytes).Replace("-", ""); + } +} diff --git a/MediaBrowser.Common/IApplicationHost.cs b/MediaBrowser.Common/IApplicationHost.cs index cb7343440..2248e9c85 100644 --- a/MediaBrowser.Common/IApplicationHost.cs +++ b/MediaBrowser.Common/IApplicationHost.cs @@ -75,10 +75,10 @@ namespace MediaBrowser.Common /// <summary> /// Gets the exports. /// </summary> - /// <typeparam name="T"></typeparam> - /// <param name="manageLiftime">if set to <c>true</c> [manage liftime].</param> - /// <returns>IEnumerable{``0}.</returns> - IEnumerable<T> GetExports<T>(bool manageLifetime = true); + /// <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> /// Resolves this instance. diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 05b48a2a1..91ab066f9 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -23,6 +23,12 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + </PropertyGroup> + + <PropertyGroup> + <!-- We need at least C# 7.1 for the "default literal" feature--> + <LangVersion>latest</LangVersion> </PropertyGroup> </Project> diff --git a/MediaBrowser.Common/Net/HttpRequestOptions.cs b/MediaBrowser.Common/Net/HttpRequestOptions.cs index 76bd35e57..94b972a02 100644 --- a/MediaBrowser.Common/Net/HttpRequestOptions.cs +++ b/MediaBrowser.Common/Net/HttpRequestOptions.cs @@ -64,12 +64,6 @@ namespace MediaBrowser.Common.Net set => RequestHeaders[HeaderNames.Host] = value; } - /// <summary> - /// Gets or sets the progress. - /// </summary> - /// <value>The progress.</value> - public IProgress<double> Progress { get; set; } - public Dictionary<string, string> RequestHeaders { get; private set; } public string RequestContentType { get; set; } @@ -79,10 +73,6 @@ namespace MediaBrowser.Common.Net public bool BufferContent { get; set; } - public bool LogRequest { get; set; } - public bool LogRequestAsDebug { get; set; } - public bool LogErrors { get; set; } - public bool LogErrorResponseBody { get; set; } public bool EnableKeepAlive { get; set; } @@ -105,8 +95,6 @@ namespace MediaBrowser.Common.Net { RequestHeaders = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); - LogRequest = true; - LogErrors = true; CacheMode = CacheMode.None; DecompressionMethod = CompressionMethod.Deflate; } diff --git a/MediaBrowser.Common/Net/IHttpClient.cs b/MediaBrowser.Common/Net/IHttpClient.cs index db69c6f2c..d84a4d664 100644 --- a/MediaBrowser.Common/Net/IHttpClient.cs +++ b/MediaBrowser.Common/Net/IHttpClient.cs @@ -47,21 +47,5 @@ namespace MediaBrowser.Common.Net /// <param name="options">The options.</param> /// <returns>Task{HttpResponseInfo}.</returns> Task<HttpResponseInfo> Post(HttpRequestOptions options); - - /// <summary> - /// Downloads the contents of a given url into a temporary location - /// </summary> - /// <param name="options">The options.</param> - /// <returns>Task{System.String}.</returns> - /// <exception cref="System.ArgumentNullException">progress</exception> - /// <exception cref="Model.Net.HttpException"></exception> - Task<string> GetTempFile(HttpRequestOptions options); - - /// <summary> - /// Gets the temporary file response. - /// </summary> - /// <param name="options">The options.</param> - /// <returns>Task{HttpResponseInfo}.</returns> - Task<HttpResponseInfo> GetTempFileResponse(HttpRequestOptions options); } } diff --git a/MediaBrowser.Common/Updates/IInstallationManager.cs b/MediaBrowser.Common/Updates/IInstallationManager.cs index a263be35f..88ac7e473 100644 --- a/MediaBrowser.Common/Updates/IInstallationManager.cs +++ b/MediaBrowser.Common/Updates/IInstallationManager.cs @@ -16,11 +16,6 @@ namespace MediaBrowser.Common.Updates event EventHandler<InstallationEventArgs> PackageInstallationCancelled; /// <summary> - /// The current installations - /// </summary> - List<Tuple<InstallationInfo, CancellationTokenSource>> CurrentInstallations { get; set; } - - /// <summary> /// The completed installations /// </summary> IEnumerable<InstallationInfo> CompletedInstallations { get; } @@ -33,7 +28,7 @@ namespace MediaBrowser.Common.Updates /// <summary> /// Occurs when [plugin updated]. /// </summary> - event EventHandler<GenericEventArgs<Tuple<IPlugin, PackageVersionInfo>>> PluginUpdated; + event EventHandler<GenericEventArgs<(IPlugin, PackageVersionInfo)>> PluginUpdated; /// <summary> /// Occurs when [plugin updated]. @@ -102,12 +97,9 @@ namespace MediaBrowser.Common.Updates /// Installs the package. /// </summary> /// <param name="package">The package.</param> - /// <param name="isPlugin">if set to <c>true</c> [is plugin].</param> - /// <param name="progress">The progress.</param> /// <param name="cancellationToken">The cancellation token.</param> - /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException">package</exception> - Task InstallPackage(PackageVersionInfo package, bool isPlugin, IProgress<double> progress, CancellationToken cancellationToken); + /// <returns><see cref="Task" />.</returns> + Task InstallPackage(PackageVersionInfo package, CancellationToken cancellationToken = default); /// <summary> /// Uninstalls a plugin @@ -115,5 +107,12 @@ namespace MediaBrowser.Common.Updates /// <param name="plugin">The plugin.</param> /// <exception cref="ArgumentException"></exception> void UninstallPlugin(IPlugin 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); } } diff --git a/MediaBrowser.Controller/Drawing/IImageEncoder.cs b/MediaBrowser.Controller/Drawing/IImageEncoder.cs index 4eaecd0a0..a0f9ae46e 100644 --- a/MediaBrowser.Controller/Drawing/IImageEncoder.cs +++ b/MediaBrowser.Controller/Drawing/IImageEncoder.cs @@ -18,16 +18,6 @@ namespace MediaBrowser.Controller.Drawing IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; } /// <summary> - /// Encodes the image. - /// </summary> - string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat); - - /// <summary> - /// Creates the image collage. - /// </summary> - /// <param name="options">The options.</param> - void CreateImageCollage(ImageCollageOptions options); - /// <summary> /// Gets the name. /// </summary> /// <value>The name.</value> @@ -46,5 +36,16 @@ namespace MediaBrowser.Controller.Drawing bool SupportsImageEncoding { get; } ImageDimensions GetImageSize(string path); + + /// <summary> + /// Encodes the image. + /// </summary> + string EncodeImage(string inputPath, DateTime dateModified, string outputPath, bool autoOrient, ImageOrientation? orientation, int quality, ImageProcessingOptions options, ImageFormat outputFormat); + + /// <summary> + /// Creates the image collage. + /// </summary> + /// <param name="options">The options.</param> + void CreateImageCollage(ImageCollageOptions options); } } diff --git a/MediaBrowser.Controller/Drawing/IImageProcessor.cs b/MediaBrowser.Controller/Drawing/IImageProcessor.cs index a11e2186f..a58a11bd1 100644 --- a/MediaBrowser.Controller/Drawing/IImageProcessor.cs +++ b/MediaBrowser.Controller/Drawing/IImageProcessor.cs @@ -24,7 +24,15 @@ namespace MediaBrowser.Controller.Drawing /// Gets the image enhancers. /// </summary> /// <value>The image enhancers.</value> - IImageEnhancer[] ImageEnhancers { get; } + IReadOnlyCollection<IImageEnhancer> ImageEnhancers { get; set; } + + /// <summary> + /// Gets a value indicating whether [supports image collage creation]. + /// </summary> + /// <value><c>true</c> if [supports image collage creation]; otherwise, <c>false</c>.</value> + bool SupportsImageCollageCreation { get; } + + IImageEncoder ImageEncoder { get; set; } /// <summary> /// Gets the dimensions of the image. @@ -51,18 +59,12 @@ namespace MediaBrowser.Controller.Drawing ImageDimensions GetImageDimensions(BaseItem item, ItemImageInfo info, bool updateItem); /// <summary> - /// Adds the parts. - /// </summary> - /// <param name="enhancers">The enhancers.</param> - void AddParts(IEnumerable<IImageEnhancer> enhancers); - - /// <summary> /// Gets the supported enhancers. /// </summary> /// <param name="item">The item.</param> /// <param name="imageType">Type of the image.</param> /// <returns>IEnumerable{IImageEnhancer}.</returns> - IImageEnhancer[] GetSupportedEnhancers(BaseItem item, ImageType imageType); + IEnumerable<IImageEnhancer> GetSupportedEnhancers(BaseItem item, ImageType imageType); /// <summary> /// Gets the image cache tag. @@ -80,7 +82,7 @@ namespace MediaBrowser.Controller.Drawing /// <param name="image">The image.</param> /// <param name="imageEnhancers">The image enhancers.</param> /// <returns>Guid.</returns> - string GetImageCacheTag(BaseItem item, ItemImageInfo image, IImageEnhancer[] imageEnhancers); + string GetImageCacheTag(BaseItem item, ItemImageInfo image, IReadOnlyCollection<IImageEnhancer> imageEnhancers); /// <summary> /// Processes the image. @@ -109,7 +111,7 @@ namespace MediaBrowser.Controller.Drawing /// <summary> /// Gets the supported image output formats. /// </summary> - /// <returns>IReadOnlyCollection{ImageOutput}.</returns> + /// <returns><see cref="IReadOnlyCollection{ImageOutput}" />.</returns> IReadOnlyCollection<ImageFormat> GetSupportedImageOutputFormats(); /// <summary> @@ -118,14 +120,6 @@ namespace MediaBrowser.Controller.Drawing /// <param name="options">The options.</param> void CreateImageCollage(ImageCollageOptions options); - /// <summary> - /// Gets a value indicating whether [supports image collage creation]. - /// </summary> - /// <value><c>true</c> if [supports image collage creation]; otherwise, <c>false</c>.</value> - bool SupportsImageCollageCreation { get; } - - IImageEncoder ImageEncoder { get; set; } - bool SupportsTransparency(string path); } } diff --git a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs index db432f500..29addf6e6 100644 --- a/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs +++ b/MediaBrowser.Controller/Drawing/ImageProcessingOptions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using MediaBrowser.Controller.Entities; @@ -33,9 +34,9 @@ namespace MediaBrowser.Controller.Drawing public int Quality { get; set; } - public IImageEnhancer[] Enhancers { get; set; } + public IReadOnlyCollection<IImageEnhancer> Enhancers { get; set; } - public ImageFormat[] SupportedOutputFormats { get; set; } + public IReadOnlyCollection<ImageFormat> SupportedOutputFormats { get; set; } public bool AddPlayedIndicator { get; set; } diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 01893f1b5..c6bca2518 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -19,6 +19,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> </Project> diff --git a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj index 867b82ede..a8f8da9b8 100644 --- a/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj +++ b/MediaBrowser.LocalMetadata/MediaBrowser.LocalMetadata.csproj @@ -12,6 +12,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> </Project> diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index c0f92ac4a..fdb20477f 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -3,6 +3,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> <ItemGroup> @@ -18,7 +19,7 @@ <ItemGroup> <PackageReference Include="System.Text.Encoding.CodePages" Version="4.5.1" /> - <PackageReference Include="UTF.Unknown" Version="1.0.0" /> + <PackageReference Include="UTF.Unknown" Version="2.0.0" /> </ItemGroup> </Project> diff --git a/MediaBrowser.Model/Cryptography/PasswordHash.cs b/MediaBrowser.Model/Cryptography/PasswordHash.cs index df32fdb00..4bcf0c117 100644 --- a/MediaBrowser.Model/Cryptography/PasswordHash.cs +++ b/MediaBrowser.Model/Cryptography/PasswordHash.cs @@ -84,6 +84,7 @@ namespace MediaBrowser.Model.Cryptography _hash = Array.Empty<Byte>(); } + // TODO: move this class and use the HexHelper class public static byte[] ConvertFromByteString(string byteString) { byte[] bytes = new byte[byteString.Length / 2]; diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 3de2cca2d..e9f43ea56 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -10,6 +10,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> <ItemGroup> diff --git a/MediaBrowser.Model/Net/ISocketFactory.cs b/MediaBrowser.Model/Net/ISocketFactory.cs index e58f4cc14..2f857f1af 100644 --- a/MediaBrowser.Model/Net/ISocketFactory.cs +++ b/MediaBrowser.Model/Net/ISocketFactory.cs @@ -17,8 +17,6 @@ namespace MediaBrowser.Model.Net ISocket CreateUdpBroadcastSocket(int localPort); - ISocket CreateTcpSocket(IPAddress remoteAddress, int remotePort); - /// <summary> /// Creates a new unicast socket using the specified local port number. /// </summary> @@ -35,14 +33,4 @@ namespace MediaBrowser.Model.Net Stream CreateNetworkStream(ISocket socket, bool ownsSocket); } - - public enum SocketType - { - Stream - } - - public enum ProtocolType - { - Tcp - } } diff --git a/MediaBrowser.Model/Updates/InstallationInfo.cs b/MediaBrowser.Model/Updates/InstallationInfo.cs index a3f19e236..7554e9fe2 100644 --- a/MediaBrowser.Model/Updates/InstallationInfo.cs +++ b/MediaBrowser.Model/Updates/InstallationInfo.cs @@ -36,11 +36,5 @@ namespace MediaBrowser.Model.Updates /// </summary> /// <value>The update class.</value> public PackageVersionClass UpdateClass { get; set; } - - /// <summary> - /// Gets or sets the percent complete. - /// </summary> - /// <value>The percent complete.</value> - public double? PercentComplete { get; set; } } } diff --git a/MediaBrowser.Model/Updates/PackageVersionInfo.cs b/MediaBrowser.Model/Updates/PackageVersionInfo.cs index be531770d..7ef07c0df 100644 --- a/MediaBrowser.Model/Updates/PackageVersionInfo.cs +++ b/MediaBrowser.Model/Updates/PackageVersionInfo.cs @@ -30,23 +30,25 @@ namespace MediaBrowser.Model.Updates /// The _version /// </summary> private Version _version; + /// <summary> /// Gets or sets the version. /// Had to make this an interpreted property since Protobuf can't handle Version /// </summary> /// <value>The version.</value> [IgnoreDataMember] - public Version version => _version ?? (_version = new Version(ValueOrDefault(versionStr, "0.0.0.1"))); - - /// <summary> - /// Values the or default. - /// </summary> - /// <param name="str">The STR.</param> - /// <param name="def">The def.</param> - /// <returns>System.String.</returns> - private static string ValueOrDefault(string str, string def) + public Version Version { - return string.IsNullOrEmpty(str) ? def : str; + get + { + if (_version == null) + { + var ver = versionStr; + _version = new Version(string.IsNullOrEmpty(ver) ? "0.0.0.1" : ver); + } + + return _version; + } } /// <summary> diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 5941ed436..ab4759c61 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -21,6 +21,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> </Project> diff --git a/MediaBrowser.Providers/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Omdb/OmdbProvider.cs index 19dce34d6..f8b876580 100644 --- a/MediaBrowser.Providers/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Omdb/OmdbProvider.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Net.Http; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -347,7 +348,7 @@ namespace MediaBrowser.Providers.Omdb CancellationToken = cancellationToken, BufferContent = true, EnableDefaultUserAgent = true - }, "GET"); + }, HttpMethod.Get); } internal string GetDataFilePath(string imdbId) diff --git a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs index 4b41589f1..ef412db5a 100644 --- a/MediaBrowser.Providers/Studios/StudiosImageProvider.cs +++ b/MediaBrowser.Providers/Studios/StudiosImageProvider.cs @@ -2,10 +2,10 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Common.Net; -using MediaBrowser.Common.Progress; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Providers; @@ -143,26 +143,20 @@ namespace MediaBrowser.Providers.Studios if (!fileInfo.Exists || (DateTime.UtcNow - fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays > 1) { - var temp = await httpClient.GetTempFile(new HttpRequestOptions - { - CancellationToken = cancellationToken, - Progress = new SimpleProgress<double>(), - Url = url - - }).ConfigureAwait(false); - Directory.CreateDirectory(Path.GetDirectoryName(file)); - try - { - File.Copy(temp, file, true); - } - catch + using (var res = await httpClient.SendAsync( + new HttpRequestOptions + { + CancellationToken = cancellationToken, + Url = url + }, + HttpMethod.Get).ConfigureAwait(false)) + using (var content = res.Content) + using (var fileStream = new FileStream(file, FileMode.Create)) { - + await content.CopyToAsync(fileStream).ConfigureAwait(false); } - - return temp; } return file; diff --git a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj index c099e77d6..883986894 100644 --- a/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj +++ b/MediaBrowser.WebDashboard/MediaBrowser.WebDashboard.csproj @@ -18,6 +18,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> </Project> diff --git a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj index ba29c656b..f653270a6 100644 --- a/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj +++ b/MediaBrowser.XbmcMetadata/MediaBrowser.XbmcMetadata.csproj @@ -12,6 +12,7 @@ <PropertyGroup> <TargetFramework>netstandard2.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> + <GenerateDocumentationFile>true</GenerateDocumentationFile> </PropertyGroup> </Project> diff --git a/Mono.Nat/Upnp/Messages/GetServicesMessage.cs b/Mono.Nat/Upnp/Messages/GetServicesMessage.cs index 72b4c2b25..f619f5ca4 100644 --- a/Mono.Nat/Upnp/Messages/GetServicesMessage.cs +++ b/Mono.Nat/Upnp/Messages/GetServicesMessage.cs @@ -25,50 +25,37 @@ // using System; -using System.Diagnostics; using System.Net; using MediaBrowser.Common.Net; -using Microsoft.Extensions.Logging; namespace Mono.Nat.Upnp { internal class GetServicesMessage : MessageBase { - private string servicesDescriptionUrl; - private EndPoint hostAddress; - private readonly ILogger _logger; + private string _servicesDescriptionUrl; + private EndPoint _hostAddress; - public GetServicesMessage(string description, EndPoint hostAddress, ILogger logger) + public GetServicesMessage(string description, EndPoint hostAddress) : base(null) { if (string.IsNullOrEmpty(description)) - _logger.LogWarning("Description is null"); - - if (hostAddress == null) - _logger.LogWarning("hostaddress is null"); - - this.servicesDescriptionUrl = description; - this.hostAddress = hostAddress; - _logger = logger; - } - - public override string Method - { - get { - return "GET"; + throw new ArgumentException("Description is null/empty", nameof(description)); } + + this._servicesDescriptionUrl = description; + this._hostAddress = hostAddress ?? throw new ArgumentNullException(nameof(hostAddress)); } + public override string Method => "GET"; + public override HttpRequestOptions Encode() { - var req = new HttpRequestOptions(); - - // The periodic request logging may keep some devices awake - req.LogRequestAsDebug = true; - req.LogErrors = false; + var req = new HttpRequestOptions() + { + Url = $"http://{this._hostAddress}{this._servicesDescriptionUrl}" + }; - req.Url = "http://" + this.hostAddress.ToString() + this.servicesDescriptionUrl; req.RequestHeaders.Add("ACCEPT-LANGUAGE", "en"); return req; diff --git a/Mono.Nat/Upnp/Messages/UpnpMessage.cs b/Mono.Nat/Upnp/Messages/UpnpMessage.cs index ade9df50b..d47241d4a 100644 --- a/Mono.Nat/Upnp/Messages/UpnpMessage.cs +++ b/Mono.Nat/Upnp/Messages/UpnpMessage.cs @@ -24,13 +24,8 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // -using System; -using System.Diagnostics; using System.Xml; -using System.Net; -using System.IO; using System.Text; -using System.Globalization; using MediaBrowser.Common.Net; namespace Mono.Nat.Upnp @@ -46,38 +41,31 @@ namespace Mono.Nat.Upnp protected HttpRequestOptions CreateRequest(string upnpMethod, string methodParameters) { - string ss = "http://" + this.device.HostEndPoint.ToString() + this.device.ControlUrl; + var req = new HttpRequestOptions() + { + Url = $"http://{this.device.HostEndPoint}{this.device.ControlUrl}", + EnableKeepAlive = false, + RequestContentType = "text/xml", + RequestContent = "<s:Envelope " + + "xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" " + + "s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" + + "<s:Body>" + + "<u:" + upnpMethod + " " + + "xmlns:u=\"" + device.ServiceType + "\">" + + methodParameters + + "</u:" + upnpMethod + ">" + + "</s:Body>" + + "</s:Envelope>\r\n\r\n" + }; - var req = new HttpRequestOptions(); - req.LogErrors = false; - - // The periodic request logging may keep some devices awake - req.LogRequestAsDebug = true; - - req.Url = ss; - req.EnableKeepAlive = false; - req.RequestContentType = "text/xml"; req.RequestHeaders.Add("SOAPACTION", "\"" + device.ServiceType + "#" + upnpMethod + "\""); - req.RequestContent = "<s:Envelope " - + "xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" " - + "s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" - + "<s:Body>" - + "<u:" + upnpMethod + " " - + "xmlns:u=\"" + device.ServiceType + "\">" - + methodParameters - + "</u:" + upnpMethod + ">" - + "</s:Body>" - + "</s:Envelope>\r\n\r\n"; return req; } public abstract HttpRequestOptions Encode(); - public virtual string Method - { - get { return "POST"; } - } + public virtual string Method => "POST"; protected void WriteFullElement(XmlWriter writer, string element, string value) { diff --git a/Mono.Nat/Upnp/UpnpNatDevice.cs b/Mono.Nat/Upnp/UpnpNatDevice.cs index fd408ee63..3ff1eeb90 100644 --- a/Mono.Nat/Upnp/UpnpNatDevice.cs +++ b/Mono.Nat/Upnp/UpnpNatDevice.cs @@ -27,11 +27,9 @@ // using System; -using System.IO; using System.Net; using System.Xml; using System.Text; -using System.Diagnostics; using System.Threading.Tasks; using MediaBrowser.Common.Net; using Microsoft.Extensions.Logging; @@ -96,7 +94,7 @@ namespace Mono.Nat.Upnp public async Task GetServicesList() { // Create a HTTPWebRequest to download the list of services the device offers - var message = new GetServicesMessage(this.serviceDescriptionUrl, this.hostEndPoint, _logger); + var message = new GetServicesMessage(this.serviceDescriptionUrl, this.hostEndPoint); using (var response = await _httpClient.SendAsync(message.Encode(), message.Method).ConfigureAwait(false)) { diff --git a/jellyfin.ruleset b/jellyfin.ruleset index e7e02a7d5..8ea1d6b16 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -38,5 +38,7 @@ <Rule Id="CA1054" Action="None" /> <!-- disable warning CA1303: Do not pass literals as localized parameters --> <Rule Id="CA1303" Action="None" /> + <!-- disable warning CA2000: Dispose objects before losing scope --> + <Rule Id="CA2000" Action="None" /> </Rules> </RuleSet> |
