diff options
83 files changed, 902 insertions, 960 deletions
diff --git a/.ci/azure-pipelines-package.yml b/.ci/azure-pipelines-package.yml index 272111299..1677f71c7 100644 --- a/.ci/azure-pipelines-package.yml +++ b/.ci/azure-pipelines-package.yml @@ -80,7 +80,15 @@ jobs: pool: vmImage: 'ubuntu-latest' + variables: + - name: JellyfinVersion + value: 0.0.0 + steps: + - script: echo "##vso[task.setvariable variable=JellyfinVersion]$( awk -F '/' '{ print $NF }' <<<'$(Build.SourceBranch)' | sed 's/^v//' )" + displayName: Set release version (stable) + condition: startsWith(variables['Build.SourceBranch'], 'refs/tags') + - task: Docker@2 displayName: 'Push Unstable Image' condition: startsWith(variables['Build.SourceBranch'], 'refs/heads/master') @@ -105,7 +113,7 @@ jobs: containerRegistry: Docker Hub tags: | stable-$(Build.BuildNumber)-$(BuildConfiguration) - stable-$(BuildConfiguration) + $(JellyfinVersion)-$(BuildConfiguration) - job: CollectArtifacts displayName: 'Collect Artifacts' diff --git a/Emby.Dlna/Didl/DidlBuilder.cs b/Emby.Dlna/Didl/DidlBuilder.cs index 66baa9512..70e358019 100644 --- a/Emby.Dlna/Didl/DidlBuilder.cs +++ b/Emby.Dlna/Didl/DidlBuilder.cs @@ -364,7 +364,8 @@ namespace Emby.Dlna.Didl writer.WriteAttributeString("bitrate", totalBitrate.Value.ToString(_usCulture)); } - var mediaProfile = _profile.GetVideoMediaProfile(streamInfo.Container, + var mediaProfile = _profile.GetVideoMediaProfile( + streamInfo.Container, streamInfo.TargetAudioCodec.FirstOrDefault(), streamInfo.TargetVideoCodec.FirstOrDefault(), streamInfo.TargetAudioBitrate, diff --git a/Emby.Dlna/DlnaManager.cs b/Emby.Dlna/DlnaManager.cs index 5911a73ef..269f7ee43 100644 --- a/Emby.Dlna/DlnaManager.cs +++ b/Emby.Dlna/DlnaManager.cs @@ -122,15 +122,15 @@ namespace Emby.Dlna var builder = new StringBuilder(); builder.AppendLine("No matching device profile found. The default will need to be used."); - builder.AppendLine(string.Format("DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty)); - builder.AppendLine(string.Format("FriendlyName:{0}", profile.FriendlyName ?? string.Empty)); - builder.AppendLine(string.Format("Manufacturer:{0}", profile.Manufacturer ?? string.Empty)); - builder.AppendLine(string.Format("ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty)); - builder.AppendLine(string.Format("ModelDescription:{0}", profile.ModelDescription ?? string.Empty)); - builder.AppendLine(string.Format("ModelName:{0}", profile.ModelName ?? string.Empty)); - builder.AppendLine(string.Format("ModelNumber:{0}", profile.ModelNumber ?? string.Empty)); - builder.AppendLine(string.Format("ModelUrl:{0}", profile.ModelUrl ?? string.Empty)); - builder.AppendLine(string.Format("SerialNumber:{0}", profile.SerialNumber ?? string.Empty)); + builder.AppendFormat(CultureInfo.InvariantCulture, "DeviceDescription:{0}", profile.DeviceDescription ?? string.Empty).AppendLine(); + builder.AppendFormat(CultureInfo.InvariantCulture, "FriendlyName:{0}", profile.FriendlyName ?? string.Empty).AppendLine(); + builder.AppendFormat(CultureInfo.InvariantCulture, "Manufacturer:{0}", profile.Manufacturer ?? string.Empty).AppendLine(); + builder.AppendFormat(CultureInfo.InvariantCulture, "ManufacturerUrl:{0}", profile.ManufacturerUrl ?? string.Empty).AppendLine(); + builder.AppendFormat(CultureInfo.InvariantCulture, "ModelDescription:{0}", profile.ModelDescription ?? string.Empty).AppendLine(); + builder.AppendFormat(CultureInfo.InvariantCulture, "ModelName:{0}", profile.ModelName ?? string.Empty).AppendLine(); + builder.AppendFormat(CultureInfo.InvariantCulture, "ModelNumber:{0}", profile.ModelNumber ?? string.Empty).AppendLine(); + builder.AppendFormat(CultureInfo.InvariantCulture, "ModelUrl:{0}", profile.ModelUrl ?? string.Empty).AppendLine(); + builder.AppendFormat(CultureInfo.InvariantCulture, "SerialNumber:{0}", profile.SerialNumber ?? string.Empty).AppendLine(); _logger.LogInformation(builder.ToString()); } @@ -387,7 +387,7 @@ namespace Emby.Dlna foreach (var name in _assembly.GetManifestResourceNames()) { - if (!name.StartsWith(namespaceName)) + if (!name.StartsWith(namespaceName, StringComparison.Ordinal)) { continue; } @@ -406,7 +406,7 @@ namespace Emby.Dlna using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read)) { - await stream.CopyToAsync(fileStream); + await stream.CopyToAsync(fileStream).ConfigureAwait(false); } } } @@ -509,7 +509,7 @@ namespace Emby.Dlna return _jsonSerializer.DeserializeFromString<DeviceProfile>(json); } - class InternalProfileInfo + private class InternalProfileInfo { internal DeviceProfileInfo Info { get; set; } diff --git a/Emby.Drawing/ImageProcessor.cs b/Emby.Drawing/ImageProcessor.cs index 8696cb280..f585b90ca 100644 --- a/Emby.Drawing/ImageProcessor.cs +++ b/Emby.Drawing/ImageProcessor.cs @@ -448,21 +448,21 @@ namespace Emby.Drawing /// or /// filename. /// </exception> - public string GetCachePath(string path, string filename) + public string GetCachePath(ReadOnlySpan<char> path, ReadOnlySpan<char> filename) { - if (string.IsNullOrEmpty(path)) + if (path.IsEmpty) { - throw new ArgumentNullException(nameof(path)); + throw new ArgumentException("Path can't be empty.", nameof(path)); } - if (string.IsNullOrEmpty(filename)) + if (path.IsEmpty) { - throw new ArgumentNullException(nameof(filename)); + throw new ArgumentException("Filename can't be empty.", nameof(filename)); } - var prefix = filename.Substring(0, 1); + var prefix = filename.Slice(0, 1); - return Path.Combine(path, prefix, filename); + return Path.Join(path, prefix, filename); } /// <inheritdoc /> diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs index 2fa6b4353..d2e324dda 100644 --- a/Emby.Naming/TV/SeasonPathParser.cs +++ b/Emby.Naming/TV/SeasonPathParser.cs @@ -77,7 +77,7 @@ namespace Emby.Naming.TV if (filename.StartsWith("s", StringComparison.OrdinalIgnoreCase)) { - var testFilename = filename.Substring(1); + var testFilename = filename.AsSpan().Slice(1); if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) { diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 2a7cddd87..8d6292867 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -23,6 +22,7 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Serialization; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Episode = MediaBrowser.Controller.Entities.TV.Episode; using Movie = MediaBrowser.Controller.Entities.Movies.Movie; @@ -46,10 +46,7 @@ namespace Emby.Server.Implementations.Channels private readonly IFileSystem _fileSystem; private readonly IJsonSerializer _jsonSerializer; private readonly IProviderManager _providerManager; - - private readonly ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>> _channelItemMediaInfo = - new ConcurrentDictionary<string, Tuple<DateTime, List<MediaSourceInfo>>>(); - + private readonly IMemoryCache _memoryCache; private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1); /// <summary> @@ -64,6 +61,7 @@ namespace Emby.Server.Implementations.Channels /// <param name="userDataManager">The user data manager.</param> /// <param name="jsonSerializer">The JSON serializer.</param> /// <param name="providerManager">The provider manager.</param> + /// <param name="memoryCache">The memory cache.</param> public ChannelManager( IUserManager userManager, IDtoService dtoService, @@ -73,7 +71,8 @@ namespace Emby.Server.Implementations.Channels IFileSystem fileSystem, IUserDataManager userDataManager, IJsonSerializer jsonSerializer, - IProviderManager providerManager) + IProviderManager providerManager, + IMemoryCache memoryCache) { _userManager = userManager; _dtoService = dtoService; @@ -84,6 +83,7 @@ namespace Emby.Server.Implementations.Channels _userDataManager = userDataManager; _jsonSerializer = jsonSerializer; _providerManager = providerManager; + _memoryCache = memoryCache; } internal IChannel[] Channels { get; private set; } @@ -418,20 +418,15 @@ namespace Emby.Server.Implementations.Channels private async Task<IEnumerable<MediaSourceInfo>> GetChannelItemMediaSourcesInternal(IRequiresMediaInfoCallback channel, string id, CancellationToken cancellationToken) { - if (_channelItemMediaInfo.TryGetValue(id, out Tuple<DateTime, List<MediaSourceInfo>> cachedInfo)) + if (_memoryCache.TryGetValue(id, out List<MediaSourceInfo> cachedInfo)) { - if ((DateTime.UtcNow - cachedInfo.Item1).TotalMinutes < 5) - { - return cachedInfo.Item2; - } + return cachedInfo; } var mediaInfo = await channel.GetChannelItemMediaInfo(id, cancellationToken) .ConfigureAwait(false); var list = mediaInfo.ToList(); - - var item2 = new Tuple<DateTime, List<MediaSourceInfo>>(DateTime.UtcNow, list); - _channelItemMediaInfo.AddOrUpdate(id, item2, (key, oldValue) => item2); + _memoryCache.CreateEntry(id).SetValue(list).SetAbsoluteExpiration(DateTimeOffset.UtcNow.AddMinutes(5)); return list; } diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index e46cfa177..6188da35a 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -2777,82 +2777,82 @@ namespace Emby.Server.Implementations.Data private string FixUnicodeChars(string buffer) { - if (buffer.IndexOf('\u2013') > -1) + if (buffer.IndexOf('\u2013', StringComparison.Ordinal) > -1) { buffer = buffer.Replace('\u2013', '-'); // en dash } - if (buffer.IndexOf('\u2014') > -1) + if (buffer.IndexOf('\u2014', StringComparison.Ordinal) > -1) { buffer = buffer.Replace('\u2014', '-'); // em dash } - if (buffer.IndexOf('\u2015') > -1) + if (buffer.IndexOf('\u2015', StringComparison.Ordinal) > -1) { buffer = buffer.Replace('\u2015', '-'); // horizontal bar } - if (buffer.IndexOf('\u2017') > -1) + if (buffer.IndexOf('\u2017', StringComparison.Ordinal) > -1) { buffer = buffer.Replace('\u2017', '_'); // double low line } - if (buffer.IndexOf('\u2018') > -1) + if (buffer.IndexOf('\u2018', StringComparison.Ordinal) > -1) { buffer = buffer.Replace('\u2018', '\''); // left single quotation mark } - if (buffer.IndexOf('\u2019') > -1) + if (buffer.IndexOf('\u2019', StringComparison.Ordinal) > -1) { buffer = buffer.Replace('\u2019', '\''); // right single quotation mark } - if (buffer.IndexOf('\u201a') > -1) + if (buffer.IndexOf('\u201a', StringComparison.Ordinal) > -1) { buffer = buffer.Replace('\u201a', ','); // single low-9 quotation mark } - if (buffer.IndexOf('\u201b') > -1) + if (buffer.IndexOf('\u201b', StringComparison.Ordinal) > -1) { buffer = buffer.Replace('\u201b', '\''); // single high-reversed-9 quotation mark } - if (buffer.IndexOf('\u201c') > -1) + if (buffer.IndexOf('\u201c', StringComparison.Ordinal) > -1) { buffer = buffer.Replace('\u201c', '\"'); // left double quotation mark } - if (buffer.IndexOf('\u201d') > -1) + if (buffer.IndexOf('\u201d', StringComparison.Ordinal) > -1) { buffer = buffer.Replace('\u201d', '\"'); // right double quotation mark } - if (buffer.IndexOf('\u201e') > -1) + if (buffer.IndexOf('\u201e', StringComparison.Ordinal) > -1) { buffer = buffer.Replace('\u201e', '\"'); // double low-9 quotation mark } - if (buffer.IndexOf('\u2026') > -1) + if (buffer.IndexOf('\u2026', StringComparison.Ordinal) > -1) { - buffer = buffer.Replace("\u2026", "..."); // horizontal ellipsis + buffer = buffer.Replace("\u2026", "...", StringComparison.Ordinal); // horizontal ellipsis } - if (buffer.IndexOf('\u2032') > -1) + if (buffer.IndexOf('\u2032', StringComparison.Ordinal) > -1) { buffer = buffer.Replace('\u2032', '\''); // prime } - if (buffer.IndexOf('\u2033') > -1) + if (buffer.IndexOf('\u2033', StringComparison.Ordinal) > -1) { buffer = buffer.Replace('\u2033', '\"'); // double prime } - if (buffer.IndexOf('\u0060') > -1) + if (buffer.IndexOf('\u0060', StringComparison.Ordinal) > -1) { buffer = buffer.Replace('\u0060', '\''); // grave accent } - if (buffer.IndexOf('\u00B4') > -1) + if (buffer.IndexOf('\u00B4', StringComparison.Ordinal) > -1) { buffer = buffer.Replace('\u00B4', '\''); // acute accent } @@ -3001,7 +3001,6 @@ namespace Emby.Server.Implementations.Data { connection.RunInTransaction(db => { - var statements = PrepareAll(db, statementTexts).ToList(); if (!isReturningZeroItems) @@ -4671,8 +4670,12 @@ namespace Emby.Server.Implementations.Data if (query.BlockUnratedItems.Length > 1) { - var inClause = string.Join(",", query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'")); - whereClauses.Add(string.Format("(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))", inClause)); + var inClause = string.Join(',', query.BlockUnratedItems.Select(i => "'" + i.ToString() + "'")); + whereClauses.Add( + string.Format( + CultureInfo.InvariantCulture, + "(InheritedParentalRatingValue > 0 or UnratedType not in ({0}))", + inClause)); } if (query.ExcludeInheritedTags.Length > 0) @@ -4681,7 +4684,7 @@ namespace Emby.Server.Implementations.Data if (statement == null) { int index = 0; - string excludedTags = string.Join(",", query.ExcludeInheritedTags.Select(t => paramName + index++)); + string excludedTags = string.Join(',', query.ExcludeInheritedTags.Select(t => paramName + index++)); whereClauses.Add("((select CleanValue from itemvalues where ItemId=Guid and Type=6 and cleanvalue in (" + excludedTags + ")) is null)"); } else diff --git a/Emby.Server.Implementations/Devices/DeviceManager.cs b/Emby.Server.Implementations/Devices/DeviceManager.cs index e75745cc6..2921a7f0e 100644 --- a/Emby.Server.Implementations/Devices/DeviceManager.cs +++ b/Emby.Server.Implementations/Devices/DeviceManager.cs @@ -5,8 +5,8 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; -using Jellyfin.Data.Enums; using Jellyfin.Data.Entities; +using Jellyfin.Data.Enums; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Devices; @@ -17,16 +17,17 @@ using MediaBrowser.Model.Events; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Session; +using Microsoft.Extensions.Caching.Memory; namespace Emby.Server.Implementations.Devices { public class DeviceManager : IDeviceManager { + private readonly IMemoryCache _memoryCache; private readonly IJsonSerializer _json; private readonly IUserManager _userManager; private readonly IServerConfigurationManager _config; private readonly IAuthenticationRepository _authRepo; - private readonly Dictionary<string, ClientCapabilities> _capabilitiesCache; private readonly object _capabilitiesSyncLock = new object(); public event EventHandler<GenericEventArgs<Tuple<string, DeviceOptions>>> DeviceOptionsUpdated; @@ -35,13 +36,14 @@ namespace Emby.Server.Implementations.Devices IAuthenticationRepository authRepo, IJsonSerializer json, IUserManager userManager, - IServerConfigurationManager config) + IServerConfigurationManager config, + IMemoryCache memoryCache) { _json = json; _userManager = userManager; _config = config; + _memoryCache = memoryCache; _authRepo = authRepo; - _capabilitiesCache = new Dictionary<string, ClientCapabilities>(StringComparer.OrdinalIgnoreCase); } public void SaveCapabilities(string deviceId, ClientCapabilities capabilities) @@ -51,8 +53,7 @@ namespace Emby.Server.Implementations.Devices lock (_capabilitiesSyncLock) { - _capabilitiesCache[deviceId] = capabilities; - + _memoryCache.CreateEntry(deviceId).SetValue(capabilities); _json.SerializeToFile(capabilities, path); } } @@ -71,13 +72,13 @@ namespace Emby.Server.Implementations.Devices public ClientCapabilities GetCapabilities(string id) { - lock (_capabilitiesSyncLock) + if (_memoryCache.TryGetValue(id, out ClientCapabilities result)) { - if (_capabilitiesCache.TryGetValue(id, out var result)) - { - return result; - } + return result; + } + lock (_capabilitiesSyncLock) + { var path = Path.Combine(GetDevicePath(id), "capabilities.json"); try { diff --git a/Emby.Server.Implementations/HttpServer/FileWriter.cs b/Emby.Server.Implementations/HttpServer/FileWriter.cs index 590eee1b4..6fce8de44 100644 --- a/Emby.Server.Implementations/HttpServer/FileWriter.cs +++ b/Emby.Server.Implementations/HttpServer/FileWriter.cs @@ -29,7 +29,6 @@ namespace Emby.Server.Implementations.HttpServer private readonly IStreamHelper _streamHelper; private readonly ILogger _logger; - private readonly IFileSystem _fileSystem; /// <summary> /// The _options. @@ -49,7 +48,6 @@ namespace Emby.Server.Implementations.HttpServer } _streamHelper = streamHelper; - _fileSystem = fileSystem; Path = path; _logger = logger; diff --git a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs index c3428ee62..0d4a789b5 100644 --- a/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs +++ b/Emby.Server.Implementations/HttpServer/HttpListenerHost.cs @@ -449,7 +449,7 @@ namespace Emby.Server.Implementations.HttpServer if (string.Equals(httpReq.Verb, "OPTIONS", StringComparison.OrdinalIgnoreCase)) { httpRes.StatusCode = 200; - foreach(var (key, value) in GetDefaultCorsHeaders(httpReq)) + foreach (var (key, value) in GetDefaultCorsHeaders(httpReq)) { httpRes.Headers.Add(key, value); } @@ -486,7 +486,7 @@ namespace Emby.Server.Implementations.HttpServer var handler = GetServiceHandler(httpReq); if (handler != null) { - await handler.ProcessRequestAsync(this, httpReq, httpRes, _logger, cancellationToken).ConfigureAwait(false); + await handler.ProcessRequestAsync(this, httpReq, httpRes, cancellationToken).ConfigureAwait(false); } else { diff --git a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs index 318bc6a24..c9f802a51 100644 --- a/Emby.Server.Implementations/HttpServer/Security/AuthService.cs +++ b/Emby.Server.Implementations/HttpServer/Security/AuthService.cs @@ -13,26 +13,22 @@ using MediaBrowser.Controller.Security; using MediaBrowser.Controller.Session; using MediaBrowser.Model.Services; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.HttpServer.Security { public class AuthService : IAuthService { - private readonly ILogger<AuthService> _logger; private readonly IAuthorizationContext _authorizationContext; private readonly ISessionManager _sessionManager; private readonly IServerConfigurationManager _config; private readonly INetworkManager _networkManager; public AuthService( - ILogger<AuthService> logger, IAuthorizationContext authorizationContext, IServerConfigurationManager config, ISessionManager sessionManager, INetworkManager networkManager) { - _logger = logger; _authorizationContext = authorizationContext; _config = config; _sessionManager = sessionManager; diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index ef93779aa..fe74f1de7 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -21,6 +21,7 @@ namespace Emby.Server.Implementations.IO private readonly List<string> _affectedPaths = new List<string>(); private readonly object _timerLock = new object(); private Timer _timer; + private bool _disposed; public FileRefresher(string path, IServerConfigurationManager configurationManager, ILibraryManager libraryManager, ILogger logger) { @@ -213,11 +214,11 @@ namespace Emby.Server.Implementations.IO } } - private bool _disposed; public void Dispose() { _disposed = true; DisposeTimer(); + GC.SuppressFinalize(this); } } } diff --git a/Emby.Server.Implementations/Images/ArtistImageProvider.cs b/Emby.Server.Implementations/Images/ArtistImageProvider.cs index 52896720e..bf57382ed 100644 --- a/Emby.Server.Implementations/Images/ArtistImageProvider.cs +++ b/Emby.Server.Implementations/Images/ArtistImageProvider.cs @@ -11,7 +11,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; -using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; @@ -25,14 +24,9 @@ namespace Emby.Server.Implementations.Images /// </summary> public class ArtistImageProvider : BaseDynamicImageProvider<MusicArtist> { - /// <summary> - /// The library manager. - /// </summary> - private readonly ILibraryManager _libraryManager; - - public ArtistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor) + public ArtistImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor) + : base(fileSystem, providerManager, applicationPaths, imageProcessor) { - _libraryManager = libraryManager; } /// <summary> diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs index 6e6ef1359..e30a67593 100644 --- a/Emby.Server.Implementations/Library/IgnorePatterns.cs +++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs @@ -18,7 +18,21 @@ namespace Emby.Server.Implementations.Library { "**/small.jpg", "**/albumart.jpg", - "**/*sample*", + + // We have neither non-greedy matching or character group repetitions, working around that here. + // https://github.com/dazinator/DotNet.Glob#patterns + // .*/sample\..{1,5} + "**/sample.?", + "**/sample.??", + "**/sample.???", // Matches sample.mkv + "**/sample.????", // Matches sample.webm + "**/sample.?????", + "**/*.sample.?", + "**/*.sample.??", + "**/*.sample.???", + "**/*.sample.????", + "**/*.sample.?????", + "**/sample/*", // Directories "**/metadata/**", @@ -64,10 +78,13 @@ namespace Emby.Server.Implementations.Library "**/.grab/**", "**/.grab", - // Unix hidden files and directories - "**/.*/**", + // Unix hidden files "**/.*", + // Mac - if you ever remove the above. + // "**/._*", + // "**/.DS_Store", + // thumbs.db "**/thumbs.db", diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index bd6371ca4..04b530fce 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1,7 +1,6 @@ #pragma warning disable CS1591 using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Globalization; using System.IO; @@ -46,6 +45,7 @@ using MediaBrowser.Model.Net; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Tasks; using MediaBrowser.Providers.MediaInfo; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Episode = MediaBrowser.Controller.Entities.TV.Episode; using Genre = MediaBrowser.Controller.Entities.Genre; @@ -62,6 +62,7 @@ namespace Emby.Server.Implementations.Library private const string ShortcutFileExtension = ".mblink"; private readonly ILogger<LibraryManager> _logger; + private readonly IMemoryCache _memoryCache; private readonly ITaskManager _taskManager; private readonly IUserManager _userManager; private readonly IUserDataManager _userDataRepository; @@ -73,7 +74,6 @@ namespace Emby.Server.Implementations.Library private readonly IMediaEncoder _mediaEncoder; private readonly IFileSystem _fileSystem; private readonly IItemRepository _itemRepository; - private readonly ConcurrentDictionary<Guid, BaseItem> _libraryItemsCache; private readonly IImageProcessor _imageProcessor; /// <summary> @@ -111,6 +111,7 @@ namespace Emby.Server.Implementations.Library /// <param name="mediaEncoder">The media encoder.</param> /// <param name="itemRepository">The item repository.</param> /// <param name="imageProcessor">The image processor.</param> + /// <param name="memoryCache">The memory cache.</param> public LibraryManager( IServerApplicationHost appHost, ILogger<LibraryManager> logger, @@ -124,7 +125,8 @@ namespace Emby.Server.Implementations.Library Lazy<IUserViewManager> userviewManagerFactory, IMediaEncoder mediaEncoder, IItemRepository itemRepository, - IImageProcessor imageProcessor) + IImageProcessor imageProcessor, + IMemoryCache memoryCache) { _appHost = appHost; _logger = logger; @@ -139,8 +141,7 @@ namespace Emby.Server.Implementations.Library _mediaEncoder = mediaEncoder; _itemRepository = itemRepository; _imageProcessor = imageProcessor; - - _libraryItemsCache = new ConcurrentDictionary<Guid, BaseItem>(); + _memoryCache = memoryCache; _configurationManager.ConfigurationUpdated += ConfigurationUpdated; @@ -298,7 +299,7 @@ namespace Emby.Server.Implementations.Library } } - _libraryItemsCache.AddOrUpdate(item.Id, item, delegate { return item; }); + _memoryCache.CreateEntry(item.Id).SetValue(item); } public void DeleteItem(BaseItem item, DeleteOptions options) @@ -446,7 +447,7 @@ namespace Emby.Server.Implementations.Library _itemRepository.DeleteItem(child.Id); } - _libraryItemsCache.TryRemove(item.Id, out BaseItem removed); + _memoryCache.Remove(item.Id); ReportItemRemoved(item, parent); } @@ -516,7 +517,7 @@ namespace Emby.Server.Implementations.Library { // Try to normalize paths located underneath program-data in an attempt to make them more portable key = key.Substring(_configurationManager.ApplicationPaths.ProgramDataPath.Length) - .TrimStart(new[] { '/', '\\' }) + .TrimStart('/', '\\') .Replace('/', '\\'); } @@ -1247,7 +1248,7 @@ namespace Emby.Server.Implementations.Library throw new ArgumentException("Guid can't be empty", nameof(id)); } - if (_libraryItemsCache.TryGetValue(id, out BaseItem item)) + if (_memoryCache.TryGetValue(id, out BaseItem item)) { return item; } @@ -1590,7 +1591,6 @@ namespace Emby.Server.Implementations.Library public async Task<IEnumerable<Video>> GetIntros(BaseItem item, User user) { var tasks = IntroProviders - .OrderBy(i => i.GetType().Name.Contains("Default", StringComparison.OrdinalIgnoreCase) ? 1 : 0) .Take(1) .Select(i => GetIntros(i, item, user)); diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 4e1316abf..67cf8bf5b 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -46,8 +46,6 @@ namespace Emby.Server.Implementations.Library private readonly Dictionary<string, ILiveStream> _openStreams = new Dictionary<string, ILiveStream>(StringComparer.OrdinalIgnoreCase); private readonly SemaphoreSlim _liveStreamSemaphore = new SemaphoreSlim(1, 1); - private readonly object _disposeLock = new object(); - private IMediaSourceProvider[] _providers; public MediaSourceManager( @@ -623,12 +621,14 @@ namespace Emby.Server.Implementations.Library if (liveStreamInfo is IDirectStreamProvider) { - var info = await _mediaEncoder.GetMediaInfo(new MediaInfoRequest - { - MediaSource = mediaSource, - ExtractChapters = false, - MediaType = DlnaProfileType.Video - }, cancellationToken).ConfigureAwait(false); + var info = await _mediaEncoder.GetMediaInfo( + new MediaInfoRequest + { + MediaSource = mediaSource, + ExtractChapters = false, + MediaType = DlnaProfileType.Video + }, + cancellationToken).ConfigureAwait(false); mediaSource.MediaStreams = info.MediaStreams; mediaSource.Container = info.Container; @@ -859,21 +859,21 @@ namespace Emby.Server.Implementations.Library } } - private Tuple<IMediaSourceProvider, string> GetProvider(string key) + private (IMediaSourceProvider, string) GetProvider(string key) { if (string.IsNullOrEmpty(key)) { - throw new ArgumentException("key"); + throw new ArgumentException("Key can't be empty.", nameof(key)); } var keys = key.Split(new[] { LiveStreamIdDelimeter }, 2); var provider = _providers.FirstOrDefault(i => string.Equals(i.GetType().FullName.GetMD5().ToString("N", CultureInfo.InvariantCulture), keys[0], StringComparison.OrdinalIgnoreCase)); - var splitIndex = key.IndexOf(LiveStreamIdDelimeter); + var splitIndex = key.IndexOf(LiveStreamIdDelimeter, StringComparison.Ordinal); var keyId = key.Substring(splitIndex + 1); - return new Tuple<IMediaSourceProvider, string>(provider, keyId); + return (provider, keyId); } /// <summary> @@ -893,15 +893,12 @@ namespace Emby.Server.Implementations.Library { if (dispose) { - lock (_disposeLock) + foreach (var key in _openStreams.Keys.ToList()) { - foreach (var key in _openStreams.Keys.ToList()) - { - var task = CloseLiveStream(key); - - Task.WaitAll(task); - } + CloseLiveStream(key).GetAwaiter().GetResult(); } + + _liveStreamSemaphore.Dispose(); } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index c8e41001a..3332e1806 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -1,6 +1,5 @@ using System.Globalization; using Emby.Naming.TV; -using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Globalization; @@ -13,7 +12,6 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// </summary> public class SeasonResolver : FolderResolver<Season> { - private readonly IServerConfigurationManager _config; private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; private readonly ILogger<SeasonResolver> _logger; @@ -21,17 +19,14 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// <summary> /// Initializes a new instance of the <see cref="SeasonResolver"/> class. /// </summary> - /// <param name="config">The config.</param> /// <param name="libraryManager">The library manager.</param> /// <param name="localization">The localization.</param> /// <param name="logger">The logger.</param> public SeasonResolver( - IServerConfigurationManager config, ILibraryManager libraryManager, ILocalizationManager localization, ILogger<SeasonResolver> logger) { - _config = config; _libraryManager = libraryManager; _localization = localization; _logger = logger; diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs index 175b3af57..f9e5e6bbc 100644 --- a/Emby.Server.Implementations/Library/UserDataManager.cs +++ b/Emby.Server.Implementations/Library/UserDataManager.cs @@ -13,7 +13,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; -using Microsoft.Extensions.Logging; using Book = MediaBrowser.Controller.Entities.Book; namespace Emby.Server.Implementations.Library @@ -28,18 +27,15 @@ namespace Emby.Server.Implementations.Library private readonly ConcurrentDictionary<string, UserItemData> _userData = new ConcurrentDictionary<string, UserItemData>(StringComparer.OrdinalIgnoreCase); - private readonly ILogger<UserDataManager> _logger; private readonly IServerConfigurationManager _config; private readonly IUserManager _userManager; private readonly IUserDataRepository _repository; public UserDataManager( - ILogger<UserDataManager> logger, IServerConfigurationManager config, IUserManager userManager, IUserDataRepository repository) { - _logger = logger; _config = config; _userManager = userManager; _repository = repository; diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 3709f8fe4..77a7069eb 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -461,7 +461,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings private async Task<List<ScheduleDirect.ShowImages>> GetImageForPrograms( ListingsProviderInfo info, List<string> programIds, - CancellationToken cancellationToken) + CancellationToken cancellationToken) { if (programIds.Count == 0) { @@ -474,7 +474,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings { var imageId = i.Substring(0, 10); - if (!imageIdString.Contains(imageId)) + if (!imageIdString.Contains(imageId, StringComparison.Ordinal)) { imageIdString += "\"" + imageId + "\","; } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 2e2488e6e..00420bd2a 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -195,7 +195,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun while (!sr.EndOfStream) { string line = StripXML(sr.ReadLine()); - if (line.Contains("Channel")) + if (line.Contains("Channel", StringComparison.Ordinal)) { LiveTvTunerStatus status; var index = line.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); @@ -226,6 +226,11 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun private static string StripXML(string source) { + if (string.IsNullOrEmpty(source)) + { + return string.Empty; + } + char[] buffer = new char[source.Length]; int bufferIndex = 0; bool inside = false; @@ -270,7 +275,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun for (int i = 0; i < model.TunerCount; ++i) { - var name = string.Format("Tuner {0}", i + 1); + var name = string.Format(CultureInfo.InvariantCulture, "Tuner {0}", i + 1); var currentChannel = "none"; // @todo Get current channel and map back to Station Id var isAvailable = await manager.CheckTunerAvailability(ipInfo, i, cancellationToken).ConfigureAwait(false); var status = isAvailable ? LiveTvTunerStatus.Available : LiveTvTunerStatus.LiveTv; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index 57c5b7500..d4a88e299 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -77,7 +77,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } } - public class HdHomerunManager : IDisposable + public sealed class HdHomerunManager : IDisposable { public const int HdHomeRunPort = 65001; @@ -105,6 +105,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun StopStreaming(socket).GetAwaiter().GetResult(); } } + + GC.SuppressFinalize(this); } public async Task<bool> CheckTunerAvailability(IPAddress remoteIp, int tuner, CancellationToken cancellationToken) @@ -162,7 +164,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } _activeTuner = i; - var lockKeyString = string.Format("{0:d}", lockKeyValue); + var lockKeyString = string.Format(CultureInfo.InvariantCulture, "{0:d}", lockKeyValue); var lockkeyMsg = CreateSetMessage(i, "lockkey", lockKeyString, null); await stream.WriteAsync(lockkeyMsg, 0, lockkeyMsg.Length, cancellationToken).ConfigureAwait(false); int receivedBytes = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false); @@ -173,8 +175,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun continue; } - var commandList = commands.GetCommands(); - foreach (var command in commandList) + foreach (var command in commands.GetCommands()) { var channelMsg = CreateSetMessage(i, command.Item1, command.Item2, lockKeyValue); await stream.WriteAsync(channelMsg, 0, channelMsg.Length, cancellationToken).ConfigureAwait(false); @@ -188,7 +189,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } } - var targetValue = string.Format("rtp://{0}:{1}", localIp, localPort); + var targetValue = string.Format(CultureInfo.InvariantCulture, "rtp://{0}:{1}", localIp, localPort); var targetMsg = CreateSetMessage(i, "target", targetValue, lockKeyValue); await stream.WriteAsync(targetMsg, 0, targetMsg.Length, cancellationToken).ConfigureAwait(false); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs index c798c0a85..875977219 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3uParser.cs @@ -158,15 +158,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts private string GetChannelNumber(string extInf, Dictionary<string, string> attributes, string mediaUrl) { var nameParts = extInf.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); - var nameInExtInf = nameParts.Length > 1 ? nameParts[nameParts.Length - 1].Trim() : null; + var nameInExtInf = nameParts.Length > 1 ? nameParts[^1].AsSpan().Trim() : ReadOnlySpan<char>.Empty; string numberString = null; string attributeValue; - double doubleValue; if (attributes.TryGetValue("tvg-chno", out attributeValue)) { - if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue)) + if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) { numberString = attributeValue; } @@ -176,36 +175,36 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { if (attributes.TryGetValue("tvg-id", out attributeValue)) { - if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue)) + if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) { numberString = attributeValue; } else if (attributes.TryGetValue("channel-id", out attributeValue)) { - if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out doubleValue)) + if (double.TryParse(attributeValue, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) { numberString = attributeValue; } } } - if (String.IsNullOrWhiteSpace(numberString)) + if (string.IsNullOrWhiteSpace(numberString)) { // Using this as a fallback now as this leads to Problems with channels like "5 USA" // where 5 isnt ment to be the channel number // Check for channel number with the format from SatIp // #EXTINF:0,84. VOX Schweiz // #EXTINF:0,84.0 - VOX Schweiz - if (!string.IsNullOrWhiteSpace(nameInExtInf)) + if (!nameInExtInf.IsEmpty && !nameInExtInf.IsWhiteSpace()) { var numberIndex = nameInExtInf.IndexOf(' '); if (numberIndex > 0) { - var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' }); + var numberPart = nameInExtInf.Slice(0, numberIndex).Trim(new[] { ' ', '.' }); - if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out var number)) + if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) { - numberString = numberPart; + numberString = numberPart.ToString(); } } } @@ -231,7 +230,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { try { - numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/').Last()); + numberString = Path.GetFileNameWithoutExtension(mediaUrl.Split('/')[^1]); if (!IsValidChannelNumber(numberString)) { @@ -258,7 +257,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return false; } - if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out var value)) + if (!double.TryParse(numberString, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) { return false; } @@ -281,7 +280,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { var numberPart = nameInExtInf.Substring(0, numberIndex).Trim(new[] { ' ', '.' }); - if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out var number)) + if (double.TryParse(numberPart, NumberStyles.Any, CultureInfo.InvariantCulture, out _)) { // channel.Number = number.ToString(); nameInExtInf = nameInExtInf.Substring(numberIndex + 1).Trim(new[] { ' ', '-' }); diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index a6e9779c9..eec880208 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -101,8 +101,8 @@ "TaskCleanTranscode": "Lösche Transkodier Pfad", "TaskUpdatePluginsDescription": "Lädt Updates für Plugins herunter, welche dazu eingestellt sind automatisch zu updaten und installiert sie.", "TaskUpdatePlugins": "Update Plugins", - "TaskRefreshPeopleDescription": "Erneuert Metadaten für Schausteller und Regisseure in deinen Bibliotheken.", - "TaskRefreshPeople": "Erneuere Schausteller", + "TaskRefreshPeopleDescription": "Erneuert Metadaten für Schauspieler und Regisseure in deinen Bibliotheken.", + "TaskRefreshPeople": "Erneuere Schauspieler", "TaskCleanLogsDescription": "Lösche Log Dateien die älter als {0} Tage sind.", "TaskCleanLogs": "Lösche Log Pfad", "TaskRefreshLibraryDescription": "Scanne alle Bibliotheken für hinzugefügte Datein und erneuere Metadaten.", diff --git a/Emby.Server.Implementations/Localization/Core/zh-TW.json b/Emby.Server.Implementations/Localization/Core/zh-TW.json index a22f66df9..a21cdad95 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-TW.json +++ b/Emby.Server.Implementations/Localization/Core/zh-TW.json @@ -92,7 +92,7 @@ "HeaderRecordingGroups": "錄製組", "Inherit": "繼承", "SubtitleDownloadFailureFromForItem": "無法為 {1} 從 {0} 下載字幕", - "TaskDownloadMissingSubtitlesDescription": "在網路上透過描述資料搜尋遺失的字幕。", + "TaskDownloadMissingSubtitlesDescription": "在網路上透過中繼資料搜尋遺失的字幕。", "TaskDownloadMissingSubtitles": "下載遺失的字幕", "TaskRefreshChannels": "重新整理頻道", "TaskUpdatePlugins": "更新插件", diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 62a23118f..90e2766b8 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -247,7 +247,7 @@ namespace Emby.Server.Implementations.Localization } // Try splitting by : to handle "Germany: FSK 18" - var index = rating.IndexOf(':'); + var index = rating.IndexOf(':', StringComparison.Ordinal); if (index != -1) { rating = rating.Substring(index).TrimStart(':').Trim(); @@ -312,12 +312,12 @@ namespace Emby.Server.Implementations.Localization throw new ArgumentNullException(nameof(culture)); } - const string prefix = "Core"; - var key = prefix + culture; + const string Prefix = "Core"; + var key = Prefix + culture; return _dictionaries.GetOrAdd( key, - f => GetDictionary(prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult()); + f => GetDictionary(Prefix, culture, DefaultCulture + ".json").GetAwaiter().GetResult()); } private async Task<Dictionary<string, string>> GetDictionary(string prefix, string culture, string baseFilename) diff --git a/Emby.Server.Implementations/Net/UdpSocket.cs b/Emby.Server.Implementations/Net/UdpSocket.cs index b51c03446..4e25768cf 100644 --- a/Emby.Server.Implementations/Net/UdpSocket.cs +++ b/Emby.Server.Implementations/Net/UdpSocket.cs @@ -15,13 +15,11 @@ namespace Emby.Server.Implementations.Net public sealed class UdpSocket : ISocket, IDisposable { private Socket _socket; - private int _localPort; + private readonly int _localPort; private bool _disposed = false; public Socket Socket => _socket; - public IPAddress LocalIPAddress { get; } - private readonly SocketAsyncEventArgs _receiveSocketAsyncEventArgs = new SocketAsyncEventArgs() { SocketFlags = SocketFlags.None @@ -51,18 +49,33 @@ namespace Emby.Server.Implementations.Net InitReceiveSocketAsyncEventArgs(); } + public UdpSocket(Socket socket, IPEndPoint endPoint) + { + if (socket == null) + { + throw new ArgumentNullException(nameof(socket)); + } + + _socket = socket; + _socket.Connect(endPoint); + + InitReceiveSocketAsyncEventArgs(); + } + + public IPAddress LocalIPAddress { get; } + private void InitReceiveSocketAsyncEventArgs() { var receiveBuffer = new byte[8192]; _receiveSocketAsyncEventArgs.SetBuffer(receiveBuffer, 0, receiveBuffer.Length); - _receiveSocketAsyncEventArgs.Completed += _receiveSocketAsyncEventArgs_Completed; + _receiveSocketAsyncEventArgs.Completed += OnReceiveSocketAsyncEventArgsCompleted; var sendBuffer = new byte[8192]; _sendSocketAsyncEventArgs.SetBuffer(sendBuffer, 0, sendBuffer.Length); - _sendSocketAsyncEventArgs.Completed += _sendSocketAsyncEventArgs_Completed; + _sendSocketAsyncEventArgs.Completed += OnSendSocketAsyncEventArgsCompleted; } - private void _receiveSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e) + private void OnReceiveSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e) { var tcs = _currentReceiveTaskCompletionSource; if (tcs != null) @@ -86,7 +99,7 @@ namespace Emby.Server.Implementations.Net } } - private void _sendSocketAsyncEventArgs_Completed(object sender, SocketAsyncEventArgs e) + private void OnSendSocketAsyncEventArgsCompleted(object sender, SocketAsyncEventArgs e) { var tcs = _currentSendTaskCompletionSource; if (tcs != null) @@ -104,19 +117,6 @@ namespace Emby.Server.Implementations.Net } } - public UdpSocket(Socket socket, IPEndPoint endPoint) - { - if (socket == null) - { - throw new ArgumentNullException(nameof(socket)); - } - - _socket = socket; - _socket.Connect(endPoint); - - InitReceiveSocketAsyncEventArgs(); - } - public IAsyncResult BeginReceive(byte[] buffer, int offset, int count, AsyncCallback callback) { ThrowIfDisposed(); @@ -247,6 +247,7 @@ namespace Emby.Server.Implementations.Net } } + /// <inheritdoc /> public void Dispose() { if (_disposed) @@ -255,6 +256,8 @@ namespace Emby.Server.Implementations.Net } _socket?.Dispose(); + _receiveSocketAsyncEventArgs.Dispose(); + _sendSocketAsyncEventArgs.Dispose(); _currentReceiveTaskCompletionSource?.TrySetCanceled(); _currentSendTaskCompletionSource?.TrySetCanceled(); diff --git a/Emby.Server.Implementations/Networking/NetworkManager.cs b/Emby.Server.Implementations/Networking/NetworkManager.cs index 50cb44b28..089ec30e6 100644 --- a/Emby.Server.Implementations/Networking/NetworkManager.cs +++ b/Emby.Server.Implementations/Networking/NetworkManager.cs @@ -165,7 +165,7 @@ namespace Emby.Server.Implementations.Networking (octet[0] == 127) || // RFC1122 (octet[0] == 169 && octet[1] == 254)) // RFC3927 { - return false; + return true; } if (checkSubnets && IsInPrivateAddressSpaceAndLocalSubnet(endpoint)) @@ -390,7 +390,7 @@ namespace Emby.Server.Implementations.Networking var host = uri.DnsSafeHost; _logger.LogDebug("Resolving host {0}", host); - address = GetIpAddresses(host).Result.FirstOrDefault(); + address = GetIpAddresses(host).GetAwaiter().GetResult().FirstOrDefault(); if (address != null) { diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 5dd1af4b8..38ceadedb 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -349,16 +349,14 @@ namespace Emby.Server.Implementations.Playlists AlbumTitle = child.Album }; - var hasAlbumArtist = child as IHasAlbumArtist; - if (hasAlbumArtist != null) + if (child is IHasAlbumArtist hasAlbumArtist) { - entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault(); + entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null; } - var hasArtist = child as IHasArtist; - if (hasArtist != null) + if (child is IHasArtist hasArtist) { - entry.TrackArtist = hasArtist.Artists.FirstOrDefault(); + entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null; } if (child.RunTimeTicks.HasValue) @@ -385,16 +383,14 @@ namespace Emby.Server.Implementations.Playlists AlbumTitle = child.Album }; - var hasAlbumArtist = child as IHasAlbumArtist; - if (hasAlbumArtist != null) + if (child is IHasAlbumArtist hasAlbumArtist) { - entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault(); + entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null; } - var hasArtist = child as IHasArtist; - if (hasArtist != null) + if (child is IHasArtist hasArtist) { - entry.TrackArtist = hasArtist.Artists.FirstOrDefault(); + entry.TrackArtist = hasArtist.Artists.Count > 0 ? hasArtist.Artists[0] : null; } if (child.RunTimeTicks.HasValue) @@ -411,8 +407,10 @@ namespace Emby.Server.Implementations.Playlists if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase)) { - var playlist = new M3uPlaylist(); - playlist.IsExtended = true; + var playlist = new M3uPlaylist + { + IsExtended = true + }; foreach (var child in item.GetLinkedChildren()) { var entry = new M3uPlaylistEntry() @@ -422,10 +420,9 @@ namespace Emby.Server.Implementations.Playlists Album = child.Album }; - var hasAlbumArtist = child as IHasAlbumArtist; - if (hasAlbumArtist != null) + if (child is IHasAlbumArtist hasAlbumArtist) { - entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault(); + entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null; } if (child.RunTimeTicks.HasValue) @@ -453,10 +450,9 @@ namespace Emby.Server.Implementations.Playlists Album = child.Album }; - var hasAlbumArtist = child as IHasAlbumArtist; - if (hasAlbumArtist != null) + if (child is IHasAlbumArtist hasAlbumArtist) { - entry.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault(); + entry.AlbumArtist = hasAlbumArtist.AlbumArtists.Count > 0 ? hasAlbumArtist.AlbumArtists[0] : null; } if (child.RunTimeTicks.HasValue) @@ -514,7 +510,7 @@ namespace Emby.Server.Implementations.Playlists if (!folderPath.EndsWith(Path.DirectorySeparatorChar)) { - folderPath = folderPath + Path.DirectorySeparatorChar; + folderPath += Path.DirectorySeparatorChar; } var folderUri = new Uri(folderPath); @@ -537,32 +533,12 @@ namespace Emby.Server.Implementations.Playlists return relativePath; } - private static string UnEscape(string content) - { - if (content == null) - { - return content; - } - - return content.Replace("&", "&").Replace("'", "'").Replace(""", "\"").Replace(">", ">").Replace("<", "<"); - } - - private static string Escape(string content) - { - if (content == null) - { - return null; - } - - return content.Replace("&", "&").Replace("'", "'").Replace("\"", """).Replace(">", ">").Replace("<", "<"); - } - public Folder GetPlaylistsFolder(Guid userId) { - var typeName = "PlaylistsFolder"; + const string TypeName = "PlaylistsFolder"; - return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, typeName, StringComparison.Ordinal)) ?? - _libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, typeName, StringComparison.Ordinal)); + return _libraryManager.RootFolder.Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal)) ?? + _libraryManager.GetUserRootFolder().Children.OfType<Folder>().FirstOrDefault(i => string.Equals(i.GetType().Name, TypeName, StringComparison.Ordinal)); } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs index 3fe15ec68..81096026b 100644 --- a/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs +++ b/Emby.Server.Implementations/ScheduledTasks/TaskManager.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Threading.Tasks; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Events; -using MediaBrowser.Model.IO; using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Tasks; using Microsoft.Extensions.Logging; @@ -37,7 +36,6 @@ namespace Emby.Server.Implementations.ScheduledTasks private readonly IJsonSerializer _jsonSerializer; private readonly IApplicationPaths _applicationPaths; private readonly ILogger<TaskManager> _logger; - private readonly IFileSystem _fileSystem; /// <summary> /// Initializes a new instance of the <see cref="TaskManager" /> class. @@ -45,17 +43,14 @@ namespace Emby.Server.Implementations.ScheduledTasks /// <param name="applicationPaths">The application paths.</param> /// <param name="jsonSerializer">The json serializer.</param> /// <param name="logger">The logger.</param> - /// <param name="fileSystem">The filesystem manager.</param> public TaskManager( IApplicationPaths applicationPaths, IJsonSerializer jsonSerializer, - ILogger<TaskManager> logger, - IFileSystem fileSystem) + ILogger<TaskManager> logger) { _applicationPaths = applicationPaths; _jsonSerializer = jsonSerializer; _logger = logger; - _fileSystem = fileSystem; ScheduledTasks = Array.Empty<IScheduledTaskWorker>(); } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index 3854be703..8439f8a99 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -14,7 +14,6 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.Tasks; -using Microsoft.Extensions.Logging; using MediaBrowser.Model.Globalization; namespace Emby.Server.Implementations.ScheduledTasks @@ -25,11 +24,6 @@ namespace Emby.Server.Implementations.ScheduledTasks public class ChapterImagesTask : IScheduledTask { /// <summary> - /// The _logger. - /// </summary> - private readonly ILogger<ChapterImagesTask> _logger; - - /// <summary> /// The _library manager. /// </summary> private readonly ILibraryManager _libraryManager; @@ -46,7 +40,6 @@ namespace Emby.Server.Implementations.ScheduledTasks /// Initializes a new instance of the <see cref="ChapterImagesTask" /> class. /// </summary> public ChapterImagesTask( - ILoggerFactory loggerFactory, ILibraryManager libraryManager, IItemRepository itemRepo, IApplicationPaths appPaths, @@ -54,7 +47,6 @@ namespace Emby.Server.Implementations.ScheduledTasks IFileSystem fileSystem, ILocalizationManager localization) { - _logger = loggerFactory.CreateLogger<ChapterImagesTask>(); _libraryManager = libraryManager; _itemRepo = itemRepo; _appPaths = appPaths; diff --git a/Emby.Server.Implementations/Services/ServiceController.cs b/Emby.Server.Implementations/Services/ServiceController.cs index d884d4f37..47e7261e8 100644 --- a/Emby.Server.Implementations/Services/ServiceController.cs +++ b/Emby.Server.Implementations/Services/ServiceController.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Threading.Tasks; using Emby.Server.Implementations.HttpServer; using MediaBrowser.Model.Services; @@ -91,12 +92,22 @@ namespace Emby.Server.Implementations.Services { if (restPath.Path[0] != '/') { - throw new ArgumentException(string.Format("Route '{0}' on '{1}' must start with a '/'", restPath.Path, restPath.RequestType.GetMethodName())); + throw new ArgumentException( + string.Format( + CultureInfo.InvariantCulture, + "Route '{0}' on '{1}' must start with a '/'", + restPath.Path, + restPath.RequestType.GetMethodName())); } if (restPath.Path.IndexOfAny(InvalidRouteChars) != -1) { - throw new ArgumentException(string.Format("Route '{0}' on '{1}' contains invalid chars. ", restPath.Path, restPath.RequestType.GetMethodName())); + throw new ArgumentException( + string.Format( + CultureInfo.InvariantCulture, + "Route '{0}' on '{1}' contains invalid chars. ", + restPath.Path, + restPath.RequestType.GetMethodName())); } if (RestPathMap.TryGetValue(restPath.FirstMatchHashKey, out List<RestPath> pathsAtFirstMatch)) @@ -179,8 +190,7 @@ namespace Emby.Server.Implementations.Services var service = httpHost.CreateInstance(serviceType); - var serviceRequiresContext = service as IRequiresRequest; - if (serviceRequiresContext != null) + if (service is IRequiresRequest serviceRequiresContext) { serviceRequiresContext.Request = req; } diff --git a/Emby.Server.Implementations/Services/ServiceExec.cs b/Emby.Server.Implementations/Services/ServiceExec.cs index cbc4b754d..7b970627e 100644 --- a/Emby.Server.Implementations/Services/ServiceExec.cs +++ b/Emby.Server.Implementations/Services/ServiceExec.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -105,7 +106,13 @@ namespace Emby.Server.Implementations.Services } var expectedMethodName = actionName.Substring(0, 1) + actionName.Substring(1).ToLowerInvariant(); - throw new NotImplementedException(string.Format("Could not find method named {1}({0}) or Any({0}) on Service {2}", requestDto.GetType().GetMethodName(), expectedMethodName, serviceType.GetMethodName())); + throw new NotImplementedException( + string.Format( + CultureInfo.InvariantCulture, + "Could not find method named {1}({0}) or Any({0}) on Service {2}", + requestDto.GetType().GetMethodName(), + expectedMethodName, + serviceType.GetMethodName())); } private static async Task<object> GetTaskResult(Task task) diff --git a/Emby.Server.Implementations/Services/ServiceHandler.cs b/Emby.Server.Implementations/Services/ServiceHandler.cs index 3d4e1ca77..b4166f771 100644 --- a/Emby.Server.Implementations/Services/ServiceHandler.cs +++ b/Emby.Server.Implementations/Services/ServiceHandler.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.Net.Mime; using System.Reflection; using System.Threading; using System.Threading.Tasks; @@ -45,7 +46,7 @@ namespace Emby.Server.Implementations.Services var pos = pathInfo.LastIndexOf('.'); if (pos != -1) { - var format = pathInfo.Substring(pos + 1); + var format = pathInfo.AsSpan().Slice(pos + 1); contentType = GetFormatContentType(format); if (contentType != null) { @@ -56,18 +57,21 @@ namespace Emby.Server.Implementations.Services return pathInfo; } - private static string GetFormatContentType(string format) + private static string GetFormatContentType(ReadOnlySpan<char> format) { - // built-in formats - switch (format) + if (format.Equals("json", StringComparison.Ordinal)) { - case "json": return "application/json"; - case "xml": return "application/xml"; - default: return null; + return MediaTypeNames.Application.Json; } + else if (format.Equals("xml", StringComparison.Ordinal)) + { + return MediaTypeNames.Application.Xml; + } + + return null; } - public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, ILogger logger, CancellationToken cancellationToken) + public async Task ProcessRequestAsync(HttpListenerHost httpHost, IRequest httpReq, HttpResponse httpRes, CancellationToken cancellationToken) { httpReq.Items["__route"] = _restPath; @@ -76,10 +80,10 @@ namespace Emby.Server.Implementations.Services httpReq.ResponseContentType = _responseContentType; } - var request = await CreateRequest(httpHost, httpReq, _restPath, logger).ConfigureAwait(false); + var request = await CreateRequest(httpHost, httpReq, _restPath).ConfigureAwait(false); httpHost.ApplyRequestFilters(httpReq, httpRes, request); - + httpRes.HttpContext.SetServiceStackRequest(httpReq); var response = await httpHost.ServiceController.Execute(httpHost, request, httpReq).ConfigureAwait(false); @@ -92,7 +96,7 @@ namespace Emby.Server.Implementations.Services await ResponseHelper.WriteToResponse(httpRes, httpReq, response, cancellationToken).ConfigureAwait(false); } - public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath, ILogger logger) + public static async Task<object> CreateRequest(HttpListenerHost host, IRequest httpReq, RestPath restPath) { var requestType = restPath.RequestType; diff --git a/Emby.Server.Implementations/Services/ServicePath.cs b/Emby.Server.Implementations/Services/ServicePath.cs index bdda7c089..442b2ab1c 100644 --- a/Emby.Server.Implementations/Services/ServicePath.cs +++ b/Emby.Server.Implementations/Services/ServicePath.cs @@ -156,7 +156,7 @@ namespace Emby.Server.Implementations.Services { var component = components[i]; - if (component.StartsWith(VariablePrefix)) + if (component.StartsWith(VariablePrefix, StringComparison.Ordinal)) { var variableName = component.Substring(1, component.Length - 2); if (variableName[variableName.Length - 1] == WildCardChar) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index ca9f95c70..862a7296c 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -848,8 +848,8 @@ namespace Emby.Server.Implementations.Session /// </summary> /// <param name="info">The info.</param> /// <returns>Task.</returns> - /// <exception cref="ArgumentNullException">info</exception> - /// <exception cref="ArgumentOutOfRangeException">positionTicks</exception> + /// <exception cref="ArgumentNullException"><c>info</c> is <c>null</c>.</exception> + /// <exception cref="ArgumentOutOfRangeException"><c>info.PositionTicks</c> is <c>null</c> or negative.</exception> public async Task OnPlaybackStopped(PlaybackStopInfo info) { CheckDisposed(); diff --git a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs index b9db6ecd0..8bebd37dc 100644 --- a/Emby.Server.Implementations/Session/SessionWebSocketListener.cs +++ b/Emby.Server.Implementations/Session/SessionWebSocketListener.cs @@ -93,7 +93,7 @@ namespace Emby.Server.Implementations.Session if (session != null) { EnsureController(session, e.Argument); - await KeepAliveWebSocket(e.Argument); + await KeepAliveWebSocket(e.Argument).ConfigureAwait(false); } else { @@ -177,7 +177,7 @@ namespace Emby.Server.Implementations.Session // Notify WebSocket about timeout try { - await SendForceKeepAlive(webSocket); + await SendForceKeepAlive(webSocket).ConfigureAwait(false); } catch (WebSocketException exception) { @@ -233,6 +233,7 @@ namespace Emby.Server.Implementations.Session if (_keepAliveCancellationToken != null) { _keepAliveCancellationToken.Cancel(); + _keepAliveCancellationToken.Dispose(); _keepAliveCancellationToken = null; } } @@ -268,7 +269,7 @@ namespace Emby.Server.Implementations.Session lost = _webSockets.Where(i => (DateTime.UtcNow - i.LastKeepAliveDate).TotalSeconds >= WebSocketLostTimeout).ToList(); } - if (inactive.Any()) + if (inactive.Count > 0) { _logger.LogInformation("Sending ForceKeepAlive message to {0} inactive WebSockets.", inactive.Count); } @@ -277,7 +278,7 @@ namespace Emby.Server.Implementations.Session { try { - await SendForceKeepAlive(webSocket); + await SendForceKeepAlive(webSocket).ConfigureAwait(false); } catch (WebSocketException exception) { @@ -288,7 +289,7 @@ namespace Emby.Server.Implementations.Session lock (_webSocketsLock) { - if (lost.Any()) + if (lost.Count > 0) { _logger.LogInformation("Lost {0} WebSockets.", lost.Count); foreach (var webSocket in lost) @@ -298,7 +299,7 @@ namespace Emby.Server.Implementations.Session } } - if (!_webSockets.Any()) + if (_webSockets.Count == 0) { StopKeepAlive(); } @@ -312,11 +313,13 @@ namespace Emby.Server.Implementations.Session /// <returns>Task.</returns> private Task SendForceKeepAlive(IWebSocketConnection webSocket) { - return webSocket.SendAsync(new WebSocketMessage<int> - { - MessageType = "ForceKeepAlive", - Data = WebSocketLostTimeout - }, CancellationToken.None); + return webSocket.SendAsync( + new WebSocketMessage<int> + { + MessageType = "ForceKeepAlive", + Data = WebSocketLostTimeout + }, + CancellationToken.None); } /// <summary> @@ -330,12 +333,11 @@ namespace Emby.Server.Implementations.Session { while (!cancellationToken.IsCancellationRequested) { - await callback(); - Task task = Task.Delay(interval, cancellationToken); + await callback().ConfigureAwait(false); try { - await task; + await Task.Delay(interval, cancellationToken).ConfigureAwait(false); } catch (TaskCanceledException) { diff --git a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs index 2b7d818be..1f68a9c81 100644 --- a/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs +++ b/Emby.Server.Implementations/Sorting/AiredEpisodeOrderComparer.cs @@ -154,8 +154,8 @@ namespace Emby.Server.Implementations.Sorting private static int CompareEpisodes(Episode x, Episode y) { - var xValue = (x.ParentIndexNumber ?? -1) * 1000 + (x.IndexNumber ?? -1); - var yValue = (y.ParentIndexNumber ?? -1) * 1000 + (y.IndexNumber ?? -1); + var xValue = ((x.ParentIndexNumber ?? -1) * 1000) + (x.IndexNumber ?? -1); + var yValue = ((y.ParentIndexNumber ?? -1) * 1000) + (y.IndexNumber ?? -1); return xValue.CompareTo(yValue); } diff --git a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs index 39d17833f..80b977731 100644 --- a/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs +++ b/Emby.Server.Implementations/SyncPlay/SyncPlayController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -27,14 +28,17 @@ namespace Emby.Server.Implementations.SyncPlay /// All sessions will receive the message. /// </summary> AllGroup = 0, + /// <summary> /// Only the specified session will receive the message. /// </summary> CurrentSession = 1, + /// <summary> /// All sessions, except the current one, will receive the message. /// </summary> AllExceptCurrentSession = 2, + /// <summary> /// Only sessions that are not buffering will receive the message. /// </summary> @@ -56,15 +60,6 @@ namespace Emby.Server.Implementations.SyncPlay /// </summary> private readonly GroupInfo _group = new GroupInfo(); - /// <inheritdoc /> - public Guid GetGroupId() => _group.GroupId; - - /// <inheritdoc /> - public Guid GetPlayingItemId() => _group.PlayingItem.Id; - - /// <inheritdoc /> - public bool IsGroupEmpty() => _group.IsEmpty(); - /// <summary> /// Initializes a new instance of the <see cref="SyncPlayController" /> class. /// </summary> @@ -78,6 +73,15 @@ namespace Emby.Server.Implementations.SyncPlay _syncPlayManager = syncPlayManager; } + /// <inheritdoc /> + public Guid GetGroupId() => _group.GroupId; + + /// <inheritdoc /> + public Guid GetPlayingItemId() => _group.PlayingItem.Id; + + /// <inheritdoc /> + public bool IsGroupEmpty() => _group.IsEmpty(); + /// <summary> /// Converts DateTime to UTC string. /// </summary> @@ -85,7 +89,7 @@ namespace Emby.Server.Implementations.SyncPlay /// <value>The UTC string.</value> private string DateToUTCString(DateTime date) { - return date.ToUniversalTime().ToString("o"); + return date.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture); } /// <summary> @@ -94,23 +98,23 @@ namespace Emby.Server.Implementations.SyncPlay /// <param name="from">The current session.</param> /// <param name="type">The filtering type.</param> /// <value>The array of sessions matching the filter.</value> - private SessionInfo[] FilterSessions(SessionInfo from, BroadcastType type) + private IEnumerable<SessionInfo> FilterSessions(SessionInfo from, BroadcastType type) { switch (type) { case BroadcastType.CurrentSession: return new SessionInfo[] { from }; case BroadcastType.AllGroup: - return _group.Participants.Values.Select( - session => session.Session).ToArray(); + return _group.Participants.Values + .Select(session => session.Session); case BroadcastType.AllExceptCurrentSession: - return _group.Participants.Values.Select( - session => session.Session).Where( - session => !session.Id.Equals(from.Id)).ToArray(); + return _group.Participants.Values + .Select(session => session.Session) + .Where(session => !session.Id.Equals(from.Id, StringComparison.Ordinal)); case BroadcastType.AllReady: - return _group.Participants.Values.Where( - session => !session.IsBuffering).Select( - session => session.Session).ToArray(); + return _group.Participants.Values + .Where(session => !session.IsBuffering) + .Select(session => session.Session); default: return Array.Empty<SessionInfo>(); } @@ -128,10 +132,9 @@ namespace Emby.Server.Implementations.SyncPlay { IEnumerable<Task> GetTasks() { - SessionInfo[] sessions = FilterSessions(from, type); - foreach (var session in sessions) + foreach (var session in FilterSessions(from, type)) { - yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id.ToString(), message, cancellationToken); + yield return _sessionManager.SendSyncPlayGroupUpdate(session.Id, message, cancellationToken); } } @@ -150,10 +153,9 @@ namespace Emby.Server.Implementations.SyncPlay { IEnumerable<Task> GetTasks() { - SessionInfo[] sessions = FilterSessions(from, type); - foreach (var session in sessions) + foreach (var session in FilterSessions(from, type)) { - yield return _sessionManager.SendSyncPlayCommand(session.Id.ToString(), message, cancellationToken); + yield return _sessionManager.SendSyncPlayCommand(session.Id, message, cancellationToken); } } @@ -236,9 +238,11 @@ namespace Emby.Server.Implementations.SyncPlay } else { - var playRequest = new PlayRequest(); - playRequest.ItemIds = new Guid[] { _group.PlayingItem.Id }; - playRequest.StartPositionTicks = _group.PositionTicks; + var playRequest = new PlayRequest + { + ItemIds = new Guid[] { _group.PlayingItem.Id }, + StartPositionTicks = _group.PositionTicks + }; var update = NewSyncPlayGroupUpdate(GroupUpdateType.PrepareSession, playRequest); SendGroupUpdate(session, BroadcastType.CurrentSession, update, cancellationToken); } diff --git a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj index 6db514b2e..65a439459 100644 --- a/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj +++ b/Jellyfin.Drawing.Skia/Jellyfin.Drawing.Skia.csproj @@ -18,8 +18,8 @@ </ItemGroup> <ItemGroup> - <PackageReference Include="BlurHashSharp" Version="1.0.1" /> - <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.0.0" /> + <PackageReference Include="BlurHashSharp" Version="1.1.0" /> + <PackageReference Include="BlurHashSharp.SkiaSharp" Version="1.1.0" /> <PackageReference Include="SkiaSharp" Version="1.68.3" /> <PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="1.68.3" /> <PackageReference Include="Jellyfin.SkiaSharp.NativeAssets.LinuxArm" Version="1.68.1" /> diff --git a/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs b/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs index f2df066ec..6136a2ff9 100644 --- a/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs +++ b/Jellyfin.Drawing.Skia/PercentPlayedDrawer.cs @@ -19,22 +19,18 @@ namespace Jellyfin.Drawing.Skia /// <param name="percent">The percentage played to display with the indicator.</param> public static void Process(SKCanvas canvas, ImageDimensions imageSize, double percent) { - using (var paint = new SKPaint()) - { - var endX = imageSize.Width - 1; - var endY = imageSize.Height - 1; + using var paint = new SKPaint(); + var endX = imageSize.Width - 1; + var endY = imageSize.Height - 1; - paint.Color = SKColor.Parse("#99000000"); - paint.Style = SKPaintStyle.Fill; - canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, (float)endX, (float)endY), paint); + paint.Color = SKColor.Parse("#99000000"); + paint.Style = SKPaintStyle.Fill; + canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, endX, endY), paint); - double foregroundWidth = endX; - foregroundWidth *= percent; - foregroundWidth /= 100; + double foregroundWidth = (endX * percent) / 100; - paint.Color = SKColor.Parse("#FF00A4DC"); - canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), (float)endY), paint); - } + paint.Color = SKColor.Parse("#FF00A4DC"); + canvas.DrawRect(SKRect.Create(0, (float)endY - IndicatorHeight, Convert.ToInt32(foregroundWidth), endY), paint); } } } diff --git a/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs b/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs index 7eed5f4f7..db4f78398 100644 --- a/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs +++ b/Jellyfin.Drawing.Skia/PlayedIndicatorDrawer.cs @@ -22,31 +22,27 @@ namespace Jellyfin.Drawing.Skia { var x = imageSize.Width - OffsetFromTopRightCorner; - using (var paint = new SKPaint()) + using var paint = new SKPaint { - paint.Color = SKColor.Parse("#CC00A4DC"); - paint.Style = SKPaintStyle.Fill; - canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint); - } + Color = SKColor.Parse("#CC00A4DC"), + Style = SKPaintStyle.Fill + }; - using (var paint = new SKPaint()) - { - paint.Color = new SKColor(255, 255, 255, 255); - paint.Style = SKPaintStyle.Fill; + canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint); - paint.TextSize = 30; - paint.IsAntialias = true; + paint.Color = new SKColor(255, 255, 255, 255); + paint.TextSize = 30; + paint.IsAntialias = true; - // or: - // var emojiChar = 0x1F680; - const string Text = "✔️"; - var emojiChar = StringUtilities.GetUnicodeCharacterCode(Text, SKTextEncoding.Utf32); + // or: + // var emojiChar = 0x1F680; + const string Text = "✔️"; + var emojiChar = StringUtilities.GetUnicodeCharacterCode(Text, SKTextEncoding.Utf32); - // ask the font manager for a font with that character - paint.Typeface = SKFontManager.Default.MatchCharacter(emojiChar); + // ask the font manager for a font with that character + paint.Typeface = SKFontManager.Default.MatchCharacter(emojiChar); - canvas.DrawText(Text, (float)x - 20, OffsetFromTopRightCorner + 12, paint); - } + canvas.DrawText(Text, (float)x - 20, OffsetFromTopRightCorner + 12, paint); } } } diff --git a/Jellyfin.Drawing.Skia/SkiaCodecException.cs b/Jellyfin.Drawing.Skia/SkiaCodecException.cs index 1d2db5515..9a50a4d62 100644 --- a/Jellyfin.Drawing.Skia/SkiaCodecException.cs +++ b/Jellyfin.Drawing.Skia/SkiaCodecException.cs @@ -12,7 +12,7 @@ namespace Jellyfin.Drawing.Skia /// Initializes a new instance of the <see cref="SkiaCodecException" /> class. /// </summary> /// <param name="result">The non-successful codec result returned by Skia.</param> - public SkiaCodecException(SKCodecResult result) : base() + public SkiaCodecException(SKCodecResult result) { CodecResult = result; } diff --git a/Jellyfin.Drawing.Skia/SkiaEncoder.cs b/Jellyfin.Drawing.Skia/SkiaEncoder.cs index ba9a5809f..1f7c43de8 100644 --- a/Jellyfin.Drawing.Skia/SkiaEncoder.cs +++ b/Jellyfin.Drawing.Skia/SkiaEncoder.cs @@ -29,9 +29,7 @@ namespace Jellyfin.Drawing.Skia /// </summary> /// <param name="logger">The application logger.</param> /// <param name="appPaths">The application paths.</param> - public SkiaEncoder( - ILogger<SkiaEncoder> logger, - IApplicationPaths appPaths) + public SkiaEncoder(ILogger<SkiaEncoder> logger, IApplicationPaths appPaths) { _logger = logger; _appPaths = appPaths; @@ -102,19 +100,14 @@ namespace Jellyfin.Drawing.Skia /// <returns>The converted format.</returns> public static SKEncodedImageFormat GetImageFormat(ImageFormat selectedFormat) { - switch (selectedFormat) - { - case ImageFormat.Bmp: - return SKEncodedImageFormat.Bmp; - case ImageFormat.Jpg: - return SKEncodedImageFormat.Jpeg; - case ImageFormat.Gif: - return SKEncodedImageFormat.Gif; - case ImageFormat.Webp: - return SKEncodedImageFormat.Webp; - default: - return SKEncodedImageFormat.Png; - } + return selectedFormat switch + { + ImageFormat.Bmp => SKEncodedImageFormat.Bmp, + ImageFormat.Jpg => SKEncodedImageFormat.Jpeg, + ImageFormat.Gif => SKEncodedImageFormat.Gif, + ImageFormat.Webp => SKEncodedImageFormat.Webp, + _ => SKEncodedImageFormat.Png + }; } private static bool IsTransparentRow(SKBitmap bmp, int row) @@ -146,63 +139,34 @@ namespace Jellyfin.Drawing.Skia private SKBitmap CropWhiteSpace(SKBitmap bitmap) { var topmost = 0; - for (int row = 0; row < bitmap.Height; ++row) + while (topmost < bitmap.Height && IsTransparentRow(bitmap, topmost)) { - if (IsTransparentRow(bitmap, row)) - { - topmost = row + 1; - } - else - { - break; - } + topmost++; } int bottommost = bitmap.Height; - for (int row = bitmap.Height - 1; row >= 0; --row) + while (bottommost >= 0 && IsTransparentRow(bitmap, bottommost - 1)) { - if (IsTransparentRow(bitmap, row)) - { - bottommost = row; - } - else - { - break; - } + bottommost--; } - int leftmost = 0, rightmost = bitmap.Width; - for (int col = 0; col < bitmap.Width; ++col) + var leftmost = 0; + while (leftmost < bitmap.Width && IsTransparentColumn(bitmap, leftmost)) { - if (IsTransparentColumn(bitmap, col)) - { - leftmost = col + 1; - } - else - { - break; - } + leftmost++; } - for (int col = bitmap.Width - 1; col >= 0; --col) + var rightmost = bitmap.Width; + while (rightmost >= 0 && IsTransparentColumn(bitmap, rightmost - 1)) { - if (IsTransparentColumn(bitmap, col)) - { - rightmost = col; - } - else - { - break; - } + rightmost--; } var newRect = SKRectI.Create(leftmost, topmost, rightmost - leftmost, bottommost - topmost); - using (var image = SKImage.FromBitmap(bitmap)) - using (var subset = image.Subset(newRect)) - { - return SKBitmap.FromImage(subset); - } + using var image = SKImage.FromBitmap(bitmap); + using var subset = image.Subset(newRect); + return SKBitmap.FromImage(subset); } /// <inheritdoc /> @@ -216,14 +180,12 @@ namespace Jellyfin.Drawing.Skia throw new FileNotFoundException("File not found", path); } - using (var codec = SKCodec.Create(path, out SKCodecResult result)) - { - EnsureSuccess(result); + using var codec = SKCodec.Create(path, out SKCodecResult result); + EnsureSuccess(result); - var info = codec.Info; + var info = codec.Info; - return new ImageDimensions(info.Width, info.Height); - } + return new ImageDimensions(info.Width, info.Height); } /// <inheritdoc /> @@ -237,7 +199,8 @@ namespace Jellyfin.Drawing.Skia throw new ArgumentNullException(nameof(path)); } - return BlurHashEncoder.Encode(xComp, yComp, path); + // Any larger than 128x128 is too slow and there's no visually discernible difference + return BlurHashEncoder.Encode(xComp, yComp, path, 128, 128); } private static bool HasDiacritics(string text) @@ -253,12 +216,7 @@ namespace Jellyfin.Drawing.Skia } } - if (HasDiacritics(path)) - { - return true; - } - - return false; + return HasDiacritics(path); } private string NormalizePath(string path) @@ -283,25 +241,17 @@ namespace Jellyfin.Drawing.Skia return SKEncodedOrigin.TopLeft; } - switch (orientation.Value) - { - case ImageOrientation.TopRight: - return SKEncodedOrigin.TopRight; - case ImageOrientation.RightTop: - return SKEncodedOrigin.RightTop; - case ImageOrientation.RightBottom: - return SKEncodedOrigin.RightBottom; - case ImageOrientation.LeftTop: - return SKEncodedOrigin.LeftTop; - case ImageOrientation.LeftBottom: - return SKEncodedOrigin.LeftBottom; - case ImageOrientation.BottomRight: - return SKEncodedOrigin.BottomRight; - case ImageOrientation.BottomLeft: - return SKEncodedOrigin.BottomLeft; - default: - return SKEncodedOrigin.TopLeft; - } + return orientation.Value switch + { + ImageOrientation.TopRight => SKEncodedOrigin.TopRight, + ImageOrientation.RightTop => SKEncodedOrigin.RightTop, + ImageOrientation.RightBottom => SKEncodedOrigin.RightBottom, + ImageOrientation.LeftTop => SKEncodedOrigin.LeftTop, + ImageOrientation.LeftBottom => SKEncodedOrigin.LeftBottom, + ImageOrientation.BottomRight => SKEncodedOrigin.BottomRight, + ImageOrientation.BottomLeft => SKEncodedOrigin.BottomLeft, + _ => SKEncodedOrigin.TopLeft + }; } /// <summary> @@ -323,24 +273,22 @@ namespace Jellyfin.Drawing.Skia if (requiresTransparencyHack || forceCleanBitmap) { - using (var codec = SKCodec.Create(NormalizePath(path))) + using var codec = SKCodec.Create(NormalizePath(path)); + if (codec == null) { - if (codec == null) - { - origin = GetSKEncodedOrigin(orientation); - return null; - } + origin = GetSKEncodedOrigin(orientation); + return null; + } - // create the bitmap - var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack); + // create the bitmap + var bitmap = new SKBitmap(codec.Info.Width, codec.Info.Height, !requiresTransparencyHack); - // decode - _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels()); + // decode + _ = codec.GetPixels(bitmap.Info, bitmap.GetPixels()); - origin = codec.EncodedOrigin; + origin = codec.EncodedOrigin; - return bitmap; - } + return bitmap; } var resultBitmap = SKBitmap.Decode(NormalizePath(path)); @@ -367,15 +315,8 @@ namespace Jellyfin.Drawing.Skia { if (cropWhitespace) { - using (var bitmap = Decode(path, forceAnalyzeBitmap, orientation, out origin)) - { - if (bitmap == null) - { - return null; - } - - return CropWhiteSpace(bitmap); - } + using var bitmap = Decode(path, forceAnalyzeBitmap, orientation, out origin); + return bitmap == null ? null : CropWhiteSpace(bitmap); } return Decode(path, forceAnalyzeBitmap, orientation, out origin); @@ -403,133 +344,55 @@ namespace Jellyfin.Drawing.Skia private SKBitmap OrientImage(SKBitmap bitmap, SKEncodedOrigin origin) { + if (origin == SKEncodedOrigin.Default) + { + return bitmap; + } + + var needsFlip = origin == SKEncodedOrigin.LeftBottom + || origin == SKEncodedOrigin.LeftTop + || origin == SKEncodedOrigin.RightBottom + || origin == SKEncodedOrigin.RightTop; + var rotated = needsFlip + ? new SKBitmap(bitmap.Height, bitmap.Width) + : new SKBitmap(bitmap.Width, bitmap.Height); + using var surface = new SKCanvas(rotated); + var midX = (float)rotated.Width / 2; + var midY = (float)rotated.Height / 2; + switch (origin) { case SKEncodedOrigin.TopRight: - { - var rotated = new SKBitmap(bitmap.Width, bitmap.Height); - using (var surface = new SKCanvas(rotated)) - { - surface.Translate(rotated.Width, 0); - surface.Scale(-1, 1); - surface.DrawBitmap(bitmap, 0, 0); - } - - return rotated; - } - + surface.Scale(-1, 1, midX, midY); + break; case SKEncodedOrigin.BottomRight: - { - var rotated = new SKBitmap(bitmap.Width, bitmap.Height); - using (var surface = new SKCanvas(rotated)) - { - float px = (float)bitmap.Width / 2; - float py = (float)bitmap.Height / 2; - - surface.RotateDegrees(180, px, py); - surface.DrawBitmap(bitmap, 0, 0); - } - - return rotated; - } - + surface.RotateDegrees(180, midX, midY); + break; case SKEncodedOrigin.BottomLeft: - { - var rotated = new SKBitmap(bitmap.Width, bitmap.Height); - using (var surface = new SKCanvas(rotated)) - { - float px = (float)bitmap.Width / 2; - - float py = (float)bitmap.Height / 2; - - surface.Translate(rotated.Width, 0); - surface.Scale(-1, 1); - - surface.RotateDegrees(180, px, py); - surface.DrawBitmap(bitmap, 0, 0); - } - - return rotated; - } - + surface.Scale(1, -1, midX, midY); + break; case SKEncodedOrigin.LeftTop: - { - // TODO: Remove dual canvases, had trouble with flipping - using (var rotated = new SKBitmap(bitmap.Height, bitmap.Width)) - { - using (var surface = new SKCanvas(rotated)) - { - surface.Translate(rotated.Width, 0); - - surface.RotateDegrees(90); - - surface.DrawBitmap(bitmap, 0, 0); - } - - var flippedBitmap = new SKBitmap(rotated.Width, rotated.Height); - using (var flippedCanvas = new SKCanvas(flippedBitmap)) - { - flippedCanvas.Translate(flippedBitmap.Width, 0); - flippedCanvas.Scale(-1, 1); - flippedCanvas.DrawBitmap(rotated, 0, 0); - } - - return flippedBitmap; - } - } - + surface.Translate(0, -rotated.Height); + surface.Scale(1, -1, midX, midY); + surface.RotateDegrees(-90); + break; case SKEncodedOrigin.RightTop: - { - var rotated = new SKBitmap(bitmap.Height, bitmap.Width); - using (var surface = new SKCanvas(rotated)) - { - surface.Translate(rotated.Width, 0); - surface.RotateDegrees(90); - surface.DrawBitmap(bitmap, 0, 0); - } - - return rotated; - } - + surface.Translate(rotated.Width, 0); + surface.RotateDegrees(90); + break; case SKEncodedOrigin.RightBottom: - { - // TODO: Remove dual canvases, had trouble with flipping - using (var rotated = new SKBitmap(bitmap.Height, bitmap.Width)) - { - using (var surface = new SKCanvas(rotated)) - { - surface.Translate(0, rotated.Height); - surface.RotateDegrees(270); - surface.DrawBitmap(bitmap, 0, 0); - } - - var flippedBitmap = new SKBitmap(rotated.Width, rotated.Height); - using (var flippedCanvas = new SKCanvas(flippedBitmap)) - { - flippedCanvas.Translate(flippedBitmap.Width, 0); - flippedCanvas.Scale(-1, 1); - flippedCanvas.DrawBitmap(rotated, 0, 0); - } - - return flippedBitmap; - } - } - + surface.Translate(rotated.Width, 0); + surface.Scale(1, -1, midX, midY); + surface.RotateDegrees(90); + break; case SKEncodedOrigin.LeftBottom: - { - var rotated = new SKBitmap(bitmap.Height, bitmap.Width); - using (var surface = new SKCanvas(rotated)) - { - surface.Translate(0, rotated.Height); - surface.RotateDegrees(270); - surface.DrawBitmap(bitmap, 0, 0); - } - - return rotated; - } - - default: return bitmap; + surface.Translate(0, rotated.Height); + surface.RotateDegrees(-90); + break; } + + surface.DrawBitmap(bitmap, 0, 0); + return rotated; } /// <inheritdoc/> @@ -552,97 +415,87 @@ namespace Jellyfin.Drawing.Skia var blur = options.Blur ?? 0; var hasIndicator = options.AddPlayedIndicator || options.UnplayedCount.HasValue || !options.PercentPlayed.Equals(0); - using (var bitmap = GetBitmap(inputPath, options.CropWhiteSpace, autoOrient, orientation)) + using var bitmap = GetBitmap(inputPath, options.CropWhiteSpace, autoOrient, orientation); + if (bitmap == null) { - if (bitmap == null) - { - throw new InvalidDataException($"Skia unable to read image {inputPath}"); - } + throw new InvalidDataException($"Skia unable to read image {inputPath}"); + } - var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height); + var originalImageSize = new ImageDimensions(bitmap.Width, bitmap.Height); - if (!options.CropWhiteSpace - && options.HasDefaultOptions(inputPath, originalImageSize) - && !autoOrient) - { - // Just spit out the original file if all the options are default - return inputPath; - } + if (!options.CropWhiteSpace + && options.HasDefaultOptions(inputPath, originalImageSize) + && !autoOrient) + { + // Just spit out the original file if all the options are default + return inputPath; + } - var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize); + var newImageSize = ImageHelper.GetNewImageSize(options, originalImageSize); - var width = newImageSize.Width; - var height = newImageSize.Height; + var width = newImageSize.Width; + var height = newImageSize.Height; + + using var resizedBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType); + // scale image + bitmap.ScalePixels(resizedBitmap, SKFilterQuality.High); + + // If all we're doing is resizing then we can stop now + if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator) + { + Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); + using var outputStream = new SKFileWStream(outputPath); + using var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels()); + pixmap.Encode(outputStream, skiaOutputFormat, quality); + return outputPath; + } - using (var resizedBitmap = new SKBitmap(width, height, bitmap.ColorType, bitmap.AlphaType)) + // create bitmap to use for canvas drawing used to draw into bitmap + using var saveBitmap = new SKBitmap(width, height); + using var canvas = new SKCanvas(saveBitmap); + // set background color if present + if (hasBackgroundColor) + { + canvas.Clear(SKColor.Parse(options.BackgroundColor)); + } + + // Add blur if option is present + if (blur > 0) + { + // create image from resized bitmap to apply blur + using var paint = new SKPaint(); + using var filter = SKImageFilter.CreateBlur(blur, blur); + paint.ImageFilter = filter; + canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint); + } + else + { + // draw resized bitmap onto canvas + canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height)); + } + + // If foreground layer present then draw + if (hasForegroundColor) + { + if (!double.TryParse(options.ForegroundLayer, out double opacity)) { - // scale image - bitmap.ScalePixels(resizedBitmap, SKFilterQuality.High); + opacity = .4; + } - // If all we're doing is resizing then we can stop now - if (!hasBackgroundColor && !hasForegroundColor && blur == 0 && !hasIndicator) - { - Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); - using (var outputStream = new SKFileWStream(outputPath)) - using (var pixmap = new SKPixmap(new SKImageInfo(width, height), resizedBitmap.GetPixels())) - { - pixmap.Encode(outputStream, skiaOutputFormat, quality); - return outputPath; - } - } + canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver); + } - // create bitmap to use for canvas drawing used to draw into bitmap - using (var saveBitmap = new SKBitmap(width, height)) // , bitmap.ColorType, bitmap.AlphaType)) - using (var canvas = new SKCanvas(saveBitmap)) - { - // set background color if present - if (hasBackgroundColor) - { - canvas.Clear(SKColor.Parse(options.BackgroundColor)); - } - - // Add blur if option is present - if (blur > 0) - { - // create image from resized bitmap to apply blur - using (var paint = new SKPaint()) - using (var filter = SKImageFilter.CreateBlur(blur, blur)) - { - paint.ImageFilter = filter; - canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height), paint); - } - } - else - { - // draw resized bitmap onto canvas - canvas.DrawBitmap(resizedBitmap, SKRect.Create(width, height)); - } - - // If foreground layer present then draw - if (hasForegroundColor) - { - if (!double.TryParse(options.ForegroundLayer, out double opacity)) - { - opacity = .4; - } - - canvas.DrawColor(new SKColor(0, 0, 0, (byte)((1 - opacity) * 0xFF)), SKBlendMode.SrcOver); - } - - if (hasIndicator) - { - DrawIndicator(canvas, width, height, options); - } - - Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); - using (var outputStream = new SKFileWStream(outputPath)) - { - using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels())) - { - pixmap.Encode(outputStream, skiaOutputFormat, quality); - } - } - } + if (hasIndicator) + { + DrawIndicator(canvas, width, height, options); + } + + Directory.CreateDirectory(Path.GetDirectoryName(outputPath)); + using (var outputStream = new SKFileWStream(outputPath)) + { + using (var pixmap = new SKPixmap(new SKImageInfo(width, height), saveBitmap.GetPixels())) + { + pixmap.Encode(outputStream, skiaOutputFormat, quality); } } diff --git a/Jellyfin.Drawing.Skia/SkiaException.cs b/Jellyfin.Drawing.Skia/SkiaException.cs index 968d3a244..5b272eac5 100644 --- a/Jellyfin.Drawing.Skia/SkiaException.cs +++ b/Jellyfin.Drawing.Skia/SkiaException.cs @@ -10,7 +10,7 @@ namespace Jellyfin.Drawing.Skia /// <summary> /// Initializes a new instance of the <see cref="SkiaException"/> class. /// </summary> - public SkiaException() : base() + public SkiaException() { } diff --git a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs index 61bef90ec..e0ee4a342 100644 --- a/Jellyfin.Drawing.Skia/StripCollageBuilder.cs +++ b/Jellyfin.Drawing.Skia/StripCollageBuilder.cs @@ -69,12 +69,10 @@ namespace Jellyfin.Drawing.Skia /// <param name="height">The desired height of the collage.</param> public void BuildSquareCollage(string[] paths, string outputPath, int width, int height) { - using (var bitmap = BuildSquareCollageBitmap(paths, width, height)) - using (var outputStream = new SKFileWStream(outputPath)) - using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels())) - { - pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90); - } + using var bitmap = BuildSquareCollageBitmap(paths, width, height); + using var outputStream = new SKFileWStream(outputPath); + using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()); + pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90); } /// <summary> @@ -86,56 +84,46 @@ namespace Jellyfin.Drawing.Skia /// <param name="height">The desired height of the collage.</param> public void BuildThumbCollage(string[] paths, string outputPath, int width, int height) { - using (var bitmap = BuildThumbCollageBitmap(paths, width, height)) - using (var outputStream = new SKFileWStream(outputPath)) - using (var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels())) - { - pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90); - } + using var bitmap = BuildThumbCollageBitmap(paths, width, height); + using var outputStream = new SKFileWStream(outputPath); + using var pixmap = new SKPixmap(new SKImageInfo(width, height), bitmap.GetPixels()); + pixmap.Encode(outputStream, GetEncodedFormat(outputPath), 90); } private SKBitmap BuildThumbCollageBitmap(string[] paths, int width, int height) { var bitmap = new SKBitmap(width, height); - using (var canvas = new SKCanvas(bitmap)) - { - canvas.Clear(SKColors.Black); + using var canvas = new SKCanvas(bitmap); + canvas.Clear(SKColors.Black); - // number of images used in the thumbnail - var iCount = 3; + // number of images used in the thumbnail + var iCount = 3; - // determine sizes for each image that will composited into the final image - var iSlice = Convert.ToInt32(width / iCount); - int iHeight = Convert.ToInt32(height * 1.00); - int imageIndex = 0; - for (int i = 0; i < iCount; i++) + // determine sizes for each image that will composited into the final image + var iSlice = Convert.ToInt32(width / iCount); + int iHeight = Convert.ToInt32(height * 1.00); + int imageIndex = 0; + for (int i = 0; i < iCount; i++) + { + using var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex); + imageIndex = newIndex; + if (currentBitmap == null) { - using (var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex)) - { - imageIndex = newIndex; - if (currentBitmap == null) - { - continue; - } - - // resize to the same aspect as the original - int iWidth = Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height); - using (var resizeBitmap = new SKBitmap(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType)) - { - currentBitmap.ScalePixels(resizeBitmap, SKFilterQuality.High); - - // crop image - int ix = Math.Abs((iWidth - iSlice) / 2); - using (var image = SKImage.FromBitmap(resizeBitmap)) - using (var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight))) - { - // draw image onto canvas - canvas.DrawImage(subset ?? image, iSlice * i, 0); - } - } - } + continue; } + + // resize to the same aspect as the original + int iWidth = Math.Abs(iHeight * currentBitmap.Width / currentBitmap.Height); + using var resizeBitmap = new SKBitmap(iWidth, iHeight, currentBitmap.ColorType, currentBitmap.AlphaType); + currentBitmap.ScalePixels(resizeBitmap, SKFilterQuality.High); + + // crop image + int ix = Math.Abs((iWidth - iSlice) / 2); + using var image = SKImage.FromBitmap(resizeBitmap); + using var subset = image.Subset(SKRectI.Create(ix, 0, iSlice, iHeight)); + // draw image onto canvas + canvas.DrawImage(subset ?? image, iSlice * i, 0); } return bitmap; @@ -176,33 +164,27 @@ namespace Jellyfin.Drawing.Skia var cellWidth = width / 2; var cellHeight = height / 2; - using (var canvas = new SKCanvas(bitmap)) + using var canvas = new SKCanvas(bitmap); + for (var x = 0; x < 2; x++) { - for (var x = 0; x < 2; x++) + for (var y = 0; y < 2; y++) { - for (var y = 0; y < 2; y++) + using var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex); + imageIndex = newIndex; + + if (currentBitmap == null) { - using (var currentBitmap = GetNextValidImage(paths, imageIndex, out int newIndex)) - { - imageIndex = newIndex; - - if (currentBitmap == null) - { - continue; - } - - using (var resizedBitmap = new SKBitmap(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType)) - { - // scale image - currentBitmap.ScalePixels(resizedBitmap, SKFilterQuality.High); - - // draw this image into the strip at the next position - var xPos = x * cellWidth; - var yPos = y * cellHeight; - canvas.DrawBitmap(resizedBitmap, xPos, yPos); - } - } + continue; } + + using var resizedBitmap = new SKBitmap(cellWidth, cellHeight, currentBitmap.ColorType, currentBitmap.AlphaType); + // scale image + currentBitmap.ScalePixels(resizedBitmap, SKFilterQuality.High); + + // draw this image into the strip at the next position + var xPos = x * cellWidth; + var yPos = y * cellHeight; + canvas.DrawBitmap(resizedBitmap, xPos, yPos); } } diff --git a/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs b/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs index cf3dbde2c..58f887c96 100644 --- a/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs +++ b/Jellyfin.Drawing.Skia/UnplayedCountIndicator.cs @@ -28,41 +28,37 @@ namespace Jellyfin.Drawing.Skia var x = imageSize.Width - OffsetFromTopRightCorner; var text = count.ToString(CultureInfo.InvariantCulture); - using (var paint = new SKPaint()) + using var paint = new SKPaint { - paint.Color = SKColor.Parse("#CC00A4DC"); - paint.Style = SKPaintStyle.Fill; - canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint); - } - - using (var paint = new SKPaint()) - { - paint.Color = new SKColor(255, 255, 255, 255); - paint.Style = SKPaintStyle.Fill; + Color = SKColor.Parse("#CC00A4DC"), + Style = SKPaintStyle.Fill + }; - paint.TextSize = 24; - paint.IsAntialias = true; + canvas.DrawCircle(x, OffsetFromTopRightCorner, 20, paint); - var y = OffsetFromTopRightCorner + 9; + paint.Color = new SKColor(255, 255, 255, 255); + paint.TextSize = 24; + paint.IsAntialias = true; - if (text.Length == 1) - { - x -= 7; - } + var y = OffsetFromTopRightCorner + 9; - if (text.Length == 2) - { - x -= 13; - } - else if (text.Length >= 3) - { - x -= 15; - y -= 2; - paint.TextSize = 18; - } + if (text.Length == 1) + { + x -= 7; + } - canvas.DrawText(text, x, y, paint); + if (text.Length == 2) + { + x -= 13; + } + else if (text.Length >= 3) + { + x -= 15; + y -= 2; + paint.TextSize = 18; } + + canvas.DrawText(text, x, y, paint); } } } diff --git a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs index 8f5c19900..486be6053 100644 --- a/Jellyfin.Server.Implementations/JellyfinDbProvider.cs +++ b/Jellyfin.Server.Implementations/JellyfinDbProvider.cs @@ -1,4 +1,6 @@ using System; +using System.IO; +using MediaBrowser.Common.Configuration; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -10,15 +12,20 @@ namespace Jellyfin.Server.Implementations public class JellyfinDbProvider { private readonly IServiceProvider _serviceProvider; + private readonly IApplicationPaths _appPaths; /// <summary> /// Initializes a new instance of the <see cref="JellyfinDbProvider"/> class. /// </summary> /// <param name="serviceProvider">The application's service provider.</param> - public JellyfinDbProvider(IServiceProvider serviceProvider) + /// <param name="appPaths">The application paths.</param> + public JellyfinDbProvider(IServiceProvider serviceProvider, IApplicationPaths appPaths) { _serviceProvider = serviceProvider; - serviceProvider.GetRequiredService<JellyfinDb>().Database.Migrate(); + _appPaths = appPaths; + + using var jellyfinDb = CreateContext(); + jellyfinDb.Database.Migrate(); } /// <summary> @@ -27,7 +34,8 @@ namespace Jellyfin.Server.Implementations /// <returns>The newly created context.</returns> public JellyfinDb CreateContext() { - return _serviceProvider.GetRequiredService<JellyfinDb>(); + var contextOptions = new DbContextOptionsBuilder<JellyfinDb>().UseSqlite($"Filename={Path.Combine(_appPaths.DataPath, "jellyfin.db")}"); + return ActivatorUtilities.CreateInstance<JellyfinDb>(_serviceProvider, contextOptions.Options); } } } diff --git a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs index 007c29643..6cb13cd23 100644 --- a/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs +++ b/Jellyfin.Server.Implementations/Users/DefaultPasswordResetProvider.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Security.Cryptography; +using System.Text.Json; using System.Threading.Tasks; using Jellyfin.Data.Entities; using MediaBrowser.Common; @@ -11,7 +12,6 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Authentication; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Library; -using MediaBrowser.Model.Serialization; using MediaBrowser.Model.Users; namespace Jellyfin.Server.Implementations.Users @@ -23,7 +23,6 @@ namespace Jellyfin.Server.Implementations.Users { private const string BaseResetFileName = "passwordreset"; - private readonly IJsonSerializer _jsonSerializer; private readonly IApplicationHost _appHost; private readonly string _passwordResetFileBase; @@ -33,16 +32,11 @@ namespace Jellyfin.Server.Implementations.Users /// Initializes a new instance of the <see cref="DefaultPasswordResetProvider"/> class. /// </summary> /// <param name="configurationManager">The configuration manager.</param> - /// <param name="jsonSerializer">The JSON serializer.</param> /// <param name="appHost">The application host.</param> - public DefaultPasswordResetProvider( - IServerConfigurationManager configurationManager, - IJsonSerializer jsonSerializer, - IApplicationHost appHost) + public DefaultPasswordResetProvider(IServerConfigurationManager configurationManager, IApplicationHost appHost) { _passwordResetFileBaseDir = configurationManager.ApplicationPaths.ProgramDataPath; _passwordResetFileBase = Path.Combine(_passwordResetFileBaseDir, BaseResetFileName); - _jsonSerializer = jsonSerializer; _appHost = appHost; // TODO: Remove the circular dependency on UserManager } @@ -63,7 +57,7 @@ namespace Jellyfin.Server.Implementations.Users SerializablePasswordReset spr; await using (var str = File.OpenRead(resetFile)) { - spr = await _jsonSerializer.DeserializeFromStreamAsync<SerializablePasswordReset>(str).ConfigureAwait(false); + spr = await JsonSerializer.DeserializeAsync<SerializablePasswordReset>(str).ConfigureAwait(false); } if (spr.ExpirationDate < DateTime.UtcNow) @@ -119,7 +113,7 @@ namespace Jellyfin.Server.Implementations.Users await using (FileStream fileStream = File.OpenWrite(filePath)) { - _jsonSerializer.SerializeToStream(spr, fileStream); + await JsonSerializer.SerializeAsync(fileStream, spr).ConfigureAwait(false); await fileStream.FlushAsync().ConfigureAwait(false); } diff --git a/Jellyfin.Server.Implementations/Users/UserManager.cs b/Jellyfin.Server.Implementations/Users/UserManager.cs index e49a99fe5..11402ee05 100644 --- a/Jellyfin.Server.Implementations/Users/UserManager.cs +++ b/Jellyfin.Server.Implementations/Users/UserManager.cs @@ -600,18 +600,13 @@ namespace Jellyfin.Server.Implementations.Users } var defaultName = Environment.UserName; - if (string.IsNullOrWhiteSpace(defaultName)) + if (string.IsNullOrWhiteSpace(defaultName) || !IsValidUsername(defaultName)) { defaultName = "MyJellyfinUser"; } _logger.LogWarning("No users, creating one with username {UserName}", defaultName); - if (!IsValidUsername(defaultName)) - { - throw new ArgumentException("Provided username is not valid!", defaultName); - } - var newUser = await CreateUserInternalAsync(defaultName, dbContext).ConfigureAwait(false); newUser.SetPermission(PermissionKind.IsAdministrator, true); newUser.SetPermission(PermissionKind.EnableContentDeletion, true); @@ -766,8 +761,8 @@ namespace Jellyfin.Server.Implementations.Users { // This is some regex that matches only on unicode "word" characters, as well as -, _ and @ // In theory this will cut out most if not all 'control' characters which should help minimize any weirdness - // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), and periods (.) - return Regex.IsMatch(name, @"^[\w\-'._@]*$"); + // Usernames can contain letters (a-z + whatever else unicode is cool with), numbers (0-9), at-signs (@), dashes (-), underscores (_), apostrophes ('), periods (.) and spaces ( ) + return Regex.IsMatch(name, @"^[\w\ \-'._@]*$"); } private IAuthenticationProvider GetAuthenticationProvider(User user) diff --git a/Jellyfin.Server/CoreAppHost.cs b/Jellyfin.Server/CoreAppHost.cs index e6e09b79d..29a59e1c8 100644 --- a/Jellyfin.Server/CoreAppHost.cs +++ b/Jellyfin.Server/CoreAppHost.cs @@ -64,11 +64,12 @@ namespace Jellyfin.Server Logger.LogWarning($"Skia not available. Will fallback to {nameof(NullImageEncoder)}."); } - // TODO: Set up scoping and use AddDbContextPool - serviceCollection.AddDbContext<JellyfinDb>( - options => options - .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"), - ServiceLifetime.Transient); + // TODO: Set up scoping and use AddDbContextPool, + // can't register as Transient since tracking transient in GC is funky + // serviceCollection.AddDbContext<JellyfinDb>( + // options => options + // .UseSqlite($"Filename={Path.Combine(ApplicationPaths.DataPath, "jellyfin.db")}"), + // ServiceLifetime.Transient); serviceCollection.AddSingleton<JellyfinDbProvider>(); diff --git a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs index 418cd92b3..c80e8e64f 100644 --- a/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/BaseHlsService.cs @@ -194,7 +194,8 @@ namespace MediaBrowser.Api.Playback.Hls var paddedBitrate = Convert.ToInt32(bitrate * 1.15); // Main stream - builder.AppendLine("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=" + paddedBitrate.ToString(CultureInfo.InvariantCulture)); + builder.Append("#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=") + .AppendLine(paddedBitrate.ToString(CultureInfo.InvariantCulture)); var playlistUrl = "hls/" + Path.GetFileName(firstPlaylist).Replace(".m3u8", "/stream.m3u8"); builder.AppendLine(playlistUrl); diff --git a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs index fe5f980b1..661c1ba5f 100644 --- a/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs +++ b/MediaBrowser.Api/Playback/Hls/DynamicHlsService.cs @@ -369,7 +369,7 @@ namespace MediaBrowser.Api.Playback.Hls var playlistFilename = Path.GetFileNameWithoutExtension(playlist); - var indexString = Path.GetFileNameWithoutExtension(file.Name).Substring(playlistFilename.Length); + var indexString = Path.GetFileNameWithoutExtension(file.Name).AsSpan().Slice(playlistFilename.Length); return int.Parse(indexString, NumberStyles.Integer, CultureInfo.InvariantCulture); } @@ -968,7 +968,8 @@ namespace MediaBrowser.Api.Playback.Hls builder.AppendLine("#EXTM3U"); builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD"); builder.AppendLine("#EXT-X-VERSION:3"); - builder.AppendLine("#EXT-X-TARGETDURATION:" + Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(CultureInfo.InvariantCulture)); + builder.Append("#EXT-X-TARGETDURATION:") + .AppendLine(Math.Ceiling(segmentLengths.Length > 0 ? segmentLengths.Max() : state.SegmentLength).ToString(CultureInfo.InvariantCulture)); builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0"); var queryStringIndex = Request.RawUrl.IndexOf('?'); @@ -983,14 +984,17 @@ namespace MediaBrowser.Api.Playback.Hls foreach (var length in segmentLengths) { - builder.AppendLine("#EXTINF:" + length.ToString("0.0000", CultureInfo.InvariantCulture) + ", nodesc"); - - builder.AppendLine(string.Format("hls1/{0}/{1}{2}{3}", + builder.Append("#EXTINF:") + .Append(length.ToString("0.0000", CultureInfo.InvariantCulture)) + .AppendLine(", nodesc"); + builder.AppendFormat( + CultureInfo.InvariantCulture, + "hls1/{0}/{1}{2}{3}", name, index.ToString(CultureInfo.InvariantCulture), GetSegmentFileExtension(request), - queryString)); + queryString).AppendLine(); index++; } diff --git a/MediaBrowser.Api/Subtitles/SubtitleService.cs b/MediaBrowser.Api/Subtitles/SubtitleService.cs index a70da8e56..6a6196d8a 100644 --- a/MediaBrowser.Api/Subtitles/SubtitleService.cs +++ b/MediaBrowser.Api/Subtitles/SubtitleService.cs @@ -175,11 +175,12 @@ namespace MediaBrowser.Api.Subtitles throw new ArgumentException("segmentLength was not given, or it was given incorrectly. (It should be bigger than 0)"); } - builder.AppendLine("#EXTM3U"); - builder.AppendLine("#EXT-X-TARGETDURATION:" + request.SegmentLength.ToString(CultureInfo.InvariantCulture)); - builder.AppendLine("#EXT-X-VERSION:3"); - builder.AppendLine("#EXT-X-MEDIA-SEQUENCE:0"); - builder.AppendLine("#EXT-X-PLAYLIST-TYPE:VOD"); + builder.AppendLine("#EXTM3U") + .Append("#EXT-X-TARGETDURATION:") + .AppendLine(request.SegmentLength.ToString(CultureInfo.InvariantCulture)) + .AppendLine("#EXT-X-VERSION:3") + .AppendLine("#EXT-X-MEDIA-SEQUENCE:0") + .AppendLine("#EXT-X-PLAYLIST-TYPE:VOD"); long positionTicks = 0; @@ -190,7 +191,9 @@ namespace MediaBrowser.Api.Subtitles var remaining = runtime - positionTicks; var lengthTicks = Math.Min(remaining, segmentLengthTicks); - builder.AppendLine("#EXTINF:" + TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture) + ","); + builder.Append("#EXTINF:") + .Append(TimeSpan.FromTicks(lengthTicks).TotalSeconds.ToString(CultureInfo.InvariantCulture)) + .AppendLine(","); var endPositionTicks = Math.Min(runtime, positionTicks + segmentLengthTicks); diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 90f62e153..f34309c40 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -675,11 +675,11 @@ namespace MediaBrowser.Controller.Entities return System.IO.Path.Combine(basePath, "channels", ChannelId.ToString("N", CultureInfo.InvariantCulture), Id.ToString("N", CultureInfo.InvariantCulture)); } - var idString = Id.ToString("N", CultureInfo.InvariantCulture); + ReadOnlySpan<char> idString = Id.ToString("N", CultureInfo.InvariantCulture); basePath = System.IO.Path.Combine(basePath, "library"); - return System.IO.Path.Combine(basePath, idString.Substring(0, 2), idString); + return System.IO.Path.Join(basePath, idString.Slice(0, 2), idString); } /// <summary> @@ -702,26 +702,27 @@ namespace MediaBrowser.Controller.Entities foreach (var removeChar in ConfigurationManager.Configuration.SortRemoveCharacters) { - sortable = sortable.Replace(removeChar, string.Empty); + sortable = sortable.Replace(removeChar, string.Empty, StringComparison.Ordinal); } foreach (var replaceChar in ConfigurationManager.Configuration.SortReplaceCharacters) { - sortable = sortable.Replace(replaceChar, " "); + sortable = sortable.Replace(replaceChar, " ", StringComparison.Ordinal); } foreach (var search in ConfigurationManager.Configuration.SortRemoveWords) { // Remove from beginning if a space follows - if (sortable.StartsWith(search + " ")) + if (sortable.StartsWith(search + " ", StringComparison.Ordinal)) { sortable = sortable.Remove(0, search.Length + 1); } + // Remove from middle if surrounded by spaces - sortable = sortable.Replace(" " + search + " ", " "); + sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal); // Remove from end if followed by a space - if (sortable.EndsWith(" " + search)) + if (sortable.EndsWith(" " + search, StringComparison.Ordinal)) { sortable = sortable.Remove(sortable.Length - (search.Length + 1)); } @@ -751,6 +752,7 @@ namespace MediaBrowser.Controller.Entities builder.Append(chunkBuilder); } + // logger.LogDebug("ModifySortChunks Start: {0} End: {1}", name, builder.ToString()); return builder.ToString().RemoveDiacritics(); } diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 0242338b7..913e171f1 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -456,6 +456,7 @@ namespace MediaBrowser.Controller.MediaEncoding var isQsvEncoder = outputVideoCodec.IndexOf("qsv", StringComparison.OrdinalIgnoreCase) != -1; var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); var isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux); + var isMacOS = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); if (!IsCopyCodec(outputVideoCodec)) { @@ -511,6 +512,12 @@ namespace MediaBrowser.Controller.MediaEncoding } } } + + if (state.IsVideoRequest + && string.Equals(encodingOptions.HardwareAccelerationType, "videotoolbox", StringComparison.OrdinalIgnoreCase)) + { + arg.Append("-hwaccel videotoolbox "); + } } arg.Append("-i ") @@ -1588,7 +1595,7 @@ namespace MediaBrowser.Controller.MediaEncoding { outputVideoCodec ??= string.Empty; - var outputSizeParam = string.Empty; + var outputSizeParam = ReadOnlySpan<char>.Empty; var request = state.BaseRequest; // Add resolution params, if specified @@ -1602,42 +1609,42 @@ namespace MediaBrowser.Controller.MediaEncoding var index = outputSizeParam.IndexOf("hwdownload", StringComparison.OrdinalIgnoreCase); if (index != -1) { - outputSizeParam = outputSizeParam.Substring(index); + outputSizeParam = outputSizeParam.Slice(index); } else { index = outputSizeParam.IndexOf("hwupload=extra_hw_frames", StringComparison.OrdinalIgnoreCase); if (index != -1) { - outputSizeParam = outputSizeParam.Substring(index); + outputSizeParam = outputSizeParam.Slice(index); } else { index = outputSizeParam.IndexOf("format", StringComparison.OrdinalIgnoreCase); if (index != -1) { - outputSizeParam = outputSizeParam.Substring(index); + outputSizeParam = outputSizeParam.Slice(index); } else { index = outputSizeParam.IndexOf("yadif", StringComparison.OrdinalIgnoreCase); if (index != -1) { - outputSizeParam = outputSizeParam.Substring(index); + outputSizeParam = outputSizeParam.Slice(index); } else { index = outputSizeParam.IndexOf("scale", StringComparison.OrdinalIgnoreCase); if (index != -1) { - outputSizeParam = outputSizeParam.Substring(index); + outputSizeParam = outputSizeParam.Slice(index); } else { index = outputSizeParam.IndexOf("vpp", StringComparison.OrdinalIgnoreCase); if (index != -1) { - outputSizeParam = outputSizeParam.Substring(index); + outputSizeParam = outputSizeParam.Slice(index); } } } @@ -1685,9 +1692,9 @@ namespace MediaBrowser.Controller.MediaEncoding // Setup default filtergraph utilizing FFMpeg overlay() and FFMpeg scale() (see the return of this function for index reference) // Always put the scaler before the overlay for better performance - var retStr = !string.IsNullOrEmpty(outputSizeParam) ? - " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"" : - " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\""; + var retStr = !outputSizeParam.IsEmpty + ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay\"" + : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay\""; // When the input may or may not be hardware VAAPI decodable if (string.Equals(outputVideoCodec, "h264_vaapi", StringComparison.OrdinalIgnoreCase)) @@ -1721,7 +1728,7 @@ namespace MediaBrowser.Controller.MediaEncoding */ if (isLinux) { - retStr = !string.IsNullOrEmpty(outputSizeParam) ? + retStr = !outputSizeParam.IsEmpty ? " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}]{3}[base];[base][sub]overlay_qsv\"" : " -filter_complex \"[{0}:{1}]{4}[sub];[0:{2}][sub]overlay_qsv\""; } @@ -1733,7 +1740,7 @@ namespace MediaBrowser.Controller.MediaEncoding mapPrefix, subtitleStreamIndex, state.VideoStream.Index, - outputSizeParam, + outputSizeParam.ToString(), videoSizeParam); } @@ -2120,7 +2127,7 @@ namespace MediaBrowser.Controller.MediaEncoding || state.DeInterlace("h265", true) || state.DeInterlace("hevc", true)) { - var deintParam = string.Empty; + string deintParam; var inputFramerate = videoStream?.RealFrameRate; // If it is already 60fps then it will create an output framerate that is much too high for roku and others to handle @@ -2183,7 +2190,6 @@ namespace MediaBrowser.Controller.MediaEncoding return output; } - /// <summary> /// Gets the number of threads. /// </summary> diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index c85d963ed..0b447e3e6 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -1367,7 +1367,9 @@ namespace MediaBrowser.MediaEncoding.Probing // OR -> COMMENT. SUBTITLE: DESCRIPTION // e.g. -> 4/13. The Doctor's Wife: Science fiction drama. When he follows a Time Lord distress signal, the Doctor puts Amy, Rory and his beloved TARDIS in grave danger. Also in HD. [AD,S] // e.g. -> CBeebies Bedtime Hour. The Mystery: Animated adventures of two friends who live on an island in the middle of the big city. Some of Abney and Teal's favourite objects are missing. [S] - if (string.IsNullOrWhiteSpace(subTitle) && !string.IsNullOrWhiteSpace(description) && description.Substring(0, Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)).Contains(":")) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename + if (string.IsNullOrWhiteSpace(subTitle) + && !string.IsNullOrWhiteSpace(description) + && description.AsSpan().Slice(0, Math.Min(description.Length, MaxSubtitleDescriptionExtractionLength)).IndexOf(':') != -1) // Check within the Subtitle size limit, otherwise from description it can get too long creating an invalid filename { string[] parts = description.Split(':'); if (parts.Length > 0) @@ -1375,7 +1377,7 @@ namespace MediaBrowser.MediaEncoding.Probing string subtitle = parts[0]; try { - if (subtitle.Contains("/")) // It contains a episode number and season number + if (subtitle.Contains('/', StringComparison.Ordinal)) // It contains a episode number and season number { string[] numbers = subtitle.Split(' '); video.IndexNumber = int.Parse(numbers[0].Replace(".", "").Split('/')[0]); @@ -1390,8 +1392,11 @@ namespace MediaBrowser.MediaEncoding.Probing } catch // Default parsing { - if (subtitle.Contains(".")) // skip the comment, keep the subtitle + if (subtitle.Contains('.', StringComparison.Ordinal)) + { + // skip the comment, keep the subtitle description = string.Join(".", subtitle.Split('.'), 1, subtitle.Split('.').Length - 1).Trim(); // skip the first + } else { description = subtitle.Trim(); // Clean up whitespaces and save it diff --git a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs index 0e2d70017..43a45291c 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/AssParser.cs @@ -35,7 +35,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles continue; } - if (line.StartsWith("[")) + if (line[0] == '[') { break; } @@ -62,7 +62,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles return trackInfo; } - long GetTicks(string time) + private long GetTicks(ReadOnlySpan<char> time) { return TimeSpan.TryParseExact(time, @"h\:mm\:ss\.ff", _usCulture, out var span) ? span.Ticks : 0; diff --git a/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs b/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs index bf8808eb8..dca5c1e8a 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/ParserValues.cs @@ -1,6 +1,9 @@ +#nullable enable +#pragma warning disable CS1591 + namespace MediaBrowser.MediaEncoding.Subtitles { - public class ParserValues + public static class ParserValues { public const string NewLine = "\r\n"; } diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs index 728efa788..cc35efb3f 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SrtParser.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Generic; using System.Globalization; @@ -20,6 +22,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles _logger = logger; } + /// <inheritdoc /> public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken) { var trackInfo = new SubtitleTrackInfo(); @@ -55,11 +58,11 @@ namespace MediaBrowser.MediaEncoding.Subtitles } subEvent.StartPositionTicks = GetTicks(time[0]); - var endTime = time[1]; - var idx = endTime.IndexOf(" ", StringComparison.Ordinal); + var endTime = time[1].AsSpan(); + var idx = endTime.IndexOf(' '); if (idx > 0) { - endTime = endTime.Substring(0, idx); + endTime = endTime.Slice(0, idx); } subEvent.EndPositionTicks = GetTicks(endTime); @@ -88,7 +91,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles return trackInfo; } - long GetTicks(string time) + private long GetTicks(ReadOnlySpan<char> time) { return TimeSpan.TryParseExact(time, @"hh\:mm\:ss\.fff", _usCulture, out var span) ? span.Ticks diff --git a/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs index 45b317b2e..143c010b7 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SrtWriter.cs @@ -8,8 +8,12 @@ using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.MediaEncoding.Subtitles { + /// <summary> + /// SRT subtitle writer. + /// </summary> public class SrtWriter : ISubtitleWriter { + /// <inheritdoc /> public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken) { using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) diff --git a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs index 77033c6b4..bd330f568 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SsaParser.cs @@ -12,6 +12,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// </summary> public class SsaParser : ISubtitleParser { + /// <inheritdoc /> public SubtitleTrackInfo Parse(Stream stream, CancellationToken cancellationToken) { var trackInfo = new SubtitleTrackInfo(); @@ -45,7 +46,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles header.AppendLine(line); } - if (line.Trim().ToLowerInvariant() == "[events]") + if (string.Equals(line.Trim(), "[events]", StringComparison.OrdinalIgnoreCase)) { eventsStarted = true; } @@ -63,27 +64,27 @@ namespace MediaBrowser.MediaEncoding.Subtitles format = line.ToLowerInvariant().Substring(8).Split(','); for (int i = 0; i < format.Length; i++) { - if (format[i].Trim().ToLowerInvariant() == "layer") + if (string.Equals(format[i].Trim(), "layer", StringComparison.OrdinalIgnoreCase)) { indexLayer = i; } - else if (format[i].Trim().ToLowerInvariant() == "start") + else if (string.Equals(format[i].Trim(), "start", StringComparison.OrdinalIgnoreCase)) { indexStart = i; } - else if (format[i].Trim().ToLowerInvariant() == "end") + else if (string.Equals(format[i].Trim(), "end", StringComparison.OrdinalIgnoreCase)) { indexEnd = i; } - else if (format[i].Trim().ToLowerInvariant() == "text") + else if (string.Equals(format[i].Trim(), "text", StringComparison.OrdinalIgnoreCase)) { indexText = i; } - else if (format[i].Trim().ToLowerInvariant() == "effect") + else if (string.Equals(format[i].Trim(), "effect", StringComparison.OrdinalIgnoreCase)) { indexEffect = i; } - else if (format[i].Trim().ToLowerInvariant() == "style") + else if (string.Equals(format[i].Trim(), "style", StringComparison.OrdinalIgnoreCase)) { indexStyle = i; } @@ -184,12 +185,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles int.Parse(timeCode[3]) * 10).Ticks; } - public static string GetFormattedText(string text) + private static string GetFormattedText(string text) { text = text.Replace("\\n", ParserValues.NewLine, StringComparison.OrdinalIgnoreCase); - bool italic = false; - for (int i = 0; i < 10; i++) // just look ten times... { if (text.Contains(@"{\fn")) @@ -200,7 +199,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { string fontName = text.Substring(start + 4, end - (start + 4)); string extraTags = string.Empty; - CheckAndAddSubTags(ref fontName, ref extraTags, out italic); + CheckAndAddSubTags(ref fontName, ref extraTags, out bool italic); text = text.Remove(start, end - start + 1); if (italic) { @@ -231,7 +230,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { string fontSize = text.Substring(start + 4, end - (start + 4)); string extraTags = string.Empty; - CheckAndAddSubTags(ref fontSize, ref extraTags, out italic); + CheckAndAddSubTags(ref fontSize, ref extraTags, out bool italic); if (IsInteger(fontSize)) { text = text.Remove(start, end - start + 1); @@ -265,7 +264,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { string color = text.Substring(start + 4, end - (start + 4)); string extraTags = string.Empty; - CheckAndAddSubTags(ref color, ref extraTags, out italic); + CheckAndAddSubTags(ref color, ref extraTags, out bool italic); color = color.Replace("&", string.Empty).TrimStart('H'); color = color.PadLeft(6, '0'); @@ -303,7 +302,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles { string color = text.Substring(start + 5, end - (start + 5)); string extraTags = string.Empty; - CheckAndAddSubTags(ref color, ref extraTags, out italic); + CheckAndAddSubTags(ref color, ref extraTags, out bool italic); color = color.Replace("&", string.Empty).TrimStart('H'); color = color.PadLeft(6, '0'); @@ -354,14 +353,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles } private static bool IsInteger(string s) - { - if (int.TryParse(s, out var i)) - { - return true; - } - - return false; - } + => int.TryParse(s, out _); private static int CountTagInText(string text, string tag) { diff --git a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs index a971115c9..2afa89cda 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/SubtitleEncoder.cs @@ -1,3 +1,5 @@ +#pragma warning disable CS1591 + using System; using System.Collections.Concurrent; using System.Diagnostics; @@ -172,7 +174,13 @@ namespace MediaBrowser.MediaEncoding.Subtitles inputFiles = new[] { mediaSource.Path }; } - var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, _mediaSourceManager.GetPathProtocol(subtitleStream.Path), subtitleStream, cancellationToken).ConfigureAwait(false); + var protocol = mediaSource.Protocol; + if (subtitleStream.IsExternal) + { + protocol = _mediaSourceManager.GetPathProtocol(subtitleStream.Path); + } + + var fileInfo = await GetReadableFile(mediaSource.Path, inputFiles, protocol, subtitleStream, cancellationToken).ConfigureAwait(false); var stream = await GetSubtitleStream(fileInfo.Path, fileInfo.Protocol, fileInfo.IsExternal, cancellationToken).ConfigureAwait(false); @@ -365,7 +373,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles /// <returns>System.Object.</returns> private SemaphoreSlim GetLock(string filename) { - return _semaphoreLocks.GetOrAdd(filename, key => new SemaphoreSlim(1, 1)); + return _semaphoreLocks.GetOrAdd(filename, _ => new SemaphoreSlim(1, 1)); } /// <summary> @@ -426,7 +434,9 @@ namespace MediaBrowser.MediaEncoding.Subtitles // FFmpeg automatically convert character encoding when it is UTF-16 // If we specify character encoding, it rejects with "do not specify a character encoding" and "Unable to recode subtitle event" - if ((inputPath.EndsWith(".smi") || inputPath.EndsWith(".sami")) && (encodingParam == "UTF-16BE" || encodingParam == "UTF-16LE")) + if ((inputPath.EndsWith(".smi") || inputPath.EndsWith(".sami")) && + (encodingParam.Equals("UTF-16BE", StringComparison.OrdinalIgnoreCase) || + encodingParam.Equals("UTF-16LE", StringComparison.OrdinalIgnoreCase))) { encodingParam = ""; } @@ -721,19 +731,19 @@ namespace MediaBrowser.MediaEncoding.Subtitles var date = _fileSystem.GetLastWriteTimeUtc(mediaPath); - var filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension; + ReadOnlySpan<char> filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture) + "_" + date.Ticks.ToString(CultureInfo.InvariantCulture) + ticksParam).GetMD5() + outputSubtitleExtension; - var prefix = filename.Substring(0, 1); + var prefix = filename.Slice(0, 1); - return Path.Combine(SubtitleCachePath, prefix, filename); + return Path.Join(SubtitleCachePath, prefix, filename); } else { - var filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension; + ReadOnlySpan<char> filename = (mediaPath + "_" + subtitleStreamIndex.ToString(CultureInfo.InvariantCulture)).GetMD5() + outputSubtitleExtension; - var prefix = filename.Substring(0, 1); + var prefix = filename.Slice(0, 1); - return Path.Combine(SubtitleCachePath, prefix, filename); + return Path.Join(SubtitleCachePath, prefix, filename); } } diff --git a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs index 7d3e18578..e5c785bc5 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/TtmlWriter.cs @@ -6,8 +6,12 @@ using MediaBrowser.Model.MediaInfo; namespace MediaBrowser.MediaEncoding.Subtitles { + /// <summary> + /// TTML subtitle writer. + /// </summary> public class TtmlWriter : ISubtitleWriter { + /// <inheritdoc /> public void Write(SubtitleTrackInfo info, Stream stream, CancellationToken cancellationToken) { // Example: https://github.com/zmalltalker/ttml2vtt/blob/master/data/sample.xml @@ -36,9 +40,10 @@ namespace MediaBrowser.MediaEncoding.Subtitles text = Regex.Replace(text, @"\\n", "<br/>", RegexOptions.IgnoreCase); - writer.WriteLine("<p begin=\"{0}\" dur=\"{1}\">{2}</p>", + writer.WriteLine( + "<p begin=\"{0}\" dur=\"{1}\">{2}</p>", trackEvent.StartPositionTicks, - (trackEvent.EndPositionTicks - trackEvent.StartPositionTicks), + trackEvent.EndPositionTicks - trackEvent.StartPositionTicks, text); } diff --git a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs index de35acbba..ad32cb794 100644 --- a/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs +++ b/MediaBrowser.MediaEncoding/Subtitles/VttWriter.cs @@ -47,7 +47,7 @@ namespace MediaBrowser.MediaEncoding.Subtitles text = Regex.Replace(text, @"\\n", " ", RegexOptions.IgnoreCase); writer.WriteLine(text); - writer.WriteLine(string.Empty); + writer.WriteLine(); } } } diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index 7a488005e..1b37cfc93 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -112,107 +112,146 @@ namespace MediaBrowser.Model.Entities { get { - if (Type == MediaStreamType.Audio) + switch (Type) { - // if (!string.IsNullOrEmpty(Title)) - //{ - // return AddLanguageIfNeeded(Title); - //} - - var attributes = new List<string>(); - - if (!string.IsNullOrEmpty(Language)) - { - attributes.Add(StringHelper.FirstToUpper(Language)); - } - - if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase)) - { - attributes.Add(AudioCodec.GetFriendlyName(Codec)); - } - else if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase)) - { - attributes.Add(Profile); - } - - if (!string.IsNullOrEmpty(ChannelLayout)) - { - attributes.Add(ChannelLayout); - } - else if (Channels.HasValue) + case MediaStreamType.Audio: { - attributes.Add(Channels.Value.ToString(CultureInfo.InvariantCulture) + " ch"); + var attributes = new List<string>(); + + if (!string.IsNullOrEmpty(Language)) + { + // Get full language string i.e. eng -> English. Will not work for some languages which use ISO 639-2/B instead of /T codes. + string fullLanguage = CultureInfo + .GetCultures(CultureTypes.NeutralCultures) + .FirstOrDefault(r => r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase)) + ?.DisplayName; + attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language)); + } + + if (!string.IsNullOrEmpty(Codec) && !string.Equals(Codec, "dca", StringComparison.OrdinalIgnoreCase)) + { + attributes.Add(AudioCodec.GetFriendlyName(Codec)); + } + else if (!string.IsNullOrEmpty(Profile) && !string.Equals(Profile, "lc", StringComparison.OrdinalIgnoreCase)) + { + attributes.Add(Profile); + } + + if (!string.IsNullOrEmpty(ChannelLayout)) + { + attributes.Add(StringHelper.FirstToUpper(ChannelLayout)); + } + else if (Channels.HasValue) + { + attributes.Add(Channels.Value.ToString(CultureInfo.InvariantCulture) + " ch"); + } + + if (IsDefault) + { + attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault); + } + + if (!string.IsNullOrEmpty(Title)) + { + var result = new StringBuilder(Title); + foreach (var tag in attributes) + { + // Keep Tags that are not already in Title. + if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1) + { + result.Append(" - ").Append(tag); + } + } + + return result.ToString(); + } + + return string.Join(" - ", attributes); } - if (IsDefault) + case MediaStreamType.Video: { - attributes.Add("Default"); + var attributes = new List<string>(); + + var resolutionText = GetResolutionText(); + + if (!string.IsNullOrEmpty(resolutionText)) + { + attributes.Add(resolutionText); + } + + if (!string.IsNullOrEmpty(Codec)) + { + attributes.Add(Codec.ToUpperInvariant()); + } + + if (!string.IsNullOrEmpty(Title)) + { + var result = new StringBuilder(Title); + foreach (var tag in attributes) + { + // Keep Tags that are not already in Title. + if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1) + { + result.Append(" - ").Append(tag); + } + } + + return result.ToString(); + } + + return string.Join(" ", attributes); } - return string.Join(" ", attributes); - } - - if (Type == MediaStreamType.Video) - { - var attributes = new List<string>(); - - var resolutionText = GetResolutionText(); - - if (!string.IsNullOrEmpty(resolutionText)) + case MediaStreamType.Subtitle: { - attributes.Add(resolutionText); + var attributes = new List<string>(); + + if (!string.IsNullOrEmpty(Language)) + { + // Get full language string i.e. eng -> English. Will not work for some languages which use ISO 639-2/B instead of /T codes. + string fullLanguage = CultureInfo + .GetCultures(CultureTypes.NeutralCultures) + .FirstOrDefault(r => r.ThreeLetterISOLanguageName.Equals(Language, StringComparison.OrdinalIgnoreCase)) + ?.DisplayName; + attributes.Add(StringHelper.FirstToUpper(fullLanguage ?? Language)); + } + else + { + attributes.Add(string.IsNullOrEmpty(localizedUndefined) ? "Und" : localizedUndefined); + } + + if (IsDefault) + { + attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault); + } + + if (IsForced) + { + attributes.Add(string.IsNullOrEmpty(localizedForced) ? "Forced" : localizedForced); + } + + if (!string.IsNullOrEmpty(Title)) + { + var result = new StringBuilder(Title); + foreach (var tag in attributes) + { + // Keep Tags that are not already in Title. + if (Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1) + { + result.Append(" - ").Append(tag); + } + } + + return result.ToString(); + } + + return string.Join(" - ", attributes.ToArray()); } - if (!string.IsNullOrEmpty(Codec)) - { - attributes.Add(Codec.ToUpperInvariant()); - } - - return string.Join(" ", attributes); + default: + return null; } - - if (Type == MediaStreamType.Subtitle) - { - - var attributes = new List<string>(); - - if (!string.IsNullOrEmpty(Language)) - { - attributes.Add(StringHelper.FirstToUpper(Language)); - } - else - { - attributes.Add(string.IsNullOrEmpty(localizedUndefined) ? "Und" : localizedUndefined); - } - - if (IsDefault) - { - attributes.Add(string.IsNullOrEmpty(localizedDefault) ? "Default" : localizedDefault); - } - - if (IsForced) - { - attributes.Add(string.IsNullOrEmpty(localizedForced) ? "Forced" : localizedForced); - } - - if (!string.IsNullOrEmpty(Title)) - { - return attributes.AsEnumerable() - // keep Tags that are not already in Title - .Where(tag => Title.IndexOf(tag, StringComparison.OrdinalIgnoreCase) == -1) - // attributes concatenation, starting with Title - .Aggregate(new StringBuilder(Title), (builder, attr) => builder.Append(" - ").Append(attr)) - .ToString(); - } - - return string.Join(" - ", attributes.ToArray()); - } - - if (Type == MediaStreamType.Video) - { - } - - return null; } } diff --git a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs index 80acb2c05..f69ec9744 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs @@ -93,7 +93,7 @@ namespace MediaBrowser.Providers.MediaInfo private string GetAudioImagePath(Audio item) { - string filename = null; + string filename; if (item.GetType() == typeof(Audio)) { @@ -116,9 +116,9 @@ namespace MediaBrowser.Providers.MediaInfo filename = item.Id.ToString("N", CultureInfo.InvariantCulture) + ".jpg"; } - var prefix = filename.Substring(0, 1); + var prefix = filename.AsSpan().Slice(0, 1); - return Path.Combine(AudioImagesPath, prefix, filename); + return Path.Join(AudioImagesPath, prefix, filename); } public string AudioImagesPath => Path.Combine(_config.ApplicationPaths.CachePath, "extracted-audio-images"); diff --git a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs index cb7c0362e..54054d015 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/Plugin.cs @@ -19,6 +19,9 @@ namespace MediaBrowser.Providers.Plugins.AudioDb public override string Description => "Get artist and album metadata or images from AudioDB."; + // TODO remove when plugin removed from server. + public override string ConfigurationFileName => "Jellyfin.Plugin.AudioDb.xml"; + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer) { diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs index a7e6267da..90266e440 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/Plugin.cs @@ -23,6 +23,9 @@ namespace MediaBrowser.Providers.Plugins.MusicBrainz public const long DefaultRateLimit = 2000u; + // TODO remove when plugin removed from server. + public override string ConfigurationFileName => "Jellyfin.Plugin.MusicBrainz.xml"; + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer) { diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs index 944ba26af..12aecba84 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbItemProvider.cs @@ -170,7 +170,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb item.SetProviderId(MetadataProvider.Imdb, result.imdbID); if (result.Year.Length > 0 - && int.TryParse(result.Year.Substring(0, Math.Min(result.Year.Length, 4)), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedYear)) + && int.TryParse(result.Year.AsSpan().Slice(0, Math.Min(result.Year.Length, 4)), NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedYear)) { item.ProductionYear = parsedYear; } diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index 9700f3b18..13098d140 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -62,7 +62,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb } if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4 - && int.TryParse(result.Year.Substring(0, 4), NumberStyles.Number, _usCulture, out var year) + && int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, _usCulture, out var year) && year >= 0) { item.ProductionYear = year; @@ -163,7 +163,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb } if (!string.IsNullOrEmpty(result.Year) && result.Year.Length >= 4 - && int.TryParse(result.Year.Substring(0, 4), NumberStyles.Number, _usCulture, out var year) + && int.TryParse(result.Year.AsSpan().Slice(0, 4), NumberStyles.Number, _usCulture, out var year) && year >= 0) { item.ProductionYear = year; diff --git a/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs b/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs index 4cf5f4ce6..41ca56164 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/Plugin.cs @@ -19,6 +19,9 @@ namespace MediaBrowser.Providers.Plugins.Omdb public override string Description => "Get metadata for movies and other video content from OMDb."; + // TODO remove when plugin removed from server. + public override string ConfigurationFileName => "Jellyfin.Plugin.Omdb.xml"; + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer) { diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs b/MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs index aa5f819f0..e7079ed3c 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/Plugin.cs @@ -17,6 +17,9 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb public override string Description => "Get metadata for movies and other video content from TheTVDB."; + // TODO remove when plugin removed from server. + public override string ConfigurationFileName => "Jellyfin.Plugin.TheTvdb.xml"; + public Plugin(IApplicationPaths applicationPaths, IXmlSerializer xmlSerializer) : base(applicationPaths, xmlSerializer) { diff --git a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs index ced287d54..52fc53872 100644 --- a/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/TheTvdb/TvdbEpisodeProvider.cs @@ -188,7 +188,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb for (var i = 0; i < episode.GuestStars.Length; ++i) { var currentActor = episode.GuestStars[i]; - var roleStartIndex = currentActor.IndexOf('('); + var roleStartIndex = currentActor.IndexOf('(', StringComparison.Ordinal); if (roleStartIndex == -1) { @@ -207,7 +207,7 @@ namespace MediaBrowser.Providers.Plugins.TheTvdb for (var j = i + 1; j < episode.GuestStars.Length; ++j) { var currentRole = episode.GuestStars[j]; - var roleEndIndex = currentRole.IndexOf(')'); + var roleEndIndex = currentRole.IndexOf(')', StringComparison.Ordinal); if (roleEndIndex == -1) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs index 76d3f8224..58cbf9eef 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs @@ -251,9 +251,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People private static string GetPersonDataPath(IApplicationPaths appPaths, string tmdbId) { - var letter = tmdbId.GetMD5().ToString().Substring(0, 1); + var letter = tmdbId.GetMD5().ToString().AsSpan().Slice(0, 1); - return Path.Combine(GetPersonsDataPath(appPaths), letter, tmdbId); + return Path.Join(GetPersonsDataPath(appPaths), letter, tmdbId); } internal static string GetPersonDataFilePath(IApplicationPaths appPaths, string tmdbId) diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index b2d99c1a1..b06464409 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -222,8 +222,16 @@ namespace MediaBrowser.XbmcMetadata.Parsers if (index != -1) { - var tmdbId = xml.Substring(index + srch.Length).TrimEnd('/').Split('-')[0]; - if (!string.IsNullOrWhiteSpace(tmdbId) && int.TryParse(tmdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + var tmdbId = xml.AsSpan().Slice(index + srch.Length).TrimEnd('/'); + index = tmdbId.IndexOf('-'); + if (index != -1) + { + tmdbId = tmdbId.Slice(0, index); + } + + if (!tmdbId.IsEmpty + && !tmdbId.IsWhiteSpace() + && int.TryParse(tmdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) { item.SetProviderId(MetadataProvider.Tmdb, value.ToString(UsCulture)); } @@ -237,8 +245,10 @@ namespace MediaBrowser.XbmcMetadata.Parsers if (index != -1) { - var tvdbId = xml.Substring(index + srch.Length).TrimEnd('/'); - if (!string.IsNullOrWhiteSpace(tvdbId) && int.TryParse(tvdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + var tvdbId = xml.AsSpan().Slice(index + srch.Length).TrimEnd('/'); + if (!tvdbId.IsEmpty + && !tvdbId.IsWhiteSpace() + && int.TryParse(tvdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) { item.SetProviderId(MetadataProvider.Tvdb, value.ToString(UsCulture)); } @@ -442,8 +452,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers { var val = reader.ReadElementContentAsString(); - var hasAspectRatio = item as IHasAspectRatio; - if (!string.IsNullOrWhiteSpace(val) && hasAspectRatio != null) + if (!string.IsNullOrWhiteSpace(val) + && item is IHasAspectRatio hasAspectRatio) { hasAspectRatio.AspectRatio = val; } @@ -162,6 +162,7 @@ The following sections describe some more advanced scenarios for running the ser It is not necessary to host the frontend web client as part of the backend server. Hosting these two components separately may be useful for frontend developers who would prefer to host the client in a separate webpack development server for a tighter development loop. See the [jellyfin-web](https://github.com/jellyfin/jellyfin-web#getting-started) repo for instructions on how to do this. -To instruct the server not to host the web content, there is a `nowebcontent` configuration flag that must be set. This can specified using the command line switch `--nowebcontent` or the environment variable `JELLYFIN_NOWEBCONTENT=true`. +To instruct the server not to host the web content, there is a `nowebclient` configuration flag that must be set. This can specified using the command line +switch `--nowebclient` or the environment variable `JELLYFIN_NOWEBCONTENT=true`. Since this is a common scenario, there is also a separate launch profile defined for Visual Studio called `Jellyfin.Server (nowebcontent)` that can be selected from the 'Start Debugging' dropdown in the main toolbar. diff --git a/SharedVersion.cs b/SharedVersion.cs index 6981c1ca9..d17074fc0 100644 --- a/SharedVersion.cs +++ b/SharedVersion.cs @@ -1,4 +1,4 @@ using System.Reflection; -[assembly: AssemblyVersion("10.6.0")] -[assembly: AssemblyFileVersion("10.6.0")] +[assembly: AssemblyVersion("10.7.0")] +[assembly: AssemblyFileVersion("10.7.0")] diff --git a/build.yaml b/build.yaml index 9e590e5a0..7c32d955c 100644 --- a/build.yaml +++ b/build.yaml @@ -1,7 +1,7 @@ --- # We just wrap `build` so this is really it name: "jellyfin" -version: "10.6.0" +version: "10.7.0" packages: - debian.amd64 - debian.arm64 diff --git a/bump_version b/bump_version index 46b7f86e0..1c943f691 100755 --- a/bump_version +++ b/bump_version @@ -4,6 +4,7 @@ set -o errexit set -o pipefail +set -o xtrace usage() { echo -e "bump_version - increase the shared version and generate changelogs" @@ -58,7 +59,7 @@ sed -i "s/${old_version_sed}/${new_version}/g" ${debian_equivs_file} debian_changelog_file="debian/changelog" debian_changelog_temp="$( mktemp )" # Create new temp file with our changelog -echo -e "jellyfin (${new_version_deb}) unstable; urgency=medium +echo -e "jellyfin-server (${new_version_deb}) unstable; urgency=medium * New upstream version ${new_version}; release changelog at https://github.com/jellyfin/jellyfin/releases/tag/v${new_version} diff --git a/debian/changelog b/debian/changelog index d38d03805..184143fe9 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,9 @@ +jellyfin-server (10.7.0-1) unstable; urgency=medium + + * Forthcoming stable release + + -- Jellyfin Packaging Team <packaging@jellyfin.org> Mon, 27 Jul 2020 19:09:45 -0400 + jellyfin-server (10.6.0-2) unstable; urgency=medium * Fix upgrade bug diff --git a/debian/metapackage/jellyfin b/debian/metapackage/jellyfin index 9a41eafae..026fcb821 100644 --- a/debian/metapackage/jellyfin +++ b/debian/metapackage/jellyfin @@ -5,7 +5,7 @@ Homepage: https://jellyfin.org Standards-Version: 3.9.2 Package: jellyfin -Version: 10.6.0 +Version: 10.7.0 Maintainer: Jellyfin Packaging Team <packaging@jellyfin.org> Depends: jellyfin-server, jellyfin-web Description: Provides the Jellyfin Free Software Media System diff --git a/fedora/jellyfin.spec b/fedora/jellyfin.spec index 9311864a6..b0a729a9e 100644 --- a/fedora/jellyfin.spec +++ b/fedora/jellyfin.spec @@ -7,7 +7,7 @@ %endif Name: jellyfin -Version: 10.6.0 +Version: 10.7.0 Release: 1%{?dist} Summary: The Free Software Media System License: GPLv3 @@ -139,6 +139,8 @@ fi %systemd_postun_with_restart jellyfin.service %changelog +* Mon Jul 27 2020 Jellyfin Packaging Team <packaging@jellyfin.org> +- Forthcoming stable release * Mon Mar 23 2020 Jellyfin Packaging Team <packaging@jellyfin.org> - Forthcoming stable release * Fri Oct 11 2019 Jellyfin Packaging Team <packaging@jellyfin.org> diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs index c145ddc9d..b4e6db8f3 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs @@ -9,17 +9,29 @@ namespace Jellyfin.Server.Implementations.Tests.Library [InlineData("/media/small.jpg", true)] [InlineData("/media/albumart.jpg", true)] [InlineData("/media/movie.sample.mp4", true)] + [InlineData("/media/movie/sample.mp4", true)] + [InlineData("/media/movie/sample/movie.mp4", true)] + [InlineData("/foo/sample/bar/baz.mkv", false)] + [InlineData("/media/movies/the sample/the sample.mkv", false)] + [InlineData("/media/movies/sampler.mkv", false)] [InlineData("/media/movies/#Recycle/test.txt", true)] [InlineData("/media/movies/#recycle/", true)] [InlineData("/media/movies/#recycle", true)] [InlineData("thumbs.db", true)] [InlineData(@"C:\media\movies\movie.avi", false)] - [InlineData("/media/.hiddendir/file.mp4", true)] + [InlineData("/media/.hiddendir/file.mp4", false)] [InlineData("/media/dir/.hiddenfile.mp4", true)] + [InlineData("/media/dir/._macjunk.mp4", true)] [InlineData("/volume1/video/Series/@eaDir", true)] [InlineData("/volume1/video/Series/@eaDir/file.txt", true)] [InlineData("/directory/@Recycle", true)] [InlineData("/directory/@Recycle/file.mp3", true)] + [InlineData("/media/movies/.@__thumb", true)] + [InlineData("/media/movies/.@__thumb/foo-bar-thumbnail.png", true)] + [InlineData("/media/music/Foo B.A.R./epic.flac", false)] + [InlineData("/media/music/Foo B.A.R", false)] + // This test is pending an upstream fix: https://github.com/dazinator/DotNet.Glob/issues/78 + // [InlineData("/media/music/Foo B.A.R.", false)] public void PathIgnored(string path, bool expected) { Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path)); |
