diff options
90 files changed, 668 insertions, 335 deletions
diff --git a/.ci/azure-pipelines-abi.yml b/.ci/azure-pipelines-abi.yml index 31f861f63..cf74a4201 100644 --- a/.ci/azure-pipelines-abi.yml +++ b/.ci/azure-pipelines-abi.yml @@ -34,7 +34,6 @@ jobs: inputs: packageType: sdk version: ${{ parameters.DotNetSdkVersion }} - includePreviewVersions: true - task: DotNetCoreCLI@2 displayName: 'Install ABI CompatibilityChecker Tool' diff --git a/.ci/azure-pipelines-main.yml b/.ci/azure-pipelines-main.yml index 1086d51d2..b7112ba24 100644 --- a/.ci/azure-pipelines-main.yml +++ b/.ci/azure-pipelines-main.yml @@ -54,7 +54,6 @@ jobs: inputs: packageType: sdk version: ${{ parameters.DotNetSdkVersion }} - includePreviewVersions: true - task: DotNetCoreCLI@2 displayName: 'Publish Server' diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index 4abe52b43..e227d5fe6 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -199,7 +199,6 @@ jobs: inputs: packageType: 'sdk' version: '6.0.x' - includePreviewVersions: true - task: DotNetCoreCLI@2 displayName: 'Build Stable Nuget packages' diff --git a/.ci/azure-pipelines-test.yml b/.ci/azure-pipelines-test.yml index 80a5732ee..cc94dc2c5 100644 --- a/.ci/azure-pipelines-test.yml +++ b/.ci/azure-pipelines-test.yml @@ -41,7 +41,6 @@ jobs: inputs: packageType: sdk version: ${{ parameters.DotNetSdkVersion }} - includePreviewVersions: true - task: SonarCloudPrepare@1 displayName: 'Prepare analysis on SonarCloud' diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index e07d913b5..ea1d30cdf 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -25,8 +25,7 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: '6.0.x' - include-prerelease: true - + - name: Initialize CodeQL uses: github/codeql-action/init@v1 with: diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index ea9188f1b..3e9346840 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -20,7 +20,6 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: '6.0.x' - include-prerelease: true - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json @@ -45,7 +44,6 @@ jobs: uses: actions/setup-dotnet@v1 with: dotnet-version: '6.0.x' - include-prerelease: true - name: Generate openapi.json run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests" - name: Upload openapi.json diff --git a/Directory.Build.props b/Directory.Build.props index b899999ef..d243cde2b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,10 +3,13 @@ <PropertyGroup> <Nullable>enable</Nullable> - <TreatWarningsAsErrors>true</TreatWarningsAsErrors> <CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)/jellyfin.ruleset</CodeAnalysisRuleSet> </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)' == 'Release' "> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> <AnalysisMode>AllEnabledByDefault</AnalysisMode> </PropertyGroup> diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs index ac336e5dc..34e5b8a36 100644 --- a/Emby.Dlna/ContentDirectory/ControlHandler.cs +++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs @@ -291,9 +291,9 @@ namespace Emby.Dlna.ContentDirectory return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + "<Features xmlns=\"urn:schemas-upnp-org:av:avs\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"urn:schemas-upnp-org:av:avs http://www.upnp.org/schemas/av/avs.xsd\">" + "<Feature name=\"samsung.com_BASICVIEW\" version=\"1\">" - + "<container id=\"I\" type=\"object.item.imageItem\"/>" - + "<container id=\"A\" type=\"object.item.audioItem\"/>" - + "<container id=\"V\" type=\"object.item.videoItem\"/>" + + "<container id=\"0\" type=\"object.item.imageItem\"/>" + + "<container id=\"0\" type=\"object.item.audioItem\"/>" + + "<container id=\"0\" type=\"object.item.videoItem\"/>" + "</Feature>" + "</Features>"; } @@ -800,6 +800,11 @@ namespace Emby.Dlna.ContentDirectory } }; + if (limit.HasValue) + { + list = list.Take(limit.Value).ToList(); + } + return new QueryResult<ServerItem> { Items = list, @@ -884,6 +889,11 @@ namespace Emby.Dlna.ContentDirectory } }; + if (limit.HasValue) + { + array = array.Take(limit.Value).ToArray(); + } + return new QueryResult<ServerItem> { Items = array, @@ -1010,6 +1020,11 @@ namespace Emby.Dlna.ContentDirectory } }; + if (limit.HasValue) + { + list = list.Take(limit.Value).ToList(); + } + return new QueryResult<ServerItem> { Items = list, @@ -1037,7 +1052,7 @@ namespace Emby.Dlna.ContentDirectory }; query.IsResumable = true; - query.Limit = 10; + query.Limit = query.Limit ?? 10; var result = _libraryManager.GetItemsResult(query); @@ -1451,7 +1466,7 @@ namespace Emby.Dlna.ContentDirectory new LatestItemsQuery { UserId = user.Id, - Limit = 50, + Limit = query.Limit ?? 50, IncludeItemTypes = new[] { nameof(Episode) }, ParentId = parent == null ? Guid.Empty : parent.Id, GroupItems = false @@ -1476,7 +1491,7 @@ namespace Emby.Dlna.ContentDirectory new LatestItemsQuery { UserId = user.Id, - Limit = 50, + Limit = query.Limit ?? 50, IncludeItemTypes = new[] { nameof(Movie) }, ParentId = parent?.Id ?? Guid.Empty, GroupItems = true diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index 0a84f30c4..b00e1c98a 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -729,7 +729,7 @@ namespace Emby.Dlna.Didl { if (item.PremiereDate.HasValue) { - AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("o", CultureInfo.InvariantCulture), NsDc); + AddValue(writer, "dc", "date", item.PremiereDate.Value.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture), NsDc); } } diff --git a/Emby.Dlna/Emby.Dlna.csproj b/Emby.Dlna/Emby.Dlna.csproj index c8332e44e..7fdbd44f0 100644 --- a/Emby.Dlna/Emby.Dlna.csproj +++ b/Emby.Dlna/Emby.Dlna.csproj @@ -72,7 +72,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0-rc.2*" /> + <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> </ItemGroup> </Project> diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs index 581e4a286..780aad9c1 100644 --- a/Emby.Dlna/Service/BaseControlHandler.cs +++ b/Emby.Dlna/Service/BaseControlHandler.cs @@ -64,7 +64,7 @@ namespace Emby.Dlna.Service requestInfo = await ParseRequestAsync(reader).ConfigureAwait(false); } - Logger.LogDebug("Received control request {0}", requestInfo.LocalName); + Logger.LogDebug("Received control request {LocalName}, params: {@Headers}", requestInfo.LocalName, requestInfo.Headers); var settings = new XmlWriterSettings { diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs index ac73cfa42..3f75e4fc7 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/Emby.Drawing/ImageProcessor.cs @@ -26,7 +26,7 @@ namespace Emby.Drawing public sealed class ImageProcessor : IImageProcessor, IDisposable { // Increment this when there's a change requiring caches to be invalidated - private const string Version = "3"; + private const char Version = '3'; private static readonly HashSet<string> _transparentImageTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { ".png", ".webp", ".gif" }; diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs index 5ddcf37fe..7bc9fbce8 100644 --- a/Emby.Naming/Common/NamingOptions.cs +++ b/Emby.Naming/Common/NamingOptions.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA1819 + using System; using System.Linq; using System.Text.RegularExpressions; diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj index e9e9edda6..4c5dcdafc 100644 --- a/Emby.Naming/Emby.Naming.csproj +++ b/Emby.Naming/Emby.Naming.csproj @@ -38,7 +38,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0" PrivateAssets="All" /> </ItemGroup> <!-- Code Analyzers--> diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 4f2fdfd3c..73919f306 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -56,6 +56,7 @@ using MediaBrowser.Common.Updates; using MediaBrowser.Controller; using MediaBrowser.Controller.Channels; using MediaBrowser.Controller.Chapters; +using MediaBrowser.Controller.ClientEvent; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Dlna; @@ -665,7 +666,7 @@ namespace Emby.Server.Implementations serviceCollection.AddScoped<MediaInfoHelper>(); serviceCollection.AddScoped<AudioHelper>(); serviceCollection.AddScoped<DynamicHlsHelper>(); - + serviceCollection.AddScoped<IClientEventLogger, ClientEventLogger>(); serviceCollection.AddSingleton<IDirectoryService, DirectoryService>(); } diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 9287f5272..c6b32a52c 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -497,7 +497,7 @@ namespace Emby.Server.Implementations.Dto } catch (Exception ex) { - _logger.LogError(ex, "Error getting {imageType} image info for {path}", image.Type, image.Path); + _logger.LogError(ex, "Error getting {ImageType} image info for {Path}", image.Type, image.Path); return null; } } diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index c1ce4b557..042b8f71a 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -25,12 +25,12 @@ <ItemGroup> <PackageReference Include="DiscUtils.Udf" Version="0.16.13" /> <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0-rc.2*" /> - <PackageReference Include="Mono.Nat" Version="3.0.1" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0" /> + <PackageReference Include="Mono.Nat" Version="3.0.2" /> <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.2" /> <PackageReference Include="sharpcompress" Version="0.30.0" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" /> diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs index e9d069cd3..7ebc800b9 100644 --- a/Emby.Server.Implementations/IO/LibraryMonitor.cs +++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs @@ -276,7 +276,7 @@ namespace Emby.Server.Implementations.IO } catch (Exception ex) { - _logger.LogError(ex, "Error watching path: {path}", path); + _logger.LogError(ex, "Error watching path: {Path}", path); } }); } diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index eeee28842..777cd2cd4 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Globalization; @@ -23,6 +21,11 @@ namespace Emby.Server.Implementations.IO private readonly string _tempPath; private static readonly bool _isEnvironmentCaseInsensitive = OperatingSystem.IsWindows(); + /// <summary> + /// Initializes a new instance of the <see cref="ManagedFileSystem"/> class. + /// </summary> + /// <param name="logger">The <see cref="ILogger"/> instance to use.</param> + /// <param name="applicationPaths">The <see cref="IApplicationPaths"/> instance to use.</param> public ManagedFileSystem( ILogger<ManagedFileSystem> logger, IApplicationPaths applicationPaths) @@ -31,6 +34,7 @@ namespace Emby.Server.Implementations.IO _tempPath = applicationPaths.TempDirectory; } + /// <inheritdoc /> public virtual void AddShortcutHandler(IShortcutHandler handler) { _shortcutHandlers.Add(handler); @@ -72,6 +76,7 @@ namespace Emby.Server.Implementations.IO return handler?.Resolve(filename); } + /// <inheritdoc /> public virtual string MakeAbsolutePath(string folderPath, string filePath) { // path is actually a stream @@ -358,11 +363,13 @@ namespace Emby.Server.Implementations.IO return GetCreationTimeUtc(GetFileSystemInfo(path)); } + /// <inheritdoc /> public virtual DateTime GetCreationTimeUtc(FileSystemMetadata info) { return info.CreationTimeUtc; } + /// <inheritdoc /> public virtual DateTime GetLastWriteTimeUtc(FileSystemMetadata info) { return info.LastWriteTimeUtc; @@ -397,6 +404,7 @@ namespace Emby.Server.Implementations.IO return GetLastWriteTimeUtc(GetFileSystemInfo(path)); } + /// <inheritdoc /> public virtual void SetHidden(string path, bool isHidden) { if (!OperatingSystem.IsWindows()) @@ -421,6 +429,7 @@ namespace Emby.Server.Implementations.IO } } + /// <inheritdoc /> public virtual void SetAttributes(string path, bool isHidden, bool readOnly) { if (!OperatingSystem.IsWindows()) @@ -444,7 +453,7 @@ namespace Emby.Server.Implementations.IO if (readOnly) { - attributes = attributes | FileAttributes.ReadOnly; + attributes |= FileAttributes.ReadOnly; } else { @@ -453,7 +462,7 @@ namespace Emby.Server.Implementations.IO if (isHidden) { - attributes = attributes | FileAttributes.Hidden; + attributes |= FileAttributes.Hidden; } else { @@ -498,6 +507,7 @@ namespace Emby.Server.Implementations.IO File.Copy(temp1, file2, true); } + /// <inheritdoc /> public virtual bool ContainsSubPath(string parentPath, string path) { if (string.IsNullOrEmpty(parentPath)) @@ -515,6 +525,7 @@ namespace Emby.Server.Implementations.IO _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); } + /// <inheritdoc /> public virtual string NormalizePath(string path) { if (string.IsNullOrEmpty(path)) @@ -530,6 +541,7 @@ namespace Emby.Server.Implementations.IO return Path.TrimEndingDirectorySeparator(path); } + /// <inheritdoc /> public virtual bool AreEqual(string path1, string path2) { if (path1 == null && path2 == null) @@ -548,6 +560,7 @@ namespace Emby.Server.Implementations.IO _isEnvironmentCaseInsensitive ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); } + /// <inheritdoc /> public virtual string GetFileNameWithoutExtension(FileSystemMetadata info) { if (info.IsDirectory) @@ -558,11 +571,11 @@ namespace Emby.Server.Implementations.IO return Path.GetFileNameWithoutExtension(info.FullName); } + /// <inheritdoc /> public virtual bool IsPathFile(string path) { - // Cannot use Path.IsPathRooted because it returns false under mono when using windows-based paths, e.g. C:\\ - if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) != -1 && - !path.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) + if (path.Contains("://", StringComparison.OrdinalIgnoreCase) + && !path.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) { return false; } @@ -570,17 +583,23 @@ namespace Emby.Server.Implementations.IO return true; } + /// <inheritdoc /> public virtual void DeleteFile(string path) { SetAttributes(path, false, false); File.Delete(path); } + /// <inheritdoc /> public virtual List<FileSystemMetadata> GetDrives() { // check for ready state to avoid waiting for drives to timeout // some drives on linux have no actual size or are used for other purposes - return DriveInfo.GetDrives().Where(d => d.IsReady && d.TotalSize != 0 && d.DriveType != DriveType.Ram) + return DriveInfo.GetDrives() + .Where( + d => (d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network || d.DriveType == DriveType.Removable) + && d.IsReady + && d.TotalSize != 0) .Select(d => new FileSystemMetadata { Name = d.Name, @@ -589,16 +608,19 @@ namespace Emby.Server.Implementations.IO }).ToList(); } + /// <inheritdoc /> public virtual IEnumerable<FileSystemMetadata> GetDirectories(string path, bool recursive = false) { return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", GetEnumerationOptions(recursive))); } + /// <inheritdoc /> public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, bool recursive = false) { return GetFiles(path, null, false, recursive); } + /// <inheritdoc /> public virtual IEnumerable<FileSystemMetadata> GetFiles(string path, IReadOnlyList<string>? extensions, bool enableCaseSensitiveExtensions, bool recursive = false) { var enumerationOptions = GetEnumerationOptions(recursive); @@ -629,6 +651,7 @@ namespace Emby.Server.Implementations.IO return ToMetadata(files); } + /// <inheritdoc /> public virtual IEnumerable<FileSystemMetadata> GetFileSystemEntries(string path, bool recursive = false) { var directoryInfo = new DirectoryInfo(path); @@ -642,16 +665,19 @@ namespace Emby.Server.Implementations.IO return infos.Select(GetFileSystemMetadata); } + /// <inheritdoc /> public virtual IEnumerable<string> GetDirectoryPaths(string path, bool recursive = false) { return Directory.EnumerateDirectories(path, "*", GetEnumerationOptions(recursive)); } + /// <inheritdoc /> public virtual IEnumerable<string> GetFilePaths(string path, bool recursive = false) { return GetFilePaths(path, null, false, recursive); } + /// <inheritdoc /> public virtual IEnumerable<string> GetFilePaths(string path, string[]? extensions, bool enableCaseSensitiveExtensions, bool recursive = false) { var enumerationOptions = GetEnumerationOptions(recursive); @@ -682,6 +708,7 @@ namespace Emby.Server.Implementations.IO return files; } + /// <inheritdoc /> public virtual IEnumerable<string> GetFileSystemEntryPaths(string path, bool recursive = false) { return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive)); diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 1326f60fe..2dbb569c6 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -492,7 +492,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogError(ex, "Error in {resolver} resolving {path}", resolver.GetType().Name, args.Path); + _logger.LogError(ex, "Error in {Resolver} resolving {Path}", resolver.GetType().Name, args.Path); return null; } } @@ -799,7 +799,7 @@ namespace Emby.Server.Implementations.Library { var userRootPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath; - _logger.LogDebug("Creating userRootPath at {path}", userRootPath); + _logger.LogDebug("Creating userRootPath at {Path}", userRootPath); Directory.CreateDirectory(userRootPath); var newItemId = GetNewItemId(userRootPath, typeof(UserRootFolder)); @@ -810,7 +810,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogError(ex, "Error creating UserRootFolder {path}", newItemId); + _logger.LogError(ex, "Error creating UserRootFolder {Path}", newItemId); } if (tmpItem == null) @@ -827,7 +827,7 @@ namespace Emby.Server.Implementations.Library } _userRootFolder = tmpItem; - _logger.LogDebug("Setting userRootFolder: {folder}", _userRootFolder); + _logger.LogDebug("Setting userRootFolder: {Folder}", _userRootFolder); } } } @@ -1213,7 +1213,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogError(ex, "Error resolving shortcut file {file}", i); + _logger.LogError(ex, "Error resolving shortcut file {File}", i); return null; } }) @@ -1698,7 +1698,7 @@ namespace Emby.Server.Implementations.Library if (video == null) { - _logger.LogError("Intro resolver returned null for {path}.", info.Path); + _logger.LogError("Intro resolver returned null for {Path}.", info.Path); } else { @@ -1717,7 +1717,7 @@ namespace Emby.Server.Implementations.Library } catch (Exception ex) { - _logger.LogError(ex, "Error resolving path {path}.", info.Path); + _logger.LogError(ex, "Error resolving path {Path}.", info.Path); } } else diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 351fced34..972d4ebbb 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -45,6 +45,7 @@ namespace Emby.Server.Implementations.Library private readonly IMediaEncoder _mediaEncoder; private readonly ILocalizationManager _localizationManager; private readonly IApplicationPaths _appPaths; + private readonly IDirectoryService _directoryService; private readonly ConcurrentDictionary<string, ILiveStream> _openStreams = new ConcurrentDictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase); private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1); @@ -61,7 +62,8 @@ namespace Emby.Server.Implementations.Library ILogger<MediaSourceManager> logger, IFileSystem fileSystem, IUserDataManager userDataManager, - IMediaEncoder mediaEncoder) + IMediaEncoder mediaEncoder, + IDirectoryService directoryService) { _itemRepo = itemRepo; _userManager = userManager; @@ -72,6 +74,7 @@ namespace Emby.Server.Implementations.Library _mediaEncoder = mediaEncoder; _localizationManager = localizationManager; _appPaths = applicationPaths; + _directoryService = directoryService; } public void AddParts(IEnumerable<IMediaSourceProvider> providers) @@ -106,16 +109,6 @@ namespace Emby.Server.Implementations.Library return false; } - public List<MediaStream> GetMediaStreams(string mediaSourceId) - { - var list = GetMediaStreams(new MediaStreamQuery - { - ItemId = new Guid(mediaSourceId) - }); - - return GetMediaStreamsForItem(list); - } - public List<MediaStream> GetMediaStreams(Guid itemId) { var list = GetMediaStreams(new MediaStreamQuery @@ -161,7 +154,7 @@ namespace Emby.Server.Implementations.Library if (allowMediaProbe && mediaSources[0].Type != MediaSourceType.Placeholder && !mediaSources[0].MediaStreams.Any(i => i.Type == MediaStreamType.Audio || i.Type == MediaStreamType.Video)) { await item.RefreshMetadata( - new MetadataRefreshOptions(new DirectoryService(_fileSystem)) + new MetadataRefreshOptions(_directoryService) { EnableRemoteContentProbe = true, MetadataRefreshMode = MetadataRefreshMode.FullRefresh @@ -212,6 +205,7 @@ namespace Emby.Server.Implementations.Library return SortMediaSources(list); } + /// <inheritdoc />> public MediaProtocol GetPathProtocol(string path) { if (path.StartsWith("Rtsp", StringComparison.OrdinalIgnoreCase)) @@ -258,7 +252,7 @@ namespace Emby.Server.Implementations.Library { if (path != null) { - if (path.IndexOf(".m3u", StringComparison.OrdinalIgnoreCase) != -1) + if (path.Contains(".m3u", StringComparison.OrdinalIgnoreCase)) { return false; } @@ -297,7 +291,7 @@ namespace Emby.Server.Implementations.Library catch (Exception ex) { _logger.LogError(ex, "Error getting media sources"); - return new List<MediaSourceInfo>(); + return Enumerable.Empty<MediaSourceInfo>(); } } @@ -494,14 +488,11 @@ namespace Emby.Server.Implementations.Library _liveStreamSemaphore.Release(); } - // TODO: Don't hardcode this - const bool isAudio = false; - try { if (mediaSource.MediaStreams.Any(i => i.Index != -1) || !mediaSource.SupportsProbing) { - AddMediaInfo(mediaSource, isAudio); + AddMediaInfo(mediaSource); } else { @@ -509,14 +500,14 @@ namespace Emby.Server.Implementations.Library string cacheKey = request.OpenToken; await new LiveStreamHelper(_mediaEncoder, _logger, _appPaths) - .AddMediaInfoWithProbe(mediaSource, isAudio, cacheKey, true, cancellationToken) + .AddMediaInfoWithProbe(mediaSource, false, cacheKey, true, cancellationToken) .ConfigureAwait(false); } } catch (Exception ex) { _logger.LogError(ex, "Error probing live tv stream"); - AddMediaInfo(mediaSource, isAudio); + AddMediaInfo(mediaSource); } // TODO: @bond Fix @@ -536,7 +527,7 @@ namespace Emby.Server.Implementations.Library return new Tuple<LiveStreamResponse, IDirectStreamProvider>(new LiveStreamResponse(clone), liveStream as IDirectStreamProvider); } - private static void AddMediaInfo(MediaSourceInfo mediaSource, bool isAudio) + private static void AddMediaInfo(MediaSourceInfo mediaSource) { mediaSource.DefaultSubtitleStreamIndex = null; @@ -855,9 +846,7 @@ namespace Emby.Server.Implementations.Library return (provider, keyId); } - /// <summary> - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// </summary> + /// <inheritdoc /> public void Dispose() { Dispose(true); diff --git a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs index 8739a9e1b..8a9a4b865 100644 --- a/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs +++ b/Emby.Server.Implementations/Library/Validators/PeopleValidator.cs @@ -78,7 +78,7 @@ namespace Emby.Server.Implementations.Library.Validators } catch (Exception ex) { - _logger.LogError(ex, "Error validating IBN entry {person}", person); + _logger.LogError(ex, "Error validating IBN entry {Person}", person); } // Update progress diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index 8045e814c..367f3cb9e 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -1308,16 +1308,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV await recorder.Record(directStreamProvider, mediaStreamInfo, recordPath, duration, onStarted, activeRecordingInfo.CancellationTokenSource.Token).ConfigureAwait(false); recordingStatus = RecordingStatus.Completed; - _logger.LogInformation("Recording completed: {recordPath}", recordPath); + _logger.LogInformation("Recording completed: {RecordPath}", recordPath); } catch (OperationCanceledException) { - _logger.LogInformation("Recording stopped: {recordPath}", recordPath); + _logger.LogInformation("Recording stopped: {RecordPath}", recordPath); recordingStatus = RecordingStatus.Completed; } catch (Exception ex) { - _logger.LogError(ex, "Error recording to {recordPath}", recordPath); + _logger.LogError(ex, "Error recording to {RecordPath}", recordPath); recordingStatus = RecordingStatus.Error; } @@ -1404,7 +1404,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } catch (Exception ex) { - _logger.LogError(ex, "Error deleting 0-byte failed recording file {path}", path); + _logger.LogError(ex, "Error deleting 0-byte failed recording file {Path}", path); } } } diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 835028b92..8688688e9 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -225,13 +225,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { try { - _logger.LogInformation("Stopping ffmpeg recording process for {path}", _targetPath); + _logger.LogInformation("Stopping ffmpeg recording process for {Path}", _targetPath); _process.StandardInput.WriteLine("q"); } catch (Exception ex) { - _logger.LogError(ex, "Error stopping recording transcoding job for {path}", _targetPath); + _logger.LogError(ex, "Error stopping recording transcoding job for {Path}", _targetPath); } if (_hasExited) @@ -241,7 +241,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV try { - _logger.LogInformation("Calling recording process.WaitForExit for {path}", _targetPath); + _logger.LogInformation("Calling recording process.WaitForExit for {Path}", _targetPath); if (_process.WaitForExit(10000)) { @@ -250,7 +250,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } catch (Exception ex) { - _logger.LogError(ex, "Error waiting for recording process to exit for {path}", _targetPath); + _logger.LogError(ex, "Error waiting for recording process to exit for {Path}", _targetPath); } if (_hasExited) @@ -260,13 +260,13 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV try { - _logger.LogInformation("Killing ffmpeg recording process for {path}", _targetPath); + _logger.LogInformation("Killing ffmpeg recording process for {Path}", _targetPath); _process.Kill(); } catch (Exception ex) { - _logger.LogError(ex, "Error killing recording transcoding job for {path}", _targetPath); + _logger.LogError(ex, "Error killing recording transcoding job for {Path}", _targetPath); } } } diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs index 21e1409ac..598e3f88a 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -393,7 +393,7 @@ namespace Emby.Server.Implementations.LiveTv } catch (Exception ex) { - _logger.LogError(ex, "Error getting image info for {name}", info.Name); + _logger.LogError(ex, "Error getting image info for {Name}", info.Name); } return null; diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index ea1a28fe8..a41b63f28 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -1054,7 +1054,7 @@ namespace Emby.Server.Implementations.LiveTv { cancellationToken.ThrowIfCancellationRequested(); - _logger.LogDebug("Refreshing guide from {name}", service.Name); + _logger.LogDebug("Refreshing guide from {Name}", service.Name); try { @@ -1135,7 +1135,7 @@ namespace Emby.Server.Implementations.LiveTv } catch (Exception ex) { - _logger.LogError(ex, "Error getting channel information for {name}", channelInfo.Item2.Name); + _logger.LogError(ex, "Error getting channel information for {Name}", channelInfo.Item2.Name); } numComplete++; @@ -1248,7 +1248,7 @@ namespace Emby.Server.Implementations.LiveTv } catch (Exception ex) { - _logger.LogError(ex, "Error getting programs for channel {name}", currentChannel.Name); + _logger.LogError(ex, "Error getting programs for channel {Name}", currentChannel.Name); } numComplete++; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index 96c1bc3f3..9fba17ca3 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -82,7 +82,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath)); - Logger.LogInformation("Opening HDHR UDP Live stream from {host}", uri.Host); + Logger.LogInformation("Opening HDHR UDP Live stream from {Host}", uri.Host); var remoteAddress = IPAddress.Parse(uri.Host); IPAddress localAddress = null; diff --git a/Emby.Server.Implementations/Localization/Core/et.json b/Emby.Server.Implementations/Localization/Core/et.json index c3596ecf1..e5405e515 100644 --- a/Emby.Server.Implementations/Localization/Core/et.json +++ b/Emby.Server.Implementations/Localization/Core/et.json @@ -17,7 +17,7 @@ "TaskCleanLogsDescription": "Kustutab logifailid, mis on vanemad kui {0} päeva.", "TaskCleanLogs": "Puhasta logikataloog", "TaskRefreshLibraryDescription": "Otsib meedikogust uusi faile ja värskendab metaandmeid.", - "Collections": "Kollektsioonid", + "Collections": "Kogumikud", "TaskRefreshLibrary": "Skaneeri meediakogu", "TaskRefreshChapterImagesDescription": "Loob peatükkidega videote jaoks pisipildid.", "TaskRefreshChapterImages": "Eralda peatükipildid", diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index 36f4e3e7c..2d7163275 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -31,7 +31,7 @@ "ItemRemovedWithName": "{0} - изъято из медиатеки", "LabelIpAddressValue": "IP-адрес: {0}", "LabelRunningTimeValue": "Длительность: {0}", - "Latest": "Последнее", + "Latest": "Крайнее", "MessageApplicationUpdated": "Jellyfin Server был обновлён", "MessageApplicationUpdatedTo": "Jellyfin Server был обновлён до {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Конфигурация сервера (раздел {0}) была обновлена", diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json index 33aa0eea0..548e395a9 100644 --- a/Emby.Server.Implementations/Localization/Core/vi.json +++ b/Emby.Server.Implementations/Localization/Core/vi.json @@ -62,11 +62,11 @@ "PluginUninstalledWithName": "{0} đã được gỡ bỏ", "PluginInstalledWithName": "{0} đã được cài đặt", "Plugin": "Plugin", - "NotificationOptionVideoPlaybackStopped": "Phát lại video đã dừng", + "NotificationOptionVideoPlaybackStopped": "Đã dừng phát lại video", "NotificationOptionVideoPlayback": "Đã bắt đầu phát lại video", "NotificationOptionUserLockedOut": "Người dùng bị khóa", "NotificationOptionTaskFailed": "Lỗi tác vụ đã lên lịch", - "NotificationOptionServerRestartRequired": "Yêu cầu khởi động lại Server", + "NotificationOptionServerRestartRequired": "Yêu cầu khởi động lại máy chủ", "NotificationOptionPluginUpdateInstalled": "Cập nhật Plugin đã được cài đặt", "NotificationOptionPluginUninstalled": "Đã gỡ bỏ Plugin", "NotificationOptionPluginInstalled": "Đã cài đặt Plugin", @@ -75,7 +75,7 @@ "NotificationOptionInstallationFailed": "Cài đặt thất bại", "NotificationOptionCameraImageUploaded": "Đã tải lên hình ảnh máy ảnh", "NotificationOptionAudioPlaybackStopped": "Phát lại âm thanh đã dừng", - "NotificationOptionAudioPlayback": "Phát lại âm thanh đã bắt đầu", + "NotificationOptionAudioPlayback": "Đã bắt đầu phát lại âm thanh", "NotificationOptionApplicationUpdateInstalled": "Bản cập nhật ứng dụng đã được cài đặt", "NotificationOptionApplicationUpdateAvailable": "Bản cập nhật ứng dụng hiện sẵn có", "NewVersionIsAvailable": "Một phiên bản mới của Jellyfin Server sẵn có để tải.", diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs index a575b260c..0941902fc 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs @@ -161,11 +161,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks } catch (UnauthorizedAccessException ex) { - _logger.LogError(ex, "Error deleting directory {path}", directory); + _logger.LogError(ex, "Error deleting directory {Path}", directory); } catch (IOException ex) { - _logger.LogError(ex, "Error deleting directory {path}", directory); + _logger.LogError(ex, "Error deleting directory {Path}", directory); } } } @@ -179,11 +179,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks } catch (UnauthorizedAccessException ex) { - _logger.LogError(ex, "Error deleting file {path}", path); + _logger.LogError(ex, "Error deleting file {Path}", path); } catch (IOException ex) { - _logger.LogError(ex, "Error deleting file {path}", path); + _logger.LogError(ex, "Error deleting file {Path}", path); } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs index b13fc7fc6..099d781cd 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs @@ -141,11 +141,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks } catch (UnauthorizedAccessException ex) { - _logger.LogError(ex, "Error deleting directory {path}", directory); + _logger.LogError(ex, "Error deleting directory {Path}", directory); } catch (IOException ex) { - _logger.LogError(ex, "Error deleting directory {path}", directory); + _logger.LogError(ex, "Error deleting directory {Path}", directory); } } } @@ -159,11 +159,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks } catch (UnauthorizedAccessException ex) { - _logger.LogError(ex, "Error deleting file {path}", path); + _logger.LogError(ex, "Error deleting file {Path}", path); } catch (IOException ex) { - _logger.LogError(ex, "Error deleting file {path}", path); + _logger.LogError(ex, "Error deleting file {Path}", path); } } } diff --git a/Jellyfin.Api/Controllers/ClientLogController.cs b/Jellyfin.Api/Controllers/ClientLogController.cs new file mode 100644 index 000000000..95d07c930 --- /dev/null +++ b/Jellyfin.Api/Controllers/ClientLogController.cs @@ -0,0 +1,149 @@ +using System; +using System.Net.Mime; +using System.Threading.Tasks; +using Jellyfin.Api.Attributes; +using Jellyfin.Api.Constants; +using Jellyfin.Api.Helpers; +using Jellyfin.Api.Models.ClientLogDtos; +using MediaBrowser.Controller.ClientEvent; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.ClientLog; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Jellyfin.Api.Controllers +{ + /// <summary> + /// Client log controller. + /// </summary> + [Authorize(Policy = Policies.DefaultAuthorization)] + public class ClientLogController : BaseJellyfinApiController + { + private const int MaxDocumentSize = 1_000_000; + private readonly IClientEventLogger _clientEventLogger; + private readonly IServerConfigurationManager _serverConfigurationManager; + + /// <summary> + /// Initializes a new instance of the <see cref="ClientLogController"/> class. + /// </summary> + /// <param name="clientEventLogger">Instance of the <see cref="IClientEventLogger"/> interface.</param> + /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + public ClientLogController( + IClientEventLogger clientEventLogger, + IServerConfigurationManager serverConfigurationManager) + { + _clientEventLogger = clientEventLogger; + _serverConfigurationManager = serverConfigurationManager; + } + + /// <summary> + /// Post event from client. + /// </summary> + /// <param name="clientLogEventDto">The client log dto.</param> + /// <response code="204">Event logged.</response> + /// <response code="403">Event logging disabled.</response> + /// <returns>Submission status.</returns> + [HttpPost] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public ActionResult LogEvent([FromBody] ClientLogEventDto clientLogEventDto) + { + if (!_serverConfigurationManager.Configuration.AllowClientLogUpload) + { + return Forbid(); + } + + var (clientName, clientVersion, userId, deviceId) = GetRequestInformation(); + Log(clientLogEventDto, userId, clientName, clientVersion, deviceId); + return NoContent(); + } + + /// <summary> + /// Bulk post events from client. + /// </summary> + /// <param name="clientLogEventDtos">The list of client log dtos.</param> + /// <response code="204">All events logged.</response> + /// <response code="403">Event logging disabled.</response> + /// <returns>Submission status.</returns> + [HttpPost("Bulk")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + public ActionResult LogEvents([FromBody] ClientLogEventDto[] clientLogEventDtos) + { + if (!_serverConfigurationManager.Configuration.AllowClientLogUpload) + { + return Forbid(); + } + + var (clientName, clientVersion, userId, deviceId) = GetRequestInformation(); + foreach (var dto in clientLogEventDtos) + { + Log(dto, userId, clientName, clientVersion, deviceId); + } + + return NoContent(); + } + + /// <summary> + /// Upload a document. + /// </summary> + /// <response code="200">Document saved.</response> + /// <response code="403">Event logging disabled.</response> + /// <response code="413">Upload size too large.</response> + /// <returns>Create response.</returns> + [HttpPost("Document")] + [ProducesResponseType(typeof(ClientLogDocumentResponseDto), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status403Forbidden)] + [ProducesResponseType(StatusCodes.Status413PayloadTooLarge)] + [AcceptsFile(MediaTypeNames.Text.Plain)] + [RequestSizeLimit(MaxDocumentSize)] + public async Task<ActionResult<ClientLogDocumentResponseDto>> LogFile() + { + if (!_serverConfigurationManager.Configuration.AllowClientLogUpload) + { + return Forbid(); + } + + if (Request.ContentLength > MaxDocumentSize) + { + // Manually validate to return proper status code. + return StatusCode(StatusCodes.Status413PayloadTooLarge, $"Payload must be less than {MaxDocumentSize:N0} bytes"); + } + + var (clientName, clientVersion, _, _) = GetRequestInformation(); + var fileName = await _clientEventLogger.WriteDocumentAsync(clientName, clientVersion, Request.Body) + .ConfigureAwait(false); + return Ok(new ClientLogDocumentResponseDto(fileName)); + } + + private void Log( + ClientLogEventDto dto, + Guid userId, + string clientName, + string clientVersion, + string deviceId) + { + _clientEventLogger.Log(new ClientLogEvent( + dto.Timestamp, + dto.Level, + userId, + clientName, + clientVersion, + deviceId, + dto.Message)); + } + + private (string ClientName, string ClientVersion, Guid UserId, string DeviceId) GetRequestInformation() + { + var clientName = ClaimHelpers.GetClient(HttpContext.User) ?? "unknown-client"; + var clientVersion = ClaimHelpers.GetIsApiKey(HttpContext.User) + ? "apikey" + : ClaimHelpers.GetVersion(HttpContext.User) ?? "unknown-version"; + var userId = ClaimHelpers.GetUserId(HttpContext.User) ?? Guid.Empty; + var deviceId = ClaimHelpers.GetDeviceId(HttpContext.User) ?? "unknown-device-id"; + + return (clientName, clientVersion, userId, deviceId); + } + } +} diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs index 42e82dd5b..049fd503b 100644 --- a/Jellyfin.Api/Controllers/DynamicHlsController.cs +++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs @@ -1794,7 +1794,7 @@ namespace Jellyfin.Api.Controllers return; } - _logger.LogDebug("Deleting partial HLS file {path}", path); + _logger.LogDebug("Deleting partial HLS file {Path}", path); try { @@ -1802,15 +1802,15 @@ namespace Jellyfin.Api.Controllers } catch (IOException ex) { - _logger.LogError(ex, "Error deleting partial stream file(s) {path}", path); + _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path); var task = Task.Delay(100); - Task.WaitAll(task); + task.Wait(); DeleteFile(path, retryCount + 1); } catch (Exception ex) { - _logger.LogError(ex, "Error deleting partial stream file(s) {path}", path); + _logger.LogError(ex, "Error deleting partial stream file(s) {Path}", path); } } diff --git a/Jellyfin.Api/Controllers/HlsSegmentController.cs b/Jellyfin.Api/Controllers/HlsSegmentController.cs index 71caa0fe0..7325dca0a 100644 --- a/Jellyfin.Api/Controllers/HlsSegmentController.cs +++ b/Jellyfin.Api/Controllers/HlsSegmentController.cs @@ -64,7 +64,7 @@ namespace Jellyfin.Api.Controllers var transcodePath = _serverConfigurationManager.GetTranscodePath(); file = Path.GetFullPath(Path.Combine(transcodePath, file)); var fileDir = Path.GetDirectoryName(file); - if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath)) + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture)) { return BadRequest("Invalid segment."); } @@ -90,7 +90,7 @@ namespace Jellyfin.Api.Controllers var transcodePath = _serverConfigurationManager.GetTranscodePath(); file = Path.GetFullPath(Path.Combine(transcodePath, file)); var fileDir = Path.GetDirectoryName(file); - if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath) || Path.GetExtension(file) != ".m3u8") + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodePath, StringComparison.InvariantCulture) || Path.GetExtension(file) != ".m3u8") { return BadRequest("Invalid segment."); } @@ -144,7 +144,7 @@ namespace Jellyfin.Api.Controllers file = Path.GetFullPath(Path.Combine(transcodeFolderPath, file)); var fileDir = Path.GetDirectoryName(file); - if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath)) + if (string.IsNullOrEmpty(fileDir) || !fileDir.StartsWith(transcodeFolderPath, StringComparison.InvariantCulture)) { return BadRequest("Invalid segment."); } diff --git a/Jellyfin.Api/Controllers/ImageByNameController.cs b/Jellyfin.Api/Controllers/ImageByNameController.cs index 99ab7f232..89bbf22c9 100644 --- a/Jellyfin.Api/Controllers/ImageByNameController.cs +++ b/Jellyfin.Api/Controllers/ImageByNameController.cs @@ -82,7 +82,7 @@ namespace Jellyfin.Api.Controllers return NotFound(); } - if (!path.StartsWith(_applicationPaths.GeneralPath)) + if (!path.StartsWith(_applicationPaths.GeneralPath, StringComparison.InvariantCulture)) { return BadRequest("Invalid image path."); } @@ -177,7 +177,7 @@ namespace Jellyfin.Api.Controllers if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) { - if (!path.StartsWith(basePath)) + if (!path.StartsWith(basePath, StringComparison.InvariantCulture)) { return BadRequest("Invalid image path."); } @@ -196,7 +196,7 @@ namespace Jellyfin.Api.Controllers if (!string.IsNullOrEmpty(path) && System.IO.File.Exists(path)) { - if (!path.StartsWith(basePath)) + if (!path.StartsWith(basePath, StringComparison.InvariantCulture)) { return BadRequest("Invalid image path."); } diff --git a/Jellyfin.Api/Controllers/SubtitleController.cs b/Jellyfin.Api/Controllers/SubtitleController.cs index db8307f28..16acedcf3 100644 --- a/Jellyfin.Api/Controllers/SubtitleController.cs +++ b/Jellyfin.Api/Controllers/SubtitleController.cs @@ -528,7 +528,7 @@ namespace Jellyfin.Api.Controllers if (fontFile != null && fileSize != null && fileSize > 0) { - _logger.LogDebug("Fallback font size is {fileSize} Bytes", fileSize); + _logger.LogDebug("Fallback font size is {FileSize} Bytes", fileSize); return PhysicalFile(fontFile.FullName, MimeTypes.GetMimeType(fontFile.FullName)); } else diff --git a/Jellyfin.Api/Helpers/ClassMigrationHelper.cs b/Jellyfin.Api/Helpers/ClassMigrationHelper.cs index a911a3324..76fb27bcc 100644 --- a/Jellyfin.Api/Helpers/ClassMigrationHelper.cs +++ b/Jellyfin.Api/Helpers/ClassMigrationHelper.cs @@ -19,7 +19,7 @@ namespace Jellyfin.Api.Helpers // If any this null throw an exception. if (source == null || destination == null) { - throw new Exception("Source or/and Destination Objects are null"); + throw new ArgumentException("Source or/and Destination Objects are null"); } // Getting the Types of the objects. diff --git a/Jellyfin.Api/Helpers/StreamingHelpers.cs b/Jellyfin.Api/Helpers/StreamingHelpers.cs index 4fc791665..1b8f24c27 100644 --- a/Jellyfin.Api/Helpers/StreamingHelpers.cs +++ b/Jellyfin.Api/Helpers/StreamingHelpers.cs @@ -148,7 +148,7 @@ namespace Jellyfin.Api.Helpers mediaSource = string.IsNullOrEmpty(streamingRequest.MediaSourceId) ? mediaSources[0] - : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.InvariantCulture)); + : mediaSources.Find(i => string.Equals(i.Id, streamingRequest.MediaSourceId, StringComparison.Ordinal)); if (mediaSource == null && Guid.Parse(streamingRequest.MediaSourceId) == streamingRequest.Id) { diff --git a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs index 07d0b5543..f435bbf00 100644 --- a/Jellyfin.Api/Helpers/TranscodingJobHelper.cs +++ b/Jellyfin.Api/Helpers/TranscodingJobHelper.cs @@ -283,6 +283,7 @@ namespace Jellyfin.Api.Helpers lock (job.ProcessLock!) { + #pragma warning disable CA1849 // Can't await in lock block job.TranscodingThrottler?.Stop().GetAwaiter().GetResult(); var process = job.Process; @@ -308,6 +309,7 @@ namespace Jellyfin.Api.Helpers { } } + #pragma warning restore CA1849 } if (delete(job.Path!)) diff --git a/Jellyfin.Api/Jellyfin.Api.csproj b/Jellyfin.Api/Jellyfin.Api.csproj index 57480b2f3..a3598edfa 100644 --- a/Jellyfin.Api/Jellyfin.Api.csproj +++ b/Jellyfin.Api/Jellyfin.Api.csproj @@ -13,8 +13,8 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0-rc.2*" /> + <PackageReference Include="Microsoft.AspNetCore.Authorization" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" /> <PackageReference Include="Swashbuckle.AspNetCore.ReDoc" Version="6.2.3" /> </ItemGroup> diff --git a/Jellyfin.Api/Models/ClientLogDtos/ClientLogDocumentResponseDto.cs b/Jellyfin.Api/Models/ClientLogDtos/ClientLogDocumentResponseDto.cs new file mode 100644 index 000000000..44509a9c0 --- /dev/null +++ b/Jellyfin.Api/Models/ClientLogDtos/ClientLogDocumentResponseDto.cs @@ -0,0 +1,22 @@ +namespace Jellyfin.Api.Models.ClientLogDtos +{ + /// <summary> + /// Client log document response dto. + /// </summary> + public class ClientLogDocumentResponseDto + { + /// <summary> + /// Initializes a new instance of the <see cref="ClientLogDocumentResponseDto"/> class. + /// </summary> + /// <param name="fileName">The file name.</param> + public ClientLogDocumentResponseDto(string fileName) + { + FileName = fileName; + } + + /// <summary> + /// Gets the resulting filename. + /// </summary> + public string FileName { get; } + } +} diff --git a/Jellyfin.Api/Models/ClientLogDtos/ClientLogEventDto.cs b/Jellyfin.Api/Models/ClientLogDtos/ClientLogEventDto.cs new file mode 100644 index 000000000..9bf9be0a4 --- /dev/null +++ b/Jellyfin.Api/Models/ClientLogDtos/ClientLogEventDto.cs @@ -0,0 +1,30 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Logging; + +namespace Jellyfin.Api.Models.ClientLogDtos +{ + /// <summary> + /// The client log dto. + /// </summary> + public class ClientLogEventDto + { + /// <summary> + /// Gets or sets the event timestamp. + /// </summary> + [Required] + public DateTime Timestamp { get; set; } + + /// <summary> + /// Gets or sets the log level. + /// </summary> + [Required] + public LogLevel Level { get; set; } + + /// <summary> + /// Gets or sets the log message. + /// </summary> + [Required] + public string Message { get; set; } = string.Empty; + } +} diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj index 2de53e7c8..58dd945c6 100644 --- a/Jellyfin.Data/Jellyfin.Data.csproj +++ b/Jellyfin.Data/Jellyfin.Data.csproj @@ -24,7 +24,7 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0" PrivateAssets="All" /> </ItemGroup> <!-- Code analysers--> @@ -35,7 +35,7 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0-rc.2*" /> + <PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" /> </ItemGroup> <ItemGroup> diff --git a/Jellyfin.Server.Implementations/Events/EventManager.cs b/Jellyfin.Server.Implementations/Events/EventManager.cs index 8c5d8f2ce..7f7c4750d 100644 --- a/Jellyfin.Server.Implementations/Events/EventManager.cs +++ b/Jellyfin.Server.Implementations/Events/EventManager.cs @@ -57,7 +57,7 @@ namespace Jellyfin.Server.Implementations.Events } catch (Exception e) { - _logger.LogError(e, "Uncaught exception in EventConsumer {type}: ", service.GetType()); + _logger.LogError(e, "Uncaught exception in EventConsumer {Type}: ", service.GetType()); } } } diff --git a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj index e26cf093b..73ee69424 100644 --- a/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj +++ b/Jellyfin.Server.Implementations/Jellyfin.Server.Implementations.csproj @@ -19,13 +19,13 @@ <ItemGroup> <PackageReference Include="System.Linq.Async" Version="5.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0-rc.2*"> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="6.0.0" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.0" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.0-rc.2*"> + <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="6.0.0"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> diff --git a/Jellyfin.Server/Jellyfin.Server.csproj b/Jellyfin.Server/Jellyfin.Server.csproj index 49d979e11..045ed6a2b 100644 --- a/Jellyfin.Server/Jellyfin.Server.csproj +++ b/Jellyfin.Server/Jellyfin.Server.csproj @@ -31,10 +31,10 @@ <ItemGroup> <PackageReference Include="CommandLineParser" Version="2.8.0" /> - <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.0-rc.2*" /> + <PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="6.0.0" /> <PackageReference Include="prometheus-net" Version="5.0.1" /> <PackageReference Include="prometheus-net.AspNetCore" Version="5.0.1" /> <PackageReference Include="Serilog.AspNetCore" Version="4.1.0" /> @@ -44,6 +44,7 @@ <PackageReference Include="Serilog.Sinks.Console" Version="4.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="5.0.0" /> <PackageReference Include="Serilog.Sinks.Graylog" Version="2.2.2" /> + <PackageReference Include="Serilog.Sinks.Map" Version="1.0.2" /> <PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.0.7" /> </ItemGroup> diff --git a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs b/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs index 74874da1b..da9b69136 100644 --- a/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs +++ b/Jellyfin.Server/Middleware/ResponseTimeMiddleware.cs @@ -68,7 +68,7 @@ namespace Jellyfin.Server.Middleware if (_enableWarning && watch.ElapsedMilliseconds > _warningThreshold) { _logger.LogWarning( - "Slow HTTP Response from {url} to {remoteIp} in {elapsed:g} with Status Code {statusCode}", + "Slow HTTP Response from {Url} to {RemoteIp} in {Elapsed:g} with Status Code {StatusCode}", context.Request.GetDisplayUrl(), context.GetNormalizedRemoteIp(), watch.Elapsed, diff --git a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs index e4d2937e7..2f1d79157 100644 --- a/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs +++ b/Jellyfin.Server/Middleware/UrlDecodeQueryFeature.cs @@ -51,7 +51,7 @@ namespace Jellyfin.Server.Middleware return; } - if (!key.Contains('=')) + if (!key.Contains('=', StringComparison.Ordinal)) { _store = value; return; diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 5f848be9e..6e4c2280b 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -13,6 +13,7 @@ using Emby.Server.Implementations; using Jellyfin.Server.Implementations; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; +using MediaBrowser.Controller.ClientEvent; using MediaBrowser.Controller.Extensions; using MediaBrowser.Model.IO; using Microsoft.AspNetCore.Hosting; @@ -25,6 +26,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Serilog; using Serilog.Extensions.Logging; +using Serilog.Filters; using SQLitePCL; using ConfigurationExtensions = MediaBrowser.Controller.Extensions.ConfigurationExtensions; using ILogger = Microsoft.Extensions.Logging.ILogger; @@ -596,22 +598,46 @@ namespace Jellyfin.Server { // Serilog.Log is used by SerilogLoggerFactory when no logger is specified Log.Logger = new LoggerConfiguration() - .ReadFrom.Configuration(configuration) - .Enrich.FromLogContext() - .Enrich.WithThreadId() + .WriteTo.Logger(lc => + lc.ReadFrom.Configuration(configuration) + .Enrich.FromLogContext() + .Enrich.WithThreadId() + .Filter.ByExcluding(Matching.FromSource<ClientEventLogger>())) + .WriteTo.Logger(lc => + lc.WriteTo.Map( + "ClientName", + (clientName, wt) + => wt.File( + Path.Combine(appPaths.LogDirectoryPath, "log_" + clientName + "_.log"), + rollingInterval: RollingInterval.Day, + outputTemplate: "{Message:l}{NewLine}{Exception}", + encoding: Encoding.UTF8)) + .Filter.ByIncludingOnly(Matching.FromSource<ClientEventLogger>())) .CreateLogger(); } catch (Exception ex) { Log.Logger = new LoggerConfiguration() - .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}") - .WriteTo.Async(x => x.File( - Path.Combine(appPaths.LogDirectoryPath, "log_.log"), - rollingInterval: RollingInterval.Day, - outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message}{NewLine}{Exception}", - encoding: Encoding.UTF8)) - .Enrich.FromLogContext() - .Enrich.WithThreadId() + .WriteTo.Logger(lc => + lc.WriteTo.Async(x => x.File( + Path.Combine(appPaths.LogDirectoryPath, "log_.log"), + rollingInterval: RollingInterval.Day, + outputTemplate: "{Message:l}{NewLine}{Exception}", + encoding: Encoding.UTF8)) + .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss}] [{Level:u3}] [{ThreadId}] {SourceContext}: {Message:lj}{NewLine}{Exception}") + .Enrich.FromLogContext() + .Enrich.WithThreadId()) + .WriteTo.Logger(lc => + lc + .WriteTo.Map( + "ClientName", + (clientName, wt) + => wt.File( + Path.Combine(appPaths.LogDirectoryPath, "log_" + clientName + "_.log"), + rollingInterval: RollingInterval.Day, + outputTemplate: "{Message:l}{NewLine}{Exception}", + encoding: Encoding.UTF8)) + .Filter.ByIncludingOnly(Matching.FromSource<ClientEventLogger>())) .CreateLogger(); Log.Logger.Fatal(ex, "Failed to create/read logger configuration"); @@ -649,7 +675,7 @@ namespace Jellyfin.Server private static string NormalizeCommandLineArgument(string arg) { - if (!arg.Contains(" ", StringComparison.OrdinalIgnoreCase)) + if (!arg.Contains(' ', StringComparison.Ordinal)) { return arg; } diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj index 9c8ce4ac5..441f06f69 100644 --- a/MediaBrowser.Common/MediaBrowser.Common.csproj +++ b/MediaBrowser.Common/MediaBrowser.Common.csproj @@ -19,9 +19,9 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="6.0.0" /> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0" PrivateAssets="All" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs b/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs new file mode 100644 index 000000000..82b5b4593 --- /dev/null +++ b/MediaBrowser.Controller/ClientEvent/ClientEventLogger.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using System.Threading.Tasks; +using MediaBrowser.Model.ClientLog; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Controller.ClientEvent +{ + /// <inheritdoc /> + public class ClientEventLogger : IClientEventLogger + { + private const string LogString = "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}] [{Level}] [{ClientName}:{ClientVersion}]: UserId: {UserId} DeviceId: {DeviceId}{NewLine}{Message}"; + private readonly ILogger<ClientEventLogger> _logger; + private readonly IServerApplicationPaths _applicationPaths; + + /// <summary> + /// Initializes a new instance of the <see cref="ClientEventLogger"/> class. + /// </summary> + /// <param name="logger">Instance of the <see cref="ILogger{ClientEventLogger}"/> interface.</param> + /// <param name="applicationPaths">Instance of the <see cref="IServerApplicationPaths"/> interface.</param> + public ClientEventLogger( + ILogger<ClientEventLogger> logger, + IServerApplicationPaths applicationPaths) + { + _logger = logger; + _applicationPaths = applicationPaths; + } + + /// <inheritdoc /> + public void Log(ClientLogEvent clientLogEvent) + { + _logger.Log( + LogLevel.Critical, + LogString, + clientLogEvent.Timestamp, + clientLogEvent.Level.ToString(), + clientLogEvent.ClientName, + clientLogEvent.ClientVersion, + clientLogEvent.UserId ?? Guid.Empty, + clientLogEvent.DeviceId, + Environment.NewLine, + clientLogEvent.Message); + } + + /// <inheritdoc /> + public async Task<string> WriteDocumentAsync(string clientName, string clientVersion, Stream fileContents) + { + var fileName = $"upload_{clientName}_{clientVersion}_{DateTime.UtcNow:yyyyMMddHHmmss}_{Guid.NewGuid():N}.log"; + var logFilePath = Path.Combine(_applicationPaths.LogDirectoryPath, fileName); + await using var fileStream = new FileStream(logFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None); + await fileContents.CopyToAsync(fileStream).ConfigureAwait(false); + return fileName; + } + } +} diff --git a/MediaBrowser.Controller/ClientEvent/IClientEventLogger.cs b/MediaBrowser.Controller/ClientEvent/IClientEventLogger.cs new file mode 100644 index 000000000..34968d493 --- /dev/null +++ b/MediaBrowser.Controller/ClientEvent/IClientEventLogger.cs @@ -0,0 +1,31 @@ +using System.IO; +using System.Threading.Tasks; +using MediaBrowser.Controller.Net; +using MediaBrowser.Model.ClientLog; + +namespace MediaBrowser.Controller.ClientEvent +{ + /// <summary> + /// The client event logger. + /// </summary> + public interface IClientEventLogger + { + /// <summary> + /// Logs the event from the client. + /// </summary> + /// <param name="clientLogEvent">The client log event.</param> + void Log(ClientLogEvent clientLogEvent); + + /// <summary> + /// Writes a file to the log directory. + /// </summary> + /// <param name="clientName">The client name writing the document.</param> + /// <param name="clientVersion">The client version writing the document.</param> + /// <param name="fileContents">The file contents to write.</param> + /// <returns>The created file name.</returns> + Task<string> WriteDocumentAsync( + string clientName, + string clientVersion, + Stream fileContents); + } +} diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 63749b1f3..a76ca2305 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -1452,7 +1452,7 @@ namespace MediaBrowser.Controller.Entities } catch (Exception ex) { - Logger.LogError(ex, "Error refreshing owned items for {path}", Path ?? Name); + Logger.LogError(ex, "Error refreshing owned items for {Path}", Path ?? Name); } } diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index 18b4ec3c6..ffd1c7f0a 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -303,7 +303,7 @@ namespace MediaBrowser.Controller.Entities if (dictionary.ContainsKey(id)) { Logger.LogError( - "Found folder containing items with duplicate id. Path: {path}, Child Name: {ChildName}", + "Found folder containing items with duplicate id. Path: {Path}, Child Name: {ChildName}", Path ?? Name, child.Path ?? child.Name); } @@ -1013,6 +1013,7 @@ namespace MediaBrowser.Controller.Entities items = CollapseBoxSetItemsIfNeeded(items, query, this, user, ConfigurationManager, CollectionManager); } + #pragma warning disable CA1309 if (!string.IsNullOrEmpty(query.NameStartsWithOrGreater)) { items = items.Where(i => string.Compare(query.NameStartsWithOrGreater, i.SortName, StringComparison.InvariantCultureIgnoreCase) < 1); @@ -1027,6 +1028,7 @@ namespace MediaBrowser.Controller.Entities { items = items.Where(i => string.Compare(query.NameLessThan, i.SortName, StringComparison.InvariantCultureIgnoreCase) == 1); } + #pragma warning restore CA1309 // This must be the last filter if (!string.IsNullOrEmpty(query.AdjacentTo)) diff --git a/MediaBrowser.Controller/IO/FileData.cs b/MediaBrowser.Controller/IO/FileData.cs index b8a0bf331..2429ac42d 100644 --- a/MediaBrowser.Controller/IO/FileData.cs +++ b/MediaBrowser.Controller/IO/FileData.cs @@ -69,7 +69,7 @@ namespace MediaBrowser.Controller.IO if (string.IsNullOrEmpty(newPath)) { // invalid shortcut - could be old or target could just be unavailable - logger.LogWarning("Encountered invalid shortcut: " + fullName); + logger.LogWarning("Encountered invalid shortcut: {Path}", fullName); continue; } @@ -83,7 +83,7 @@ namespace MediaBrowser.Controller.IO } catch (Exception ex) { - logger.LogError(ex, "Error resolving shortcut from {path}", fullName); + logger.LogError(ex, "Error resolving shortcut from {Path}", fullName); } } else if (flattenFolderDepth > 0 && isDirectory) diff --git a/MediaBrowser.Controller/Library/IMediaSourceManager.cs b/MediaBrowser.Controller/Library/IMediaSourceManager.cs index e802796d3..f1758a9d8 100644 --- a/MediaBrowser.Controller/Library/IMediaSourceManager.cs +++ b/MediaBrowser.Controller/Library/IMediaSourceManager.cs @@ -33,13 +33,6 @@ namespace MediaBrowser.Controller.Library /// <summary> /// Gets the media streams. /// </summary> - /// <param name="mediaSourceId">The media source identifier.</param> - /// <returns>IEnumerable<MediaStream>.</returns> - List<MediaStream> GetMediaStreams(string mediaSourceId); - - /// <summary> - /// Gets the media streams. - /// </summary> /// <param name="query">The query.</param> /// <returns>IEnumerable<MediaStream>.</returns> List<MediaStream> GetMediaStreams(MediaStreamQuery query); diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index d37880865..1996335fe 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -15,10 +15,10 @@ <ItemGroup> <PackageReference Include="Diacritics" Version="3.3.4" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> - <PackageReference Include="System.Threading.Tasks.Dataflow" Version="6.0.0-rc.2*" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" /> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0" PrivateAssets="All" /> + <PackageReference Include="System.Threading.Tasks.Dataflow" Version="6.0.0" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs index 2ac4c728b..08d129a82 100644 --- a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs +++ b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CA1819, CS1591 using System; diff --git a/MediaBrowser.Controller/Providers/ItemInfo.cs b/MediaBrowser.Controller/Providers/ItemInfo.cs index b8dd416a2..3a97127ea 100644 --- a/MediaBrowser.Controller/Providers/ItemInfo.cs +++ b/MediaBrowser.Controller/Providers/ItemInfo.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index 5a36c1663..80eb45423 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -412,7 +412,7 @@ namespace MediaBrowser.LocalMetadata.Parsers { var actors = reader.ReadInnerXml(); - if (actors.Contains("<", StringComparison.Ordinal)) + if (actors.Contains('<', StringComparison.Ordinal)) { // This is one of the mis-named "Actors" full nodes created by MB2 // Create a reader and pass it to the persons node processor diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs index 5e7af23fc..1a8b5bb4e 100644 --- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs +++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs @@ -144,7 +144,7 @@ namespace MediaBrowser.LocalMetadata.Savers } catch (Exception ex) { - Logger.LogError(ex, "Error setting hidden attribute on {path}", path); + Logger.LogError(ex, "Error setting hidden attribute on {Path}", path); } } diff --git a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj index c1fd8e5fb..6bb8bcdab 100644 --- a/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj +++ b/MediaBrowser.MediaEncoding/MediaBrowser.MediaEncoding.csproj @@ -24,8 +24,8 @@ <ItemGroup> <PackageReference Include="BDInfo" Version="0.7.6.1" /> <PackageReference Include="libse" Version="3.6.2" /> - <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0-rc.2*" /> - <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0-rc.2*" /> + <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> + <PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" /> <PackageReference Include="UTF.Unknown" Version="2.5.0" /> </ItemGroup> diff --git a/MediaBrowser.Model/ClientLog/ClientLogEvent.cs b/MediaBrowser.Model/ClientLog/ClientLogEvent.cs new file mode 100644 index 000000000..21087b564 --- /dev/null +++ b/MediaBrowser.Model/ClientLog/ClientLogEvent.cs @@ -0,0 +1,75 @@ +using System; +using Microsoft.Extensions.Logging; + +namespace MediaBrowser.Model.ClientLog +{ + /// <summary> + /// The client log event. + /// </summary> + public class ClientLogEvent + { + /// <summary> + /// Initializes a new instance of the <see cref="ClientLogEvent"/> class. + /// </summary> + /// <param name="timestamp">The log timestamp.</param> + /// <param name="level">The log level.</param> + /// <param name="userId">The user id.</param> + /// <param name="clientName">The client name.</param> + /// <param name="clientVersion">The client version.</param> + /// <param name="deviceId">The device id.</param> + /// <param name="message">The message.</param> + public ClientLogEvent( + DateTime timestamp, + LogLevel level, + Guid? userId, + string clientName, + string clientVersion, + string deviceId, + string message) + { + Timestamp = timestamp; + UserId = userId; + ClientName = clientName; + ClientVersion = clientVersion; + DeviceId = deviceId; + Message = message; + Level = level; + } + + /// <summary> + /// Gets the event timestamp. + /// </summary> + public DateTime Timestamp { get; } + + /// <summary> + /// Gets the log level. + /// </summary> + public LogLevel Level { get; } + + /// <summary> + /// Gets the user id. + /// </summary> + public Guid? UserId { get; } + + /// <summary> + /// Gets the client name. + /// </summary> + public string ClientName { get; } + + /// <summary> + /// Gets the client version. + /// </summary> + public string ClientVersion { get; } + + /// + /// <summary> + /// Gets the device id. + /// </summary> + public string DeviceId { get; } + + /// <summary> + /// Gets the log message. + /// </summary> + public string Message { get; } + } +} diff --git a/MediaBrowser.Model/Configuration/ServerConfiguration.cs b/MediaBrowser.Model/Configuration/ServerConfiguration.cs index d1e999666..b79d18abd 100644 --- a/MediaBrowser.Model/Configuration/ServerConfiguration.cs +++ b/MediaBrowser.Model/Configuration/ServerConfiguration.cs @@ -459,5 +459,10 @@ namespace MediaBrowser.Model.Configuration /// Gets or sets a value indicating whether older plugins should automatically be deleted from the plugin folder. /// </summary> public bool RemoveOldPlugins { get; set; } + + /// <summary> + /// Gets or sets a value indicating whether clients should be allowed to upload logs. + /// </summary> + public bool AllowClientLogUpload { get; set; } = true; } } diff --git a/MediaBrowser.Model/Dlna/DlnaMaps.cs b/MediaBrowser.Model/Dlna/DlnaMaps.cs index 95cd0ac27..4613bc542 100644 --- a/MediaBrowser.Model/Dlna/DlnaMaps.cs +++ b/MediaBrowser.Model/Dlna/DlnaMaps.cs @@ -6,20 +6,6 @@ namespace MediaBrowser.Model.Dlna { public static class DlnaMaps { - private static readonly string DefaultStreaming = - FlagsToString(DlnaFlags.StreamingTransferMode | - DlnaFlags.BackgroundTransferMode | - DlnaFlags.ConnectionStall | - DlnaFlags.ByteBasedSeek | - DlnaFlags.DlnaV15); - - private static readonly string DefaultInteractive = - FlagsToString(DlnaFlags.InteractiveTransferMode | - DlnaFlags.BackgroundTransferMode | - DlnaFlags.ConnectionStall | - DlnaFlags.ByteBasedSeek | - DlnaFlags.DlnaV15); - public static string FlagsToString(DlnaFlags flags) { return string.Format(CultureInfo.InvariantCulture, "{0:X8}{1:D24}", (ulong)flags, 0); diff --git a/MediaBrowser.Model/Dlna/StreamInfo.cs b/MediaBrowser.Model/Dlna/StreamInfo.cs index 4414415a2..cf8465067 100644 --- a/MediaBrowser.Model/Dlna/StreamInfo.cs +++ b/MediaBrowser.Model/Dlna/StreamInfo.cs @@ -794,7 +794,7 @@ namespace MediaBrowser.Model.Dlna } // strip spaces to avoid having to encode h264 profile names - list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty))); + list.Add(new NameValuePair(pair.Key, pair.Value.Replace(" ", string.Empty, StringComparison.Ordinal))); } if (!item.IsDirectStream) diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj index 16bc4adf8..85947b3de 100644 --- a/MediaBrowser.Model/MediaBrowser.Model.csproj +++ b/MediaBrowser.Model/MediaBrowser.Model.csproj @@ -29,10 +29,10 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" /> - <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0-rc.2*" /> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.0" PrivateAssets="All" /> + <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" /> <PackageReference Include="System.Globalization" Version="4.3.0" /> - <PackageReference Include="System.Text.Json" Version="6.0.0-rc.2*" /> + <PackageReference Include="System.Text.Json" Version="6.0.0" /> </ItemGroup> <ItemGroup> diff --git a/MediaBrowser.Model/MediaInfo/AudioCodec.cs b/MediaBrowser.Model/MediaInfo/AudioCodec.cs index 8b17757b8..7b83b1b9d 100644 --- a/MediaBrowser.Model/MediaInfo/AudioCodec.cs +++ b/MediaBrowser.Model/MediaInfo/AudioCodec.cs @@ -1,13 +1,11 @@ #pragma warning disable CS1591 +using System; + namespace MediaBrowser.Model.MediaInfo { public static class AudioCodec { - public const string AAC = "aac"; - public const string MP3 = "mp3"; - public const string AC3 = "ac3"; - public static string GetFriendlyName(string codec) { if (codec.Length == 0) @@ -15,17 +13,20 @@ namespace MediaBrowser.Model.MediaInfo return codec; } - switch (codec.ToLowerInvariant()) + if (string.Equals(codec, "ac3", StringComparison.OrdinalIgnoreCase)) + { + return "Dolby Digital"; + } + else if (string.Equals(codec, "eac3", StringComparison.OrdinalIgnoreCase)) { - case "ac3": - return "Dolby Digital"; - case "eac3": - return "Dolby Digital+"; - case "dca": - return "DTS"; - default: - return codec.ToUpperInvariant(); + return "Dolby Digital+"; } + else if (string.Equals(codec, "dca", StringComparison.OrdinalIgnoreCase)) + { + return "DTS"; + } + + return codec.ToUpperInvariant(); } } } diff --git a/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs b/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs index 36a240706..24eab1a74 100644 --- a/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs +++ b/MediaBrowser.Model/MediaInfo/LiveStreamRequest.cs @@ -16,22 +16,6 @@ namespace MediaBrowser.Model.MediaInfo DirectPlayProtocols = new MediaProtocol[] { MediaProtocol.Http }; } - public LiveStreamRequest(AudioOptions options) - { - MaxStreamingBitrate = options.MaxBitrate; - ItemId = options.ItemId; - DeviceProfile = options.Profile; - MaxAudioChannels = options.MaxAudioChannels; - - DirectPlayProtocols = new MediaProtocol[] { MediaProtocol.Http }; - - if (options is VideoOptions videoOptions) - { - AudioStreamIndex = videoOptions.AudioStreamIndex; - SubtitleStreamIndex = videoOptions.SubtitleStreamIndex; - } - } - public string OpenToken { get; set; } public Guid UserId { get; set; } diff --git a/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs b/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs deleted file mode 100644 index ecd9b8834..000000000 --- a/MediaBrowser.Model/MediaInfo/PlaybackInfoRequest.cs +++ /dev/null @@ -1,58 +0,0 @@ -#nullable disable -#pragma warning disable CS1591 - -using System; -using MediaBrowser.Model.Dlna; - -namespace MediaBrowser.Model.MediaInfo -{ - public class PlaybackInfoRequest - { - public PlaybackInfoRequest() - { - EnableDirectPlay = true; - EnableDirectStream = true; - EnableTranscoding = true; - AllowVideoStreamCopy = true; - AllowAudioStreamCopy = true; - IsPlayback = true; - DirectPlayProtocols = new MediaProtocol[] { MediaProtocol.Http }; - } - - public Guid Id { get; set; } - - public Guid UserId { get; set; } - - public long? MaxStreamingBitrate { get; set; } - - public long? StartTimeTicks { get; set; } - - public int? AudioStreamIndex { get; set; } - - public int? SubtitleStreamIndex { get; set; } - - public int? MaxAudioChannels { get; set; } - - public string MediaSourceId { get; set; } - - public string LiveStreamId { get; set; } - - public DeviceProfile DeviceProfile { get; set; } - - public bool EnableDirectPlay { get; set; } - - public bool EnableDirectStream { get; set; } - - public bool EnableTranscoding { get; set; } - - public bool AllowVideoStreamCopy { get; set; } - - public bool AllowAudioStreamCopy { get; set; } - - public bool IsPlayback { get; set; } - - public bool AutoOpenLiveStream { get; set; } - - public MediaProtocol[] DirectPlayProtocols { get; set; } - } -} diff --git a/MediaBrowser.Model/MediaInfo/SubtitleFormat.cs b/MediaBrowser.Model/MediaInfo/SubtitleFormat.cs index 2bd45695a..9bc5c31f6 100644 --- a/MediaBrowser.Model/MediaInfo/SubtitleFormat.cs +++ b/MediaBrowser.Model/MediaInfo/SubtitleFormat.cs @@ -8,8 +8,6 @@ namespace MediaBrowser.Model.MediaInfo public const string SSA = "ssa"; public const string ASS = "ass"; public const string VTT = "vtt"; - public const string SUB = "sub"; - public const string SMI = "smi"; public const string TTML = "ttml"; } } diff --git a/MediaBrowser.Model/Net/NetworkShareType.cs b/MediaBrowser.Model/Net/NetworkShareType.cs deleted file mode 100644 index 5d985f85d..000000000 --- a/MediaBrowser.Model/Net/NetworkShareType.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace MediaBrowser.Model.Net -{ - /// <summary> - /// Enum NetworkShareType. - /// </summary> - public enum NetworkShareType - { - /// <summary> - /// Disk share. - /// </summary> - Disk, - - /// <summary> - /// Printer share. - /// </summary> - Printer, - - /// <summary> - /// Device share. - /// </summary> - Device, - - /// <summary> - /// IPC share. - /// </summary> - Ipc, - - /// <summary> - /// Special share. - /// </summary> - Special - } -} diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index 1022a3fae..5e1985611 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -215,7 +215,7 @@ namespace MediaBrowser.Providers.Manager catch (Exception ex) { result.ErrorMessage = ex.Message; - _logger.LogError(ex, "Error in {provider}", provider.Name); + _logger.LogError(ex, "Error in {Provider}", provider.Name); } } @@ -331,7 +331,7 @@ namespace MediaBrowser.Providers.Manager catch (Exception ex) { result.ErrorMessage = ex.Message; - _logger.LogError(ex, "Error in {provider}", provider.Name); + _logger.LogError(ex, "Error in {Provider}", provider.Name); } } diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index ffb3baeb1..90d14a973 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -713,7 +713,7 @@ namespace MediaBrowser.Providers.Manager } catch (Exception ex) { - Logger.LogError(ex, "Error in {provider}", provider.Name); + Logger.LogError(ex, "Error in {Provider}", provider.Name); // If a local provider fails, consider that a failure refreshResult.ErrorMessage = ex.Message; @@ -785,7 +785,7 @@ namespace MediaBrowser.Providers.Manager catch (Exception ex) { refreshResult.ErrorMessage = ex.Message; - Logger.LogError(ex, "Error in {provider}", provider.Name); + Logger.LogError(ex, "Error in {Provider}", provider.Name); } } @@ -837,7 +837,7 @@ namespace MediaBrowser.Providers.Manager { refreshResult.Failures++; refreshResult.ErrorMessage = ex.Message; - Logger.LogError(ex, "Error in {provider}", provider.Name); + Logger.LogError(ex, "Error in {Provider}", provider.Name); } } diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 9d0a6944b..b42112111 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -16,9 +16,9 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0-rc.2*" /> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Http" Version="6.0.0" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="OptimizedPriorityQueue" Version="5.0.0" /> <PackageReference Include="PlaylistsNET" Version="1.1.3" /> diff --git a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs index c41d36d76..9b63971a9 100644 --- a/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/EmbeddedImageProvider.cs @@ -125,7 +125,7 @@ namespace MediaBrowser.Providers.MediaInfo if (attachmentStream != null) { - return await ExtractAttachment(item, cancellationToken, attachmentStream, mediaSource); + return await ExtractAttachment(item, attachmentStream, mediaSource, cancellationToken); } // Fall back to EmbeddedImage streams @@ -169,7 +169,7 @@ namespace MediaBrowser.Providers.MediaInfo }; } - private async Task<DynamicImageResponse> ExtractAttachment(Video item, CancellationToken cancellationToken, MediaAttachment attachmentStream, MediaSourceInfo mediaSource) + private async Task<DynamicImageResponse> ExtractAttachment(Video item, MediaAttachment attachmentStream, MediaSourceInfo mediaSource, CancellationToken cancellationToken) { var extension = string.IsNullOrEmpty(attachmentStream.MimeType) ? Path.GetExtension(attachmentStream.FileName) diff --git a/MediaBrowser.XbmcMetadata/EntryPoint.cs b/MediaBrowser.XbmcMetadata/EntryPoint.cs index d02aea556..935ff5f59 100644 --- a/MediaBrowser.XbmcMetadata/EntryPoint.cs +++ b/MediaBrowser.XbmcMetadata/EntryPoint.cs @@ -71,7 +71,7 @@ namespace MediaBrowser.XbmcMetadata } catch (Exception ex) { - _logger.LogError(ex, "Error saving metadata for {path}", item.Path ?? item.Name); + _logger.LogError(ex, "Error saving metadata for {Path}", item.Path ?? item.Name); } } } diff --git a/deployment/Dockerfile.centos.amd64 b/deployment/Dockerfile.centos.amd64 index 78f051e4f..3967a165d 100644 --- a/deployment/Dockerfile.centos.amd64 +++ b/deployment/Dockerfile.centos.amd64 @@ -13,7 +13,7 @@ RUN yum update -yq \ && yum install -yq @buildsys-build rpmdevtools yum-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel git wget # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/20283373-1d83-4879-8278-0afb7fd4035e/56f204f174743b29a656499ad0fc93c3/dotnet-sdk-6.0.100-rc.2.21505.57-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/17b6759f-1af0-41bc-ab12-209ba0377779/e8d02195dbf1434b940e0f05ae086453/dotnet-sdk-6.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.fedora.amd64 b/deployment/Dockerfile.fedora.amd64 index 14eeb6eed..bc40a8059 100644 --- a/deployment/Dockerfile.fedora.amd64 +++ b/deployment/Dockerfile.fedora.amd64 @@ -12,7 +12,7 @@ RUN dnf update -yq \ && dnf install -yq @buildsys-build rpmdevtools git dnf-plugins-core libcurl-devel fontconfig-devel freetype-devel openssl-devel glibc-devel libicu-devel systemd wget # Install DotNET SDK -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/20283373-1d83-4879-8278-0afb7fd4035e/56f204f174743b29a656499ad0fc93c3/dotnet-sdk-6.0.100-rc.2.21505.57-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/17b6759f-1af0-41bc-ab12-209ba0377779/e8d02195dbf1434b940e0f05ae086453/dotnet-sdk-6.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.amd64 b/deployment/Dockerfile.ubuntu.amd64 index 8733be89c..c1b541c59 100644 --- a/deployment/Dockerfile.ubuntu.amd64 +++ b/deployment/Dockerfile.ubuntu.amd64 @@ -17,7 +17,7 @@ RUN apt-get update -yqq \ libfreetype6-dev libssl-dev libssl1.1 liblttng-ust0 # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/20283373-1d83-4879-8278-0afb7fd4035e/56f204f174743b29a656499ad0fc93c3/dotnet-sdk-6.0.100-rc.2.21505.57-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/17b6759f-1af0-41bc-ab12-209ba0377779/e8d02195dbf1434b940e0f05ae086453/dotnet-sdk-6.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.arm64 b/deployment/Dockerfile.ubuntu.arm64 index 6ae0d53cc..6aa98a289 100644 --- a/deployment/Dockerfile.ubuntu.arm64 +++ b/deployment/Dockerfile.ubuntu.arm64 @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/20283373-1d83-4879-8278-0afb7fd4035e/56f204f174743b29a656499ad0fc93c3/dotnet-sdk-6.0.100-rc.2.21505.57-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/17b6759f-1af0-41bc-ab12-209ba0377779/e8d02195dbf1434b940e0f05ae086453/dotnet-sdk-6.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/deployment/Dockerfile.ubuntu.armhf b/deployment/Dockerfile.ubuntu.armhf index 154388148..cc9d8dc79 100644 --- a/deployment/Dockerfile.ubuntu.armhf +++ b/deployment/Dockerfile.ubuntu.armhf @@ -16,7 +16,7 @@ RUN apt-get update -yqq \ mmv build-essential lsb-release # Install dotnet repository -RUN wget -q https://download.visualstudio.microsoft.com/download/pr/20283373-1d83-4879-8278-0afb7fd4035e/56f204f174743b29a656499ad0fc93c3/dotnet-sdk-6.0.100-rc.2.21505.57-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ +RUN wget -q https://download.visualstudio.microsoft.com/download/pr/17b6759f-1af0-41bc-ab12-209ba0377779/e8d02195dbf1434b940e0f05ae086453/dotnet-sdk-6.0.100-linux-x64.tar.gz -O dotnet-sdk.tar.gz \ && mkdir -p dotnet-sdk \ && tar -xzf dotnet-sdk.tar.gz -C dotnet-sdk \ && ln -s $( pwd )/dotnet-sdk/dotnet /usr/bin/dotnet diff --git a/jellyfin.ruleset b/jellyfin.ruleset index dfb991170..3bced438c 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -42,6 +42,8 @@ <Rule Id="CA1305" Action="Error" /> <!-- error on CA1725: Parameter names should match base declaration --> <Rule Id="CA1725" Action="Error" /> + <!-- error on CA1725: Call async methods when in an async method --> + <Rule Id="CA1727" Action="Error" /> <!-- error on CA2016: Forward the CancellationToken parameter to methods that take one or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token --> <Rule Id="CA2016" Action="Error" /> @@ -77,6 +79,8 @@ <Rule Id="CA1822" Action="Info" /> <!-- disable warning CA2000: Dispose objects before losing scope --> <Rule Id="CA2000" Action="Info" /> + <!-- disable warning CA2253: Named placeholders should not be numeric values --> + <Rule Id="CA2253" Action="Info" /> <!-- disable warning CA5394: Do not use insecure randomness --> <Rule Id="CA5394" Action="Info" /> @@ -90,6 +94,8 @@ <Rule Id="CA1303" Action="None" /> <!-- disable warning CA1308: Normalize strings to uppercase --> <Rule Id="CA1308" Action="None" /> + <!-- disable warning CA1848: Use the LoggerMessage delegates --> + <Rule Id="CA1848" Action="None" /> <!-- disable warning CA2101: Specify marshaling for P/Invoke string arguments --> <Rule Id="CA2101" Action="None" /> <!-- disable warning CA2234: Pass System.Uri objects instead of strings --> diff --git a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj index 57ec86316..2aced0669 100644 --- a/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj +++ b/tests/Jellyfin.Api.Tests/Jellyfin.Api.Tests.csproj @@ -15,8 +15,8 @@ <PackageReference Include="AutoFixture" Version="4.17.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" /> - <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0-rc.2*" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> diff --git a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs index 9f73ed7fc..2ba5c47d7 100644 --- a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs +++ b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs @@ -231,12 +231,12 @@ namespace Jellyfin.Providers.Tests.Manager .ReturnsAsync(imageResponse); var refreshOptions = forceRefresh - ? new ImageRefreshOptions(null) + ? new ImageRefreshOptions(Mock.Of<IDirectoryService>()) { ImageRefreshMode = MetadataRefreshMode.FullRefresh, ReplaceAllImages = true } - : new ImageRefreshOptions(null); + : new ImageRefreshOptions(Mock.Of<IDirectoryService>()); var itemImageProvider = GetItemImageProvider(null, new Mock<IFileSystem>()); var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { dynamicProvider.Object }, refreshOptions, CancellationToken.None); @@ -286,7 +286,7 @@ namespace Jellyfin.Providers.Tests.Manager dynamicProvider.Setup(rp => rp.GetImage(item, imageType, It.IsAny<CancellationToken>())) .ReturnsAsync(imageResponse); - var refreshOptions = new ImageRefreshOptions(null); + var refreshOptions = new ImageRefreshOptions(Mock.Of<IDirectoryService>()); var providerManager = new Mock<IProviderManager>(MockBehavior.Strict); providerManager.Setup(pm => pm.SaveImage(item, It.IsAny<Stream>(), It.IsAny<string>(), imageType, null, It.IsAny<CancellationToken>())) @@ -323,12 +323,12 @@ namespace Jellyfin.Providers.Tests.Manager .Returns(new[] { imageType }); var refreshOptions = forceRefresh - ? new ImageRefreshOptions(null) + ? new ImageRefreshOptions(Mock.Of<IDirectoryService>()) { ImageRefreshMode = MetadataRefreshMode.FullRefresh, ReplaceAllImages = true } - : new ImageRefreshOptions(null); + : new ImageRefreshOptions(Mock.Of<IDirectoryService>()); var remoteInfo = new RemoteImageInfo[imageCount]; for (int i = 0; i < imageCount; i++) @@ -393,12 +393,12 @@ namespace Jellyfin.Providers.Tests.Manager }); var refreshOptions = fullRefresh - ? new ImageRefreshOptions(null) + ? new ImageRefreshOptions(Mock.Of<IDirectoryService>()) { ImageRefreshMode = MetadataRefreshMode.FullRefresh, ReplaceAllImages = true } - : new ImageRefreshOptions(null); + : new ImageRefreshOptions(Mock.Of<IDirectoryService>()); var remoteInfo = new RemoteImageInfo[targetImageCount]; for (int i = 0; i < targetImageCount; i++) @@ -442,7 +442,7 @@ namespace Jellyfin.Providers.Tests.Manager remoteProvider.Setup(rp => rp.GetSupportedImages(item)) .Returns(new[] { imageType }); - var refreshOptions = new ImageRefreshOptions(null); + var refreshOptions = new ImageRefreshOptions(Mock.Of<IDirectoryService>()); // populate remote with double the required images to verify count is trimmed to the library option count var remoteInfoCount = imageCount * 2; @@ -487,7 +487,7 @@ namespace Jellyfin.Providers.Tests.Manager remoteProvider.Setup(rp => rp.GetSupportedImages(item)) .Returns(new[] { imageType }); - var refreshOptions = new ImageRefreshOptions(null) + var refreshOptions = new ImageRefreshOptions(Mock.Of<IDirectoryService>()) { ImageRefreshMode = MetadataRefreshMode.FullRefresh, ReplaceAllImages = true diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs new file mode 100644 index 000000000..8ed3d8b94 --- /dev/null +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/MediaSourceManagerTests.cs @@ -0,0 +1,32 @@ +using AutoFixture; +using AutoFixture.AutoMoq; +using Emby.Server.Implementations.IO; +using Emby.Server.Implementations.Library; +using MediaBrowser.Model.IO; +using MediaBrowser.Model.MediaInfo; +using Xunit; + +namespace Jellyfin.Server.Implementations.Tests.Library +{ + public class MediaSourceManagerTests + { + private readonly MediaSourceManager _mediaSourceManager; + + public MediaSourceManagerTests() + { + IFixture fixture = new Fixture().Customize(new AutoMoqCustomization { ConfigureMembers = true }); + fixture.Inject<IFileSystem>(fixture.Create<ManagedFileSystem>()); + _mediaSourceManager = fixture.Create<MediaSourceManager>(); + } + + [Theory] + [InlineData(@"C:\mydir\myfile.ext", MediaProtocol.File)] + [InlineData("/mydir/myfile.ext", MediaProtocol.File)] + [InlineData("file:///mydir/myfile.ext", MediaProtocol.File)] + [InlineData("http://example.com/stream.m3u8", MediaProtocol.Http)] + [InlineData("https://example.com/stream.m3u8", MediaProtocol.Http)] + [InlineData("rtsp://media.example.com:554/twister/audiotrack", MediaProtocol.Rtsp)] + public void GetPathProtocol_ValidArg_Correct(string path, MediaProtocol expected) + => Assert.Equal(expected, _mediaSourceManager.GetPathProtocol(path)); + } +} diff --git a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs index 827365363..3396a94e5 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs +++ b/tests/Jellyfin.Server.Integration.Tests/Controllers/DashboardControllerTests.cs @@ -40,7 +40,7 @@ namespace Jellyfin.Server.Integration.Tests.Controllers Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(MediaTypeNames.Text.Html, response.Content.Headers.ContentType?.MediaType); StreamReader reader = new StreamReader(typeof(TestPlugin).Assembly.GetManifestResourceStream("Jellyfin.Server.Integration.Tests.TestPage.html")!); - Assert.Equal(await response.Content.ReadAsStringAsync(), reader.ReadToEnd()); + Assert.Equal(await response.Content.ReadAsStringAsync().ConfigureAwait(false), await reader.ReadToEndAsync().ConfigureAwait(false)); } [Fact] diff --git a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj index 889220d86..5b884cddf 100644 --- a/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj +++ b/tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj @@ -9,8 +9,8 @@ <PackageReference Include="AutoFixture" Version="4.17.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" /> - <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0-rc.2*" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> diff --git a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj index 3daa45e56..29d7646a6 100644 --- a/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj +++ b/tests/Jellyfin.Server.Tests/Jellyfin.Server.Tests.csproj @@ -10,8 +10,8 @@ <PackageReference Include="AutoFixture" Version="4.17.0" /> <PackageReference Include="AutoFixture.AutoMoq" Version="4.17.0" /> <PackageReference Include="AutoFixture.Xunit2" Version="4.17.0" /> - <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.0-rc.2*" /> - <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0-rc.2*" /> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Options" Version="6.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="xunit" Version="2.4.1" /> <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" /> |
