diff options
Diffstat (limited to 'Emby.Server.Implementations')
40 files changed, 436 insertions, 346 deletions
diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index 6edfad575..39524be1d 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -10,8 +10,6 @@ namespace Emby.Server.Implementations.AppBase /// </summary> public abstract class BaseApplicationPaths : IApplicationPaths { - private string _dataPath; - /// <summary> /// Initializes a new instance of the <see cref="BaseApplicationPaths"/> class. /// </summary> @@ -33,7 +31,7 @@ namespace Emby.Server.Implementations.AppBase CachePath = cacheDirectoryPath; WebPath = webDirectoryPath; - _dataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName; + DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName; } /// <summary> @@ -55,7 +53,7 @@ namespace Emby.Server.Implementations.AppBase /// Gets the folder path to the data directory. /// </summary> /// <value>The data directory.</value> - public string DataPath => _dataPath; + public string DataPath { get; } /// <inheritdoc /> public string VirtualDataPath => "%AppDataPath%"; diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 8518b1352..c9bf7f085 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -13,9 +13,7 @@ using System.Net; using System.Reflection; using System.Security.Cryptography.X509Certificates; using System.Threading.Tasks; -using Emby.Dlna; using Emby.Dlna.Main; -using Emby.Dlna.Ssdp; using Emby.Naming.Common; using Emby.Photos; using Emby.Server.Implementations.Channels; @@ -58,7 +56,6 @@ using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.ClientEvent; using MediaBrowser.Controller.Collections; using MediaBrowser.Controller.Configuration; -using MediaBrowser.Controller.Dlna; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; @@ -82,7 +79,6 @@ using MediaBrowser.LocalMetadata.Savers; using MediaBrowser.MediaEncoding.BdInfo; using MediaBrowser.MediaEncoding.Subtitles; using MediaBrowser.Model.Cryptography; -using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Globalization; using MediaBrowser.Model.IO; using MediaBrowser.Model.MediaInfo; @@ -101,7 +97,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Prometheus.DotNetRuntime; using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; @@ -133,7 +128,7 @@ namespace Emby.Server.Implementations /// <value>All concrete types.</value> private Type[] _allConcreteTypes; - private bool _disposed = false; + private bool _disposed; /// <summary> /// Initializes a new instance of the <see cref="ApplicationHost"/> class. @@ -184,26 +179,16 @@ namespace Emby.Server.Implementations public bool CoreStartupHasCompleted { get; private set; } - public virtual bool CanLaunchWebBrowser => Environment.UserInteractive - && !_startupOptions.IsService - && (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS()); - /// <summary> /// Gets the <see cref="INetworkManager"/> singleton instance. /// </summary> public INetworkManager NetManager { get; private set; } - /// <summary> - /// Gets a value indicating whether this instance has changes that require the entire application to restart. - /// </summary> - /// <value><c>true</c> if this instance has pending application restart; otherwise, <c>false</c>.</value> - public bool HasPendingRestart { get; private set; } - /// <inheritdoc /> - public bool IsShuttingDown { get; private set; } + public bool HasPendingRestart { get; private set; } /// <inheritdoc /> - public bool ShouldRestart { get; private set; } + public bool ShouldRestart { get; set; } /// <summary> /// Gets the logger. @@ -461,7 +446,7 @@ namespace Emby.Server.Implementations ConfigurationManager.AddParts(GetExports<IConfigurationFactory>()); - NetManager = new NetworkManager(ConfigurationManager, LoggerFactory.CreateLogger<NetworkManager>()); + NetManager = new NetworkManager(ConfigurationManager, _startupConfig, LoggerFactory.CreateLogger<NetworkManager>()); // Initialize runtime stat collection if (ConfigurationManager.Configuration.EnableMetrics) @@ -507,6 +492,8 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IFileSystem, ManagedFileSystem>(); serviceCollection.AddSingleton<IShortcutHandler, MbLinkShortcutHandler>(); + serviceCollection.AddScoped<ISystemManager, SystemManager>(); + serviceCollection.AddSingleton<TmdbClientManager>(); serviceCollection.AddSingleton(NetManager); @@ -572,8 +559,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<ISessionManager, SessionManager>(); - serviceCollection.AddSingleton<IDlnaManager, DlnaManager>(); - serviceCollection.AddSingleton<ICollectionManager, CollectionManager>(); serviceCollection.AddSingleton<IPlaylistManager, PlaylistManager>(); @@ -585,8 +570,6 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton<IUserViewManager, UserViewManager>(); - serviceCollection.AddSingleton<IDeviceDiscovery, DeviceDiscovery>(); - serviceCollection.AddSingleton<IChapterManager, ChapterManager>(); serviceCollection.AddSingleton<IEncodingManager, MediaEncoder.EncodingManager>(); @@ -850,24 +833,6 @@ namespace Emby.Server.Implementations } } - /// <inheritdoc /> - public void Restart() - { - ShouldRestart = true; - Shutdown(); - } - - /// <inheritdoc /> - public void Shutdown() - { - Task.Run(async () => - { - await Task.Delay(100).ConfigureAwait(false); - IsShuttingDown = true; - Resolve<IHostApplicationLifetime>().StopApplication(); - }); - } - /// <summary> /// Gets the composable part assemblies. /// </summary> @@ -923,49 +888,6 @@ namespace Emby.Server.Implementations protected abstract IEnumerable<Assembly> GetAssembliesWithPartsInternal(); - /// <summary> - /// Gets the system status. - /// </summary> - /// <param name="request">Where this request originated.</param> - /// <returns>SystemInfo.</returns> - public SystemInfo GetSystemInfo(HttpRequest request) - { - return new SystemInfo - { - HasPendingRestart = HasPendingRestart, - IsShuttingDown = IsShuttingDown, - Version = ApplicationVersionString, - WebSocketPortNumber = HttpPort, - CompletedInstallations = Resolve<IInstallationManager>().CompletedInstallations.ToArray(), - Id = SystemId, - ProgramDataPath = ApplicationPaths.ProgramDataPath, - WebPath = ApplicationPaths.WebPath, - LogPath = ApplicationPaths.LogDirectoryPath, - ItemsByNamePath = ApplicationPaths.InternalMetadataPath, - InternalMetadataPath = ApplicationPaths.InternalMetadataPath, - CachePath = ApplicationPaths.CachePath, - CanLaunchWebBrowser = CanLaunchWebBrowser, - TranscodingTempPath = ConfigurationManager.GetTranscodePath(), - ServerName = FriendlyName, - LocalAddress = GetSmartApiUrl(request), - SupportsLibraryMonitor = true, - PackageName = _startupOptions.PackageName - }; - } - - public PublicSystemInfo GetPublicSystemInfo(HttpRequest request) - { - return new PublicSystemInfo - { - Version = ApplicationVersionString, - ProductName = ApplicationProductName, - Id = SystemId, - ServerName = FriendlyName, - LocalAddress = GetSmartApiUrl(request), - StartupWizardCompleted = ConfigurationManager.CommonConfiguration.IsStartupWizardCompleted - }; - } - /// <inheritdoc/> public string GetSmartApiUrl(IPAddress remoteAddr) { @@ -983,7 +905,7 @@ namespace Emby.Server.Implementations /// <inheritdoc/> public string GetSmartApiUrl(HttpRequest request) { - // Return the host in the HTTP request as the API url + // Return the host in the HTTP request as the API URL if not configured otherwise if (ConfigurationManager.GetNetworkConfiguration().EnablePublishedServerUriByRequest) { int? requestPort = request.Host.Port; @@ -1018,7 +940,7 @@ namespace Emby.Server.Implementations public string GetApiUrlForLocalAccess(IPAddress ipAddress = null, bool allowHttps = true) { // With an empty source, the port will be null - var smart = NetManager.GetBindAddress(ipAddress, out _, true); + var smart = NetManager.GetBindAddress(ipAddress, out _, false); var scheme = !allowHttps ? Uri.UriSchemeHttp : null; int? port = !allowHttps ? HttpPort : null; return GetLocalApiUrl(smart, scheme, port); diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 961e225e9..8279acb05 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -371,8 +371,11 @@ namespace Emby.Server.Implementations.Channels Directory.CreateDirectory(Path.GetDirectoryName(path)); - await using FileStream createStream = File.Create(path); - await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false); + FileStream createStream = File.Create(path); + await using (createStream.ConfigureAwait(false)) + { + await JsonSerializer.SerializeAsync(createStream, mediaSources, _jsonOptions).ConfigureAwait(false); + } } /// <inheritdoc /> @@ -1156,7 +1159,7 @@ namespace Emby.Server.Implementations.Channels if (info.People is not null && info.People.Count > 0) { - _libraryManager.UpdatePeople(item, info.People); + await _libraryManager.UpdatePeopleAsync(item, info.People, cancellationToken).ConfigureAwait(false); } } else if (forceUpdate) diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 77cf4089b..e519364c2 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -3540,10 +3540,7 @@ namespace Emby.Server.Implementations.Data .Append(paramName) .Append("))) OR "); - if (statement is not null) - { - statement.TryBind(paramName, query.PersonIds[i]); - } + statement?.TryBind(paramName, query.PersonIds[i]); } clauseBuilder.Length -= Or.Length; @@ -4382,7 +4379,7 @@ namespace Emby.Server.Implementations.Data foreach (var videoType in query.VideoTypes) { - videoTypes.Add("data like '%\"VideoType\":\"" + videoType.ToString() + "\"%'"); + videoTypes.Add("data like '%\"VideoType\":\"" + videoType + "\"%'"); } whereClauses.Add("(" + string.Join(" OR ", videoTypes) + ")"); diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 6d27703bd..44b97e8b8 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -22,6 +22,7 @@ using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Trickplay; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; @@ -52,6 +53,7 @@ namespace Emby.Server.Implementations.Dto private readonly Lazy<ILiveTvManager> _livetvManagerFactory; private readonly ILyricManager _lyricManager; + private readonly ITrickplayManager _trickplayManager; public DtoService( ILogger<DtoService> logger, @@ -63,7 +65,8 @@ namespace Emby.Server.Implementations.Dto IApplicationHost appHost, IMediaSourceManager mediaSourceManager, Lazy<ILiveTvManager> livetvManagerFactory, - ILyricManager lyricManager) + ILyricManager lyricManager, + ITrickplayManager trickplayManager) { _logger = logger; _libraryManager = libraryManager; @@ -75,6 +78,7 @@ namespace Emby.Server.Implementations.Dto _mediaSourceManager = mediaSourceManager; _livetvManagerFactory = livetvManagerFactory; _lyricManager = lyricManager; + _trickplayManager = trickplayManager; } private ILiveTvManager LivetvManager => _livetvManagerFactory.Value; @@ -1059,6 +1063,11 @@ namespace Emby.Server.Implementations.Dto dto.Chapters = _itemRepo.GetChapters(item); } + if (options.ContainsField(ItemFields.Trickplay)) + { + dto.Trickplay = _trickplayManager.GetTrickplayManifest(item).GetAwaiter().GetResult(); + } + if (video.ExtraType.HasValue) { dto.ExtraType = video.ExtraType.Value.ToString(); diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 3aab0a5e9..80263c139 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -43,8 +43,6 @@ <TargetFramework>net7.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> - <!-- https://github.com/microsoft/ApplicationInsights-dotnet/issues/2047 --> - <NoWarn>AD0001</NoWarn> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index 54191649d..7e4994f1a 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -18,7 +18,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.EntryPoints { /// <summary> - /// Class UdpServerEntryPoint. + /// Class responsible for registering all UDP broadcast endpoints and their handlers. /// </summary> public sealed class UdpServerEntryPoint : IServerEntryPoint { @@ -35,14 +35,13 @@ namespace Emby.Server.Implementations.EntryPoints private readonly IConfiguration _config; private readonly IConfigurationManager _configurationManager; private readonly INetworkManager _networkManager; - private readonly bool _enableMultiSocketBinding; /// <summary> /// The UDP server. /// </summary> - private List<UdpServer> _udpServers; - private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); - private bool _disposed = false; + private readonly List<UdpServer> _udpServers; + private readonly CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); + private bool _disposed; /// <summary> /// Initializes a new instance of the <see cref="UdpServerEntryPoint" /> class. @@ -65,7 +64,6 @@ namespace Emby.Server.Implementations.EntryPoints _configurationManager = configurationManager; _networkManager = networkManager; _udpServers = new List<UdpServer>(); - _enableMultiSocketBinding = OperatingSystem.IsWindows() || OperatingSystem.IsLinux(); } /// <inheritdoc /> @@ -80,14 +78,16 @@ namespace Emby.Server.Implementations.EntryPoints try { - if (_enableMultiSocketBinding) + // Linux needs to bind to the broadcast addresses to get broadcast traffic + // Windows receives broadcast fine when binding to just the interface, it is unable to bind to broadcast addresses + if (OperatingSystem.IsLinux()) { - // Add global broadcast socket + // Add global broadcast listener var server = new UdpServer(_logger, _appHost, _config, IPAddress.Broadcast, PortNumber); server.Start(_cancellationTokenSource.Token); _udpServers.Add(server); - // Add bind address specific broadcast sockets + // Add bind address specific broadcast listeners // IPv6 is currently unsupported var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork); foreach (var intf in validInterfaces) @@ -102,9 +102,18 @@ namespace Emby.Server.Implementations.EntryPoints } else { - var server = new UdpServer(_logger, _appHost, _config, IPAddress.Any, PortNumber); - server.Start(_cancellationTokenSource.Token); - _udpServers.Add(server); + // Add bind address specific broadcast listeners + // IPv6 is currently unsupported + var validInterfaces = _networkManager.GetInternalBindAddresses().Where(i => i.AddressFamily == AddressFamily.InterNetwork); + foreach (var intf in validInterfaces) + { + var intfAddress = intf.Address; + _logger.LogDebug("Binding UDP server to {Address} on port {PortNumber}", intfAddress, PortNumber); + + var server = new UdpServer(_logger, _appHost, _config, intfAddress, PortNumber); + server.Start(_cancellationTokenSource.Token); + _udpServers.Add(server); + } } } catch (SocketException ex) @@ -119,7 +128,7 @@ namespace Emby.Server.Implementations.EntryPoints { if (_disposed) { - throw new ObjectDisposedException(this.GetType().Name); + throw new ObjectDisposedException(GetType().Name); } } diff --git a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs index 7f620d666..f83da566b 100644 --- a/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs +++ b/Emby.Server.Implementations/HttpServer/WebSocketConnection.cs @@ -12,7 +12,6 @@ using MediaBrowser.Controller.Net; using MediaBrowser.Controller.Net.WebSocketMessages; using MediaBrowser.Controller.Net.WebSocketMessages.Outbound; using MediaBrowser.Model.Session; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.HttpServer diff --git a/Emby.Server.Implementations/IO/FileRefresher.cs b/Emby.Server.Implementations/IO/FileRefresher.cs index 15b1836eb..e75cab64c 100644 --- a/Emby.Server.Implementations/IO/FileRefresher.cs +++ b/Emby.Server.Implementations/IO/FileRefresher.cs @@ -210,7 +210,6 @@ namespace Emby.Server.Implementations.IO DisposeTimer(); _disposed = true; - GC.SuppressFinalize(this); } } } diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 18b00ce0b..c380d67db 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.IO } // unc path - if (filePath.StartsWith("\\\\", StringComparison.Ordinal)) + if (filePath.StartsWith(@"\\", StringComparison.Ordinal)) { return filePath; } @@ -103,15 +103,17 @@ namespace Emby.Server.Implementations.IO return filePath; } + var filePathSpan = filePath.AsSpan(); + // relative path if (firstChar == '\\') { - filePath = filePath.Substring(1); + filePathSpan = filePathSpan.Slice(1); } try { - return Path.GetFullPath(Path.Combine(folderPath, filePath)); + return Path.GetFullPath(Path.Join(folderPath, filePathSpan)); } catch (ArgumentException) { diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index b0a4a4151..4f0983564 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -46,7 +46,6 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Tasks; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Episode = MediaBrowser.Controller.Entities.TV.Episode; using EpisodeInfo = Emby.Naming.TV.EpisodeInfo; @@ -839,19 +838,12 @@ namespace Emby.Server.Implementations.Library { var path = Person.GetPath(name); var id = GetItemByNameId<Person>(path); - if (GetItemById(id) is not Person item) + if (GetItemById(id) is Person item) { - item = new Person - { - Name = name, - Id = id, - DateCreated = DateTime.UtcNow, - DateModified = DateTime.UtcNow, - Path = path - }; + return item; } - return item; + return null; } /// <summary> @@ -1162,7 +1154,7 @@ namespace Emby.Server.Implementations.Library Name = Path.GetFileName(dir), Locations = _fileSystem.GetFilePaths(dir, false) - .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) + .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase)) .Select(i => { try @@ -2858,7 +2850,7 @@ namespace Emby.Server.Implementations.Library { var path = Path.Combine(virtualFolderPath, collectionType.ToString().ToLowerInvariant() + ".collection"); - File.WriteAllBytes(path, Array.Empty<byte>()); + await File.WriteAllBytesAsync(path, Array.Empty<byte>()).ConfigureAwait(false); } CollectionFolder.SaveLibraryOptions(virtualFolderPath, options); @@ -2900,9 +2892,18 @@ namespace Emby.Server.Implementations.Library var saveEntity = false; var personEntity = GetPerson(person.Name); - // if PresentationUniqueKey is empty it's likely a new item. - if (string.IsNullOrEmpty(personEntity.PresentationUniqueKey)) + if (personEntity is null) { + var path = Person.GetPath(person.Name); + personEntity = new Person() + { + Name = person.Name, + Id = GetItemByNameId<Person>(path), + DateCreated = DateTime.UtcNow, + DateModified = DateTime.UtcNow, + Path = path + }; + personEntity.PresentationUniqueKey = personEntity.CreatePresentationUniqueKey(); saveEntity = true; } @@ -3135,7 +3136,7 @@ namespace Emby.Server.Implementations.Library } var shortcut = _fileSystem.GetFilePaths(virtualFolderPath, true) - .Where(i => string.Equals(ShortcutFileExtension, Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) + .Where(i => Path.GetExtension(i.AsSpan()).Equals(ShortcutFileExtension, StringComparison.OrdinalIgnoreCase)) .FirstOrDefault(f => _appHost.ExpandVirtualPath(_fileSystem.ResolveShortcut(f)).Equals(mediaPath, StringComparison.OrdinalIgnoreCase)); if (!string.IsNullOrEmpty(shortcut)) diff --git a/Emby.Server.Implementations/Library/LiveStreamHelper.cs b/Emby.Server.Implementations/Library/LiveStreamHelper.cs index 936a08da8..59d705ace 100644 --- a/Emby.Server.Implementations/Library/LiveStreamHelper.cs +++ b/Emby.Server.Implementations/Library/LiveStreamHelper.cs @@ -48,15 +48,20 @@ namespace Emby.Server.Implementations.Library if (!string.IsNullOrEmpty(cacheKey)) { + FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath); try { - await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath); mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); // _logger.LogDebug("Found cached media info"); } - catch + catch (Exception ex) { + _logger.LogError(ex, "Error deserializing mediainfo cache"); + } + finally + { + await jsonStream.DisposeAsync().ConfigureAwait(false); } } @@ -84,10 +89,13 @@ namespace Emby.Server.Implementations.Library if (cacheFilePath is not null) { Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); - await using FileStream createStream = AsyncFile.OpenWrite(cacheFilePath); - await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); + FileStream createStream = AsyncFile.OpenWrite(cacheFilePath); + await using (createStream.ConfigureAwait(false)) + { + await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); + } - // _logger.LogDebug("Saved media info to {0}", cacheFilePath); + _logger.LogDebug("Saved media info to {0}", cacheFilePath); } } diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index c9a26a30f..91469dba9 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -625,17 +625,19 @@ namespace Emby.Server.Implementations.Library if (!string.IsNullOrEmpty(cacheKey)) { + FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath); try { - await using FileStream jsonStream = AsyncFile.OpenRead(cacheFilePath); mediaInfo = await JsonSerializer.DeserializeAsync<MediaInfo>(jsonStream, _jsonOptions, cancellationToken).ConfigureAwait(false); - - // _logger.LogDebug("Found cached media info"); } catch (Exception ex) { _logger.LogDebug(ex, "_jsonSerializer.DeserializeFromFile threw an exception."); } + finally + { + await jsonStream.DisposeAsync().ConfigureAwait(false); + } } if (mediaInfo is null) @@ -664,8 +666,11 @@ namespace Emby.Server.Implementations.Library if (cacheFilePath is not null) { Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)); - await using FileStream createStream = File.Create(cacheFilePath); - await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); + FileStream createStream = File.Create(cacheFilePath); + await using (createStream.ConfigureAwait(false)) + { + await JsonSerializer.SerializeAsync(createStream, mediaInfo, _jsonOptions, cancellationToken).ConfigureAwait(false); + } // _logger.LogDebug("Saved media info to {0}", cacheFilePath); } diff --git a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs index a74f82475..862f144e6 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Audio/AudioResolver.cs @@ -94,9 +94,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio if (AudioFileParser.IsAudioFile(args.Path, _namingOptions)) { - var extension = Path.GetExtension(args.Path); + var extension = Path.GetExtension(args.Path.AsSpan()); - if (string.Equals(extension, ".cue", StringComparison.OrdinalIgnoreCase)) + if (extension.Equals(".cue", StringComparison.OrdinalIgnoreCase)) { // if audio file exists of same name, return null return null; @@ -128,7 +128,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Audio if (item is not null) { - item.IsShortcut = string.Equals(extension, ".strm", StringComparison.OrdinalIgnoreCase); + item.IsShortcut = extension.Equals(".strm", StringComparison.OrdinalIgnoreCase); item.IsInMixedFolder = true; } diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index 381796d0e..779cfd5be 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -263,7 +263,7 @@ namespace Emby.Server.Implementations.Library.Resolvers return false; } - return directoryService.GetFilePaths(fullPath).Any(i => string.Equals(Path.GetExtension(i), ".vob", StringComparison.OrdinalIgnoreCase)); + return directoryService.GetFilePaths(fullPath).Any(i => Path.GetExtension(i.AsSpan()).Equals(".vob", StringComparison.OrdinalIgnoreCase)); } /// <summary> diff --git a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs index 042422c6f..73861ff59 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Books/BookResolver.cs @@ -32,9 +32,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books return GetBook(args); } - var extension = Path.GetExtension(args.Path); + var extension = Path.GetExtension(args.Path.AsSpan()); - if (extension is not null && _validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) + if (_validExtensions.Contains(extension, StringComparison.OrdinalIgnoreCase)) { // It's a book return new Book @@ -51,12 +51,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Books { var bookFiles = args.FileSystemChildren.Where(f => { - var fileExtension = Path.GetExtension(f.FullName) - ?? string.Empty; + var fileExtension = Path.GetExtension(f.FullName.AsSpan()); return _validExtensions.Contains( fileExtension, - StringComparer.OrdinalIgnoreCase); + StringComparison.OrdinalIgnoreCase); }).ToList(); // Don't return a Book if there is more (or less) than one document in the directory diff --git a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs index b77c6b204..c860391fc 100644 --- a/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/PhotoResolver.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; using Emby.Naming.Common; diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index e9538a5c9..858c5b281 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV var resolver = new Naming.TV.EpisodeResolver(namingOptions); var folderName = System.IO.Path.GetFileName(path); - var testPath = "\\\\test\\" + folderName; + var testPath = @"\\test\" + folderName; var episodeInfo = resolver.Resolve(testPath, true); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index b9d0f170a..74b62ca3f 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -1851,7 +1851,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return; } - await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) + var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None); + await using (stream.ConfigureAwait(false)) { var settings = new XmlWriterSettings { @@ -1860,7 +1861,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV Async = true }; - await using (var writer = XmlWriter.Create(stream, settings)) + var writer = XmlWriter.Create(stream, settings); + await using (writer.ConfigureAwait(false)) { await writer.WriteStartDocumentAsync(true).ConfigureAwait(false); await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false); @@ -1914,7 +1916,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return; } - await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) + var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None); + await using (stream.ConfigureAwait(false)) { var settings = new XmlWriterSettings { @@ -1927,7 +1930,8 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var isSeriesEpisode = timer.IsProgramSeries; - await using (var writer = XmlWriter.Create(stream, settings)) + var writer = XmlWriter.Create(stream, settings); + await using (writer.ConfigureAwait(false)) { await writer.WriteStartDocumentAsync(true).ConfigureAwait(false); @@ -1965,7 +1969,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } else { - await writer.WriteStartElementAsync(null, "movie", null); + await writer.WriteStartElementAsync(null, "movie", null).ConfigureAwait(false); if (!string.IsNullOrWhiteSpace(item.Name)) { diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index 7645c6c52..6b0520ad0 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -106,8 +106,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings options.Content = JsonContent.Create(requestList, options: _jsonOptions); options.Headers.TryAddWithoutValidation("token", token); using var response = await Send(options, true, info, cancellationToken).ConfigureAwait(false); - await using var responseStream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var dailySchedules = await JsonSerializer.DeserializeAsync<IReadOnlyList<DayDto>>(responseStream, _jsonOptions, cancellationToken).ConfigureAwait(false); + var dailySchedules = await response.Content.ReadFromJsonAsync<IReadOnlyList<DayDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false); if (dailySchedules is null) { return Array.Empty<ProgramInfo>(); @@ -122,8 +121,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings programRequestOptions.Content = JsonContent.Create(programIds, options: _jsonOptions); using var innerResponse = await Send(programRequestOptions, true, info, cancellationToken).ConfigureAwait(false); - await using var innerResponseStream = await innerResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var programDetails = await JsonSerializer.DeserializeAsync<IReadOnlyList<ProgramDetailsDto>>(innerResponseStream, _jsonOptions, cancellationToken).ConfigureAwait(false); + var programDetails = await innerResponse.Content.ReadFromJsonAsync<IReadOnlyList<ProgramDetailsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false); if (programDetails is null) { return Array.Empty<ProgramInfo>(); @@ -482,8 +480,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings try { using var innerResponse2 = await Send(message, true, info, cancellationToken).ConfigureAwait(false); - await using var response = await innerResponse2.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - return await JsonSerializer.DeserializeAsync<IReadOnlyList<ShowImagesDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false); + return await innerResponse2.Content.ReadFromJsonAsync<IReadOnlyList<ShowImagesDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { @@ -510,10 +507,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings try { using var httpResponse = await Send(options, false, info, cancellationToken).ConfigureAwait(false); - await using var response = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - - var root = await JsonSerializer.DeserializeAsync<IReadOnlyList<HeadendsDto>>(response, _jsonOptions, cancellationToken).ConfigureAwait(false); - + var root = await httpResponse.Content.ReadFromJsonAsync<IReadOnlyList<HeadendsDto>>(_jsonOptions, cancellationToken).ConfigureAwait(false); if (root is not null) { foreach (HeadendsDto headend in root) @@ -649,8 +643,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings using var response = await Send(options, false, null, cancellationToken).ConfigureAwait(false); response.EnsureSuccessStatusCode(); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var root = await JsonSerializer.DeserializeAsync<TokenDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); + var root = await response.Content.ReadFromJsonAsync<TokenDto>(_jsonOptions, cancellationToken).ConfigureAwait(false); if (string.Equals(root?.Message, "OK", StringComparison.Ordinal)) { _logger.LogInformation("Authenticated with Schedules Direct token: {Token}", root.Token); @@ -691,10 +684,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings { using var httpResponse = await Send(options, false, null, cancellationToken).ConfigureAwait(false); httpResponse.EnsureSuccessStatusCode(); - await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - using var response = httpResponse.Content; - var root = await JsonSerializer.DeserializeAsync<LineupsDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); - + var root = await httpResponse.Content.ReadFromJsonAsync<LineupsDto>(_jsonOptions, cancellationToken).ConfigureAwait(false); return root?.Lineups.Any(i => string.Equals(info.ListingsId, i.Lineup, StringComparison.OrdinalIgnoreCase)) ?? false; } catch (HttpRequestException ex) @@ -748,8 +738,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings options.Headers.TryAddWithoutValidation("token", token); using var httpResponse = await Send(options, true, info, cancellationToken).ConfigureAwait(false); - await using var stream = await httpResponse.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var root = await JsonSerializer.DeserializeAsync<ChannelDto>(stream, _jsonOptions, cancellationToken).ConfigureAwait(false); + var root = await httpResponse.Content.ReadFromJsonAsync<ChannelDto>(_jsonOptions, cancellationToken).ConfigureAwait(false); if (root is null) { return new List<ChannelInfo>(); diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs index 1721be9e2..ff25ee585 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/BaseTunerHost.cs @@ -17,7 +17,6 @@ using MediaBrowser.Controller.LiveTv; using MediaBrowser.Model.Dto; using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.LiveTv.TunerHosts diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 7e588f681..8cd0c4ffb 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -9,6 +9,7 @@ using System.IO; using System.Linq; using System.Net; using System.Net.Http; +using System.Net.Http.Json; using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -27,7 +28,6 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; using MediaBrowser.Model.Net; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun @@ -76,13 +76,10 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun var model = await GetModelInfo(info, false, cancellationToken).ConfigureAwait(false); using var response = await _httpClientFactory.CreateClient(NamedClient.Default).GetAsync(model.LineupURL ?? model.BaseURL + "/lineup.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var lineup = await JsonSerializer.DeserializeAsync<List<Channels>>(stream, _jsonOptions, cancellationToken) - .ConfigureAwait(false) ?? new List<Channels>(); - + var lineup = await response.Content.ReadFromJsonAsync<IEnumerable<Channels>>(_jsonOptions, cancellationToken).ConfigureAwait(false) ?? Enumerable.Empty<Channels>(); if (info.ImportFavoritesOnly) { - lineup = lineup.Where(i => i.Favorite).ToList(); + lineup = lineup.Where(i => i.Favorite); } return lineup.Where(i => !i.DRM).ToList(); @@ -129,9 +126,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun .GetAsync(GetApiUrl(info) + "/discover.json", HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); response.EnsureSuccessStatusCode(); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - var discoverResponse = await JsonSerializer.DeserializeAsync<DiscoverResponse>(stream, _jsonOptions, cancellationToken) - .ConfigureAwait(false); + var discoverResponse = await response.Content.ReadFromJsonAsync<DiscoverResponse>(_jsonOptions, cancellationToken).ConfigureAwait(false); if (!string.IsNullOrEmpty(cacheKey)) { @@ -175,34 +170,37 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun using var response = await _httpClientFactory.CreateClient(NamedClient.Default) .GetAsync(string.Format(CultureInfo.InvariantCulture, "{0}/tuners.html", GetApiUrl(info)), HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); - await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); - using var sr = new StreamReader(stream, System.Text.Encoding.UTF8); var tuners = new List<LiveTvTunerInfo>(); - await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false)) + var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false); + await using (stream.ConfigureAwait(false)) { - string stripedLine = StripXML(line); - if (stripedLine.Contains("Channel", StringComparison.Ordinal)) + using var sr = new StreamReader(stream, System.Text.Encoding.UTF8); + await foreach (var line in sr.ReadAllLinesAsync().ConfigureAwait(false)) { - LiveTvTunerStatus status; - var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); - var name = stripedLine.Substring(0, index - 1); - var currentChannel = stripedLine.Substring(index + 7); - if (string.Equals(currentChannel, "none", StringComparison.Ordinal)) + string stripedLine = StripXML(line); + if (stripedLine.Contains("Channel", StringComparison.Ordinal)) { - status = LiveTvTunerStatus.LiveTv; - } - else - { - status = LiveTvTunerStatus.Available; - } + LiveTvTunerStatus status; + var index = stripedLine.IndexOf("Channel", StringComparison.OrdinalIgnoreCase); + var name = stripedLine.Substring(0, index - 1); + var currentChannel = stripedLine.Substring(index + 7); + if (string.Equals(currentChannel, "none", StringComparison.Ordinal)) + { + status = LiveTvTunerStatus.LiveTv; + } + else + { + status = LiveTvTunerStatus.Available; + } - tuners.Add(new LiveTvTunerInfo - { - Name = name, - SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, - ProgramName = currentChannel, - Status = status - }); + tuners.Add(new LiveTvTunerInfo + { + Name = name, + SourceType = string.IsNullOrWhiteSpace(model.ModelNumber) ? Name : model.ModelNumber, + ProgramName = currentChannel, + Status = status + }); + } } } diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs index a8b090635..68383a554 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs @@ -44,8 +44,6 @@ 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) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index 613ea117f..db5e81df5 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Globalization; -using System.IO; using System.Linq; using System.Net.Http; using System.Threading; @@ -22,7 +21,6 @@ using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using MediaBrowser.Model.LiveTv; using MediaBrowser.Model.MediaInfo; -using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json index 15088384c..496ecabd3 100644 --- a/Emby.Server.Implementations/Localization/Core/en-US.json +++ b/Emby.Server.Implementations/Localization/Core/en-US.json @@ -112,6 +112,8 @@ "TaskCleanLogsDescription": "Deletes log files that are more than {0} days old.", "TaskRefreshPeople": "Refresh People", "TaskRefreshPeopleDescription": "Updates metadata for actors and directors in your media library.", + "TaskRefreshTrickplayImages": "Generate Trickplay Images", + "TaskRefreshTrickplayImagesDescription": "Creates trickplay previews for videos in enabled libraries.", "TaskUpdatePlugins": "Update Plugins", "TaskUpdatePluginsDescription": "Downloads and installs updates for plugins that are configured to update automatically.", "TaskCleanTranscode": "Clean Transcode Directory", diff --git a/Emby.Server.Implementations/Localization/Core/fo.json b/Emby.Server.Implementations/Localization/Core/fo.json new file mode 100644 index 000000000..40aa5f71a --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/fo.json @@ -0,0 +1,18 @@ +{ + "Artists": "Listafólk", + "Collections": "Søvn", + "Default": "Sjálvgildi", + "DeviceOfflineWithName": "{0} hevur slitið sambandið", + "External": "Ytri", + "Genres": "Greinar", + "Albums": "Album", + "AppDeviceValues": "App: {0}, Eind: {1}", + "Application": "Nýtsluskipan", + "Books": "Bøkur", + "Channels": "Rásir", + "ChapterNameValue": "Kapittul {0}", + "DeviceOnlineWithName": "{0} er sambundið", + "Favorites": "Yndis", + "Folders": "Mappur", + "Forced": "Kravt" +} diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json index a40f49506..0f1f0b3d2 100644 --- a/Emby.Server.Implementations/Localization/Core/is.json +++ b/Emby.Server.Implementations/Localization/Core/is.json @@ -13,8 +13,8 @@ "HeaderFavoriteArtists": "Uppáhalds Listamenn", "HeaderFavoriteAlbums": "Uppáhalds Plötur", "HeaderContinueWatching": "Halda áfram að horfa", - "HeaderAlbumArtists": "Höfundur plötu", - "Genres": "Tegundir", + "HeaderAlbumArtists": "Listamaður á umslagi", + "Genres": "Stefnur", "Folders": "Möppur", "Favorites": "Uppáhalds", "FailedLoginAttemptWithUserName": "{0} reyndi að auðkenna sig", @@ -22,32 +22,32 @@ "DeviceOfflineWithName": "{0} hefur aftengst", "Collections": "Söfn", "ChapterNameValue": "Kafli {0}", - "Channels": "Stöðvar", - "CameraImageUploadedFrom": "Ný ljósmynd frá myndavél hefur verið hlaðið upp frá {0}", + "Channels": "Rásir", + "CameraImageUploadedFrom": "{0} hefur hlaðið upp nýrri ljósmynd úr myndavél sinni", "Books": "Bækur", - "AuthenticationSucceededWithUserName": "{0} auðkenning tókst", - "Artists": "Listamaður", + "AuthenticationSucceededWithUserName": "Auðkenning fyrir {0} tókst", + "Artists": "Listamenn", "Application": "Forrit", "AppDeviceValues": "Snjallforrit: {0}, Tæki: {1}", "Albums": "Plötur", - "Plugin": "Viðbót", - "Photos": "Myndir", - "NotificationOptionVideoPlaybackStopped": "Myndbandafspilun stöðvuð", - "NotificationOptionVideoPlayback": "Myndbandafspilun hafin", + "Plugin": "Viðbótarvirkni", + "Photos": "Ljósmyndir", + "NotificationOptionVideoPlaybackStopped": "Myndbandsafspilun stöðvuð", + "NotificationOptionVideoPlayback": "Myndbandsafspilun hafin", "NotificationOptionUserLockedOut": "Notandi læstur úti", - "NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynileg", - "NotificationOptionPluginUpdateInstalled": "Viðbótar uppfærsla uppsett", - "NotificationOptionPluginUninstalled": "Viðbót fjarlægð", - "NotificationOptionPluginInstalled": "Viðbót sett upp", + "NotificationOptionServerRestartRequired": "Endurræsing þjóns er nauðsynleg", + "NotificationOptionPluginUpdateInstalled": "Uppfærslu á viðbótarvirkni lokið", + "NotificationOptionPluginUninstalled": "Viðbótarvirkni fjarlægð", + "NotificationOptionPluginInstalled": "Viðbótarvirkni sett upp", "NotificationOptionPluginError": "Bilun í viðbót", "NotificationOptionInstallationFailed": "Uppsetning tókst ekki", - "NotificationOptionCameraImageUploaded": "Myndavélarmynd hlaðið upp", + "NotificationOptionCameraImageUploaded": "Ljósmynd hlaðið upp", "NotificationOptionAudioPlaybackStopped": "Hljóðafspilun stöðvuð", "NotificationOptionAudioPlayback": "Hljóðafspilun hafin", "NotificationOptionApplicationUpdateInstalled": "Uppfærsla uppsett", "NotificationOptionApplicationUpdateAvailable": "Uppfærsla í boði", - "NameSeasonUnknown": "Sería óþekkt", - "NameSeasonNumber": "Sería {0}", + "NameSeasonUnknown": "Þáttaröð óþekkt", + "NameSeasonNumber": "Þáttaröð {0}", "MixedContent": "Blandað efni", "MessageServerConfigurationUpdated": "Stillingar þjóns hafa verið uppfærðar", "MessageApplicationUpdatedTo": "Jellyfin þjónn hefur verið uppfærður í {0}", @@ -57,24 +57,24 @@ "User": "Notandi", "System": "Kerfi", "NotificationOptionNewLibraryContent": "Nýju efni bætt við", - "NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er fáanleg til niðurhals.", + "NewVersionIsAvailable": "Ný útgáfa af Jellyfin þjón er tilbúin til niðurhals.", "NameInstallFailed": "{0} uppsetning mistókst", "MusicVideos": "Tónlistarmyndbönd", "Music": "Tónlist", "Movies": "Kvikmyndir", "UserDeletedWithName": "Notanda {0} hefur verið eytt", "UserCreatedWithName": "Notandi {0} hefur verið stofnaður", - "TvShows": "Þættir", + "TvShows": "Sjónvarpsþættir", "Sync": "Samstilla", "Songs": "Lög", - "ServerNameNeedsToBeRestarted": "{0} þarf að endurræsa", + "ServerNameNeedsToBeRestarted": "{0} þarf að vera endurræstur", "ScheduledTaskStartedWithName": "{0} hafin", "ScheduledTaskFailedWithName": "{0} mistókst", "PluginUpdatedWithName": "{0} var uppfært", "PluginUninstalledWithName": "{0} var fjarlægt", "PluginInstalledWithName": "{0} var sett upp", "NotificationOptionTaskFailed": "Tímasett verkefni mistókst", - "StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að hlaðast. Vinsamlega prufaðu aftur fljótlega.", + "StartupEmbyServerIsLoading": "Jellyfin netþjónnin er að ræsa sig upp. Vinsamlegast reyndu aftur fljótlega.", "VersionNumber": "Útgáfa {0}", "ValueHasBeenAddedToLibrary": "{0} hefur verið bætt við í gagnasafnið þitt", "UserStoppedPlayingItemWithValues": "{0} hefur lokið spilunar af {1} á {2}", @@ -83,14 +83,14 @@ "UserPasswordChangedWithName": "Lykilorði fyrir notandann {0} hefur verið breytt", "UserOnlineFromDevice": "{0} hefur verið virkur síðan {1}", "UserOfflineFromDevice": "{0} hefur aftengst frá {1}", - "UserLockedOutWithName": "Notanda {0} hefur verið heflaður aðgangur", - "UserDownloadingItemWithValues": "{0} Hleður niður {1}", + "UserLockedOutWithName": "Notandi {0} hefur verið læstur úti", + "UserDownloadingItemWithValues": "{0} hleður niður {1}", "SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}", - "ProviderValue": "Veitandi: {0}", + "ProviderValue": "Efnisveita: {0}", "MessageNamedServerConfigurationUpdatedWithValue": "Stilling {0} hefur verið uppfærð á netþjón", - "ValueSpecialEpisodeName": "Sérstakt - {0}", - "Shows": "Sýningar", - "Playlists": "Spilunarlisti", + "ValueSpecialEpisodeName": "Sérstaktur - {0}", + "Shows": "Þættir", + "Playlists": "Efnisskrár", "TaskRefreshChannelsDescription": "Endurhlaða upplýsingum netrása.", "TaskRefreshChannels": "Endurhlaða Rásir", "TaskCleanTranscodeDescription": "Eyða umkóðuðum skrám sem eru meira en einum degi eldri.", @@ -116,5 +116,12 @@ "TaskCleanLogsDescription": "Eyðir færslu skrám sem eru meira en {0} gömul.", "TaskCleanLogs": "Hreinsa færslu skrá", "TaskDownloadMissingSubtitlesDescription": "Leitar á netinu að texta sem vantar miðað við uppsetningu lýsigagna.", - "HearingImpaired": "Heyrnarskertur" + "HearingImpaired": "Heyrnarskertur", + "TaskOptimizeDatabaseDescription": "Þjappar gagnagrunni og bætir við lausu diskaplássi. Að keyra þessa aðgerð eftir skönnun safnsins, eða eftir einhverjar breytingar sem fela í sér gagnagrunnsbreytingar, gætu aukið hraðvirkni.", + "TaskKeyframeExtractor": "Lykilrammaplokkari", + "TaskKeyframeExtractorDescription": "Plokkar lykilramma úr myndbandsskrám til að búa til nákvæmari HLS uppskiptingarlista. Þetta verk getur tekið langan tíma.", + "TaskRefreshChapterImages": "Plokka kafla-myndir", + "TaskCleanActivityLogDescription": "Eyðir virkniskráningarfærslum sem hafa náð settum hámarksaldri.", + "Forced": "Þvingað", + "External": "Útvær" } diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json index f7b24412a..83a000014 100644 --- a/Emby.Server.Implementations/Localization/Core/lv.json +++ b/Emby.Server.Implementations/Localization/Core/lv.json @@ -1,7 +1,7 @@ { "ServerNameNeedsToBeRestarted": "{0} ir vajadzīgs restarts", "NotificationOptionTaskFailed": "Plānota uzdevuma kļūme", - "HeaderRecordingGroups": "Ierakstu Grupas", + "HeaderRecordingGroups": "Ierakstu grupas", "UserPolicyUpdatedWithName": "Lietotāju politika atjaunota priekš {0}", "SubtitleDownloadFailureFromForItem": "Subtitru lejupielāde no {0} priekš {1} neizdevās", "NotificationOptionVideoPlaybackStopped": "Video atskaņošana apturēta", @@ -14,7 +14,7 @@ "Photos": "Attēli", "NotificationOptionUserLockedOut": "Lietotājs bloķēts", "LabelRunningTimeValue": "Garums: {0}", - "Inherit": "Mantot", + "Inherit": "Pārmantot", "AppDeviceValues": "Lietotne: {0}, Ierīce: {1}", "VersionNumber": "Versija {0}", "ValueHasBeenAddedToLibrary": "{0} ir ticis pievienots jūsu multvides bibliotēkai", @@ -28,7 +28,7 @@ "UserDeletedWithName": "Lietotājs {0} ir izdzēsts", "UserCreatedWithName": "Lietotājs {0} ir ticis izveidots", "User": "Lietotājs", - "TvShows": "TV Raidījumi", + "TvShows": "TV raidījumi", "Sync": "Sinhronizācija", "System": "Sistēma", "StartupEmbyServerIsLoading": "Jellyfin Serveris lādējas. Lūdzu mēģiniet vēlreiz pēc brīža.", @@ -38,11 +38,11 @@ "PluginUninstalledWithName": "{0} tika noņemts", "PluginInstalledWithName": "{0} tika uzstādīts", "Plugin": "Paplašinājums", - "Playlists": "Atskaņošanas Saraksti", + "Playlists": "Atskaņošanas saraksti", "MixedContent": "Jaukts saturs", - "HomeVideos": "Mājas Video", + "HomeVideos": "Mājas video", "HeaderNextUp": "Nākamais", - "ChapterNameValue": "Nodaļa {0}", + "ChapterNameValue": "{0}. nodaļa", "Application": "Lietotne", "NotificationOptionServerRestartRequired": "Vajadzīgs servera restarts", "NotificationOptionPluginUpdateInstalled": "Paplašinājuma atjauninājums uzstādīts", @@ -56,14 +56,14 @@ "NotificationOptionApplicationUpdateInstalled": "Lietotnes atjauninājums uzstādīts", "NotificationOptionApplicationUpdateAvailable": "Lietotnes atjauninājums pieejams", "NewVersionIsAvailable": "Lejupielādei ir pieejama jauna Jellyfin Server versija.", - "NameSeasonUnknown": "Nezināma Sezona", - "NameSeasonNumber": "Sezona {0}", + "NameSeasonUnknown": "Nezināma sezona", + "NameSeasonNumber": "{0}. sezona", "NameInstallFailed": "{0} instalācija neizdevās", "MusicVideos": "Mūzikas video", "Music": "Mūzika", "Movies": "Filmas", "MessageServerConfigurationUpdated": "Servera konfigurācija ir tikusi atjaunota", - "MessageNamedServerConfigurationUpdatedWithValue": "Servera konfigurācijas sadaļa {0} ir tikusi atjaunota", + "MessageNamedServerConfigurationUpdatedWithValue": "Servera konfigurācijas sadaļa {0} tika atjaunota", "MessageApplicationUpdatedTo": "Jellyfin Server ir ticis atjaunots uz {0}", "MessageApplicationUpdated": "Jellyfin Server ir ticis atjaunots", "Latest": "Jaunākais", @@ -71,57 +71,57 @@ "ItemRemovedWithName": "{0} tika noņemts no bibliotēkas", "ItemAddedWithName": "{0} tika pievienots bibliotēkai", "HeaderLiveTV": "Tiešraides TV", - "HeaderContinueWatching": "Turpināt Skatīšanos", - "HeaderAlbumArtists": "Albumu Izpildītāji", + "HeaderContinueWatching": "Turpināt skatīšanos", + "HeaderAlbumArtists": "Albumu izpildītāji", "Genres": "Žanri", "Folders": "Mapes", - "Favorites": "Favorīti", - "FailedLoginAttemptWithUserName": "Neizdevies pieslēgšanās mēģinājums no {0}", - "DeviceOnlineWithName": "{0} ir pievienojies", - "DeviceOfflineWithName": "{0} ir atvienojies", + "Favorites": "Izlase", + "FailedLoginAttemptWithUserName": "Neizdevies ieiešanas mēģinājums no {0}", + "DeviceOnlineWithName": "Savienojums ar {0} ir izveidots", + "DeviceOfflineWithName": "Savienojums ar {0} ir pārtraukts", "Collections": "Kolekcijas", "Channels": "Kanāli", - "CameraImageUploadedFrom": "Jauns kameras attēls ir ticis augšupielādēts no {0}", + "CameraImageUploadedFrom": "Jauns kameras attēls tika augšupielādēts no {0}", "Books": "Grāmatas", "Artists": "Izpildītāji", "Albums": "Albumi", "ProviderValue": "Provider: {0}", - "HeaderFavoriteSongs": "Dziesmu Favorīti", - "HeaderFavoriteShows": "Raidījumu Favorīti", - "HeaderFavoriteEpisodes": "Episožu Favorīti", - "HeaderFavoriteArtists": "Izpildītāju Favorīti", - "HeaderFavoriteAlbums": "Albumu Favorīti", - "TaskCleanCacheDescription": "Nodzēš keša datnes, kas vairs nav sistēmai vajadzīgas.", - "TaskRefreshChapterImages": "Izvilkt Nodaļu Attēlus", + "HeaderFavoriteSongs": "Dziesmu izlase", + "HeaderFavoriteShows": "Raidījumu izlase", + "HeaderFavoriteEpisodes": "Sēriju izlase", + "HeaderFavoriteArtists": "Izpildītāju izlase", + "HeaderFavoriteAlbums": "Albumu izlase", + "TaskCleanCacheDescription": "Nodzēš kešatmiņas datnes, kas vairs nav sistēmai vajadzīgas.", + "TaskRefreshChapterImages": "Izvilkt nodaļu attēlus", "TasksApplicationCategory": "Lietotne", "TasksLibraryCategory": "Bibliotēka", "TaskDownloadMissingSubtitlesDescription": "Internetā meklē trūkstošus subtitrus balstoties uz metadatu uzstādījumiem.", - "TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošus subtitrus", + "TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošos subtitrus", "TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.", - "TaskRefreshChannels": "Atjaunot Kanālus", - "TaskCleanTranscodeDescription": "Izdzēš trans-kodēšanas datnes, kas ir vecākas par vienu dienu.", - "TaskCleanTranscode": "Iztīrīt Trans-kodēšanas Mapi", + "TaskRefreshChannels": "Atjaunot kanālus", + "TaskCleanTranscodeDescription": "Izdzēš transkodēšanas datnes, kas ir senākas par vienu dienu.", + "TaskCleanTranscode": "Iztīrīt transkodēšanas mapi", "TaskUpdatePluginsDescription": "Lejupielādē un uzstāda atjauninājumus paplašinājumiem, kam ir uzstādīta automātiskā atjaunināšana.", - "TaskUpdatePlugins": "Atjaunot Paplašinājumus", + "TaskUpdatePlugins": "Atjaunot paplašinājumus", "TaskRefreshPeopleDescription": "Atjauno metadatus aktieriem un direktoriem jūsu multivides bibliotēkā.", - "TaskRefreshPeople": "Atjaunot Cilvēkus", - "TaskCleanLogsDescription": "Nodzēš log datnes, kas ir vairāk par {0} dienām vecas.", - "TaskCleanLogs": "Iztīrīt Logdatņu Mapi", + "TaskRefreshPeople": "Atjaunot cilvēkus", + "TaskCleanLogsDescription": "Nodzēš logdatnes, kas ir senākas par {0} dienām.", + "TaskCleanLogs": "Iztīrīt logdatņu mapi", "TaskRefreshLibraryDescription": "Skenē jūsu multivides bibliotēku, lai atrastu jaunas datnes, un atsvaidzina metadatus.", - "TaskRefreshLibrary": "Skenēt Multivides Bibliotēku", + "TaskRefreshLibrary": "Skenēt multivides bibliotēku", "TaskRefreshChapterImagesDescription": "Izveido sīktēlus priekš video ar sadaļām.", - "TaskCleanCache": "Iztīrīt Kešošanas Mapi", - "TasksChannelsCategory": "Interneta Kanāli", + "TaskCleanCache": "Iztīrīt kešatmiņas mapi", + "TasksChannelsCategory": "Interneta kanāli", "TasksMaintenanceCategory": "Apkope", - "Forced": "Piespiests", + "Forced": "Piespiedu", "TaskCleanActivityLogDescription": "Nodzēš darbību žurnāla ierakstus, kuri ir vecāki par doto vecumu.", - "TaskCleanActivityLog": "Notīrīt Darbību Žurnālu", + "TaskCleanActivityLog": "Notīrīt darbību žurnālu", "Undefined": "Nenoteikts", "Default": "Noklusējuma", - "TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Uzdevum palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.", + "TaskOptimizeDatabaseDescription": "Saspiež datubāzi un atbrīvo atmiņu. Šī uzdevuma palaišana pēc bibliotēku skenēšanas vai citām, ar datubāzi saistītām, izmaiņām iespējams uzlabos ātrdarbību.", "TaskOptimizeDatabase": "Optimizēt datubāzi", "External": "Ārējais", "HearingImpaired": "Ar dzirdes traucējumiem", - "TaskKeyframeExtractor": "Atslēgkadru Ekstraktors", + "TaskKeyframeExtractor": "Atslēgkadru ekstraktors", "TaskKeyframeExtractorDescription": "Ekstraktē atslēgkadrus no video failiem lai izveidotu precīzākus HLS atskaņošanas sarakstus. Šis process var būt ilgs." } diff --git a/Emby.Server.Implementations/Localization/Core/ml.json b/Emby.Server.Implementations/Localization/Core/ml.json index 0620fbcdb..0b50fa529 100644 --- a/Emby.Server.Implementations/Localization/Core/ml.json +++ b/Emby.Server.Implementations/Localization/Core/ml.json @@ -121,5 +121,7 @@ "TaskOptimizeDatabaseDescription": "ഡാറ്റാബേസ് ചുരുക്കുകയും സ്വതന്ത്ര ഇടം വെട്ടിച്ചുരുക്കുകയും ചെയ്യുന്നു. ലൈബ്രറി സ്കാൻ ചെയ്തതിനുശേഷം അല്ലെങ്കിൽ ഡാറ്റാബേസ് പരിഷ്ക്കരണങ്ങളെ സൂചിപ്പിക്കുന്ന മറ്റ് മാറ്റങ്ങൾ ചെയ്തതിന് ശേഷം ഈ ടാസ്ക് പ്രവർത്തിപ്പിക്കുന്നത് പ്രകടനം മെച്ചപ്പെടുത്തും.", "TaskOptimizeDatabase": "ഡാറ്റാബേസ് ഒപ്റ്റിമൈസ് ചെയ്യുക", "HearingImpaired": "കേൾവി തകരാറുകൾ", - "External": "പുറമേയുള്ള" + "External": "പുറമേയുള്ള", + "TaskKeyframeExtractorDescription": "കൂടുതൽ കൃത്യമായ HLS പ്ലേലിസ്റ്റുകൾ സൃഷ്ടിക്കുന്നതിന് വീഡിയോ ഫയലുകളിൽ നിന്ന് കീഫ്രെയിമുകൾ എക്സ്ട്രാക്റ്റ് ചെയ്യുന്നു. ഈ പ്രവർത്തനം പൂർത്തിയാവാൻ കുറച്ചധികം സമയം എടുത്തേക്കാം.", + "TaskKeyframeExtractor": "കീഫ്രെയിം എക്സ്ട്രാക്റ്റർ" } diff --git a/Emby.Server.Implementations/Localization/Core/si.json b/Emby.Server.Implementations/Localization/Core/si.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/si.json @@ -0,0 +1 @@ +{} diff --git a/Emby.Server.Implementations/Localization/Core/sk.json b/Emby.Server.Implementations/Localization/Core/sk.json index 858cc40dd..c231d76fe 100644 --- a/Emby.Server.Implementations/Localization/Core/sk.json +++ b/Emby.Server.Implementations/Localization/Core/sk.json @@ -124,5 +124,5 @@ "TaskKeyframeExtractorDescription": "Extrahuje kľúčové snímky z video súborov na vytvorenie presnejších HLS playlistov. Táto úloha môže trvať dlhšiu dobu.", "TaskKeyframeExtractor": "Extraktor kľúčových snímkov", "External": "Externé", - "HearingImpaired": "Sluchovo Postihnutý" + "HearingImpaired": "Sluchovo postihnutí" } diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index 96f435399..16776b6bd 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -71,25 +71,28 @@ namespace Emby.Server.Implementations.Localization string countryCode = resource.Substring(RatingsPath.Length, 2); var dict = new Dictionary<string, ParentalRating>(StringComparer.OrdinalIgnoreCase); - await using var stream = _assembly.GetManifestResourceStream(resource); - using var reader = new StreamReader(stream!); // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames() - await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false)) + var stream = _assembly.GetManifestResourceStream(resource); + await using (stream!.ConfigureAwait(false)) // shouldn't be null here, we just got the resource path from Assembly.GetManifestResourceNames() { - if (string.IsNullOrWhiteSpace(line)) + using var reader = new StreamReader(stream!); + await foreach (var line in reader.ReadAllLinesAsync().ConfigureAwait(false)) { - continue; - } - - string[] parts = line.Split(','); - if (parts.Length == 2 - && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) - { - var name = parts[0]; - dict.Add(name, new ParentalRating(name, value)); - } - else - { - _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode); + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + string[] parts = line.Split(','); + if (parts.Length == 2 + && int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + { + var name = parts[0]; + dict.Add(name, new ParentalRating(name, value)); + } + else + { + _logger.LogWarning("Malformed line in ratings file for country {CountryCode}", countryCode); + } } } diff --git a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs index 7732e32d0..896f47923 100644 --- a/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs +++ b/Emby.Server.Implementations/MediaEncoder/EncodingManager.cs @@ -222,7 +222,7 @@ namespace Emby.Server.Implementations.MediaEncoder { var deadImages = images .Except(chapters.Select(i => i.ImagePath).Where(i => !string.IsNullOrEmpty(i)), StringComparer.OrdinalIgnoreCase) - .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i), StringComparison.OrdinalIgnoreCase)) + .Where(i => BaseItem.SupportedImageExtensions.Contains(Path.GetExtension(i.AsSpan()), StringComparison.OrdinalIgnoreCase)) .ToList(); foreach (var image in deadImages) diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs index 51e92953d..2bcd5eab2 100644 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ b/Emby.Server.Implementations/Net/SocketFactory.cs @@ -1,12 +1,15 @@ -#pragma warning disable CS1591 - using System; +using System.Linq; using System.Net; +using System.Net.NetworkInformation; using System.Net.Sockets; using MediaBrowser.Model.Net; namespace Emby.Server.Implementations.Net { + /// <summary> + /// Factory class to create different kinds of sockets. + /// </summary> public class SocketFactory : ISocketFactory { /// <inheritdoc /> @@ -29,7 +32,7 @@ namespace Emby.Server.Implementations.Net } catch { - socket?.Dispose(); + socket.Dispose(); throw; } @@ -38,7 +41,8 @@ namespace Emby.Server.Implementations.Net /// <inheritdoc /> public Socket CreateSsdpUdpSocket(IPData bindInterface, int localPort) { - ArgumentNullException.ThrowIfNull(bindInterface.Address); + var interfaceAddress = bindInterface.Address; + ArgumentNullException.ThrowIfNull(interfaceAddress); if (localPort < 0) { @@ -49,13 +53,13 @@ namespace Emby.Server.Implementations.Net try { socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); - socket.Bind(new IPEndPoint(bindInterface.Address, localPort)); + socket.Bind(new IPEndPoint(interfaceAddress, localPort)); return socket; } catch { - socket?.Dispose(); + socket.Dispose(); throw; } @@ -82,22 +86,31 @@ namespace Emby.Server.Implementations.Net try { - var interfaceIndex = bindInterface.Index; - var interfaceIndexSwapped = (int)IPAddress.HostToNetworkOrder(interfaceIndex); - socket.MulticastLoopback = false; socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.PacketInformation, true); socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, multicastTimeToLive); - socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastInterface, interfaceIndexSwapped); - socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex)); - socket.Bind(new IPEndPoint(multicastAddress, localPort)); + + if (OperatingSystem.IsLinux() || OperatingSystem.IsMacOS()) + { + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress)); + socket.Bind(new IPEndPoint(multicastAddress, localPort)); + } + else + { + // Only create socket if interface supports multicast + var interfaceIndex = bindInterface.Index; + var interfaceIndexSwapped = IPAddress.HostToNetworkOrder(interfaceIndex); + + socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress, interfaceIndex)); + socket.Bind(new IPEndPoint(bindIPAddress, localPort)); + } return socket; } catch { - socket?.Dispose(); + socket.Dispose(); throw; } diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 0cb450cdf..649c49924 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -327,9 +327,9 @@ namespace Emby.Server.Implementations.Playlists // this is probably best done as a metadata provider // saving a file over itself will require some work to prevent this from happening when not needed var playlistPath = item.Path; - var extension = Path.GetExtension(playlistPath); + var extension = Path.GetExtension(playlistPath.AsSpan()); - if (string.Equals(".wpl", extension, StringComparison.OrdinalIgnoreCase)) + if (extension.Equals(".wpl", StringComparison.OrdinalIgnoreCase)) { var playlist = new WplPlaylist(); foreach (var child in item.GetLinkedChildren()) @@ -362,8 +362,7 @@ namespace Emby.Server.Implementations.Playlists string text = new WplContent().ToText(playlist); File.WriteAllText(playlistPath, text); } - - if (string.Equals(".zpl", extension, StringComparison.OrdinalIgnoreCase)) + else if (extension.Equals(".zpl", StringComparison.OrdinalIgnoreCase)) { var playlist = new ZplPlaylist(); foreach (var child in item.GetLinkedChildren()) @@ -396,8 +395,7 @@ namespace Emby.Server.Implementations.Playlists string text = new ZplContent().ToText(playlist); File.WriteAllText(playlistPath, text); } - - if (string.Equals(".m3u", extension, StringComparison.OrdinalIgnoreCase)) + else if (extension.Equals(".m3u", StringComparison.OrdinalIgnoreCase)) { var playlist = new M3uPlaylist { @@ -428,8 +426,7 @@ namespace Emby.Server.Implementations.Playlists string text = new M3uContent().ToText(playlist); File.WriteAllText(playlistPath, text); } - - if (string.Equals(".m3u8", extension, StringComparison.OrdinalIgnoreCase)) + else if (extension.Equals(".m3u8", StringComparison.OrdinalIgnoreCase)) { var playlist = new M3uPlaylist(); playlist.IsExtended = true; @@ -458,8 +455,7 @@ namespace Emby.Server.Implementations.Playlists string text = new M3uContent().ToText(playlist); File.WriteAllText(playlistPath, text); } - - if (string.Equals(".pls", extension, StringComparison.OrdinalIgnoreCase)) + else if (extension.Equals(".pls", StringComparison.OrdinalIgnoreCase)) { var playlist = new PlsPlaylist(); foreach (var child in item.GetLinkedChildren()) diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index d7189ef0c..20793ee39 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -386,11 +386,11 @@ namespace Emby.Server.Implementations.Plugins var url = new Uri(packageInfo.ImageUrl); imagePath = Path.Join(path, url.Segments[^1]); - await using var fileStream = AsyncFile.OpenWrite(imagePath); - + var fileStream = AsyncFile.OpenWrite(imagePath); + Stream? downloadStream = null; try { - await using var downloadStream = await HttpClientFactory + downloadStream = await HttpClientFactory .CreateClient(NamedClient.Default) .GetStreamAsync(url) .ConfigureAwait(false); @@ -402,6 +402,14 @@ namespace Emby.Server.Implementations.Plugins _logger.LogError(ex, "Failed to download image to path {Path} on disk.", imagePath); imagePath = string.Empty; } + finally + { + await fileStream.DisposeAsync().ConfigureAwait(false); + if (downloadStream is not null) + { + await downloadStream.DisposeAsync().ConfigureAwait(false); + } + } } var manifest = new PluginManifest @@ -421,7 +429,7 @@ namespace Emby.Server.Implementations.Plugins ImagePath = imagePath }; - if (!await ReconcileManifest(manifest, path)) + if (!await ReconcileManifest(manifest, path).ConfigureAwait(false)) { // An error occurred during reconciliation and saving could be undesirable. return false; @@ -458,7 +466,7 @@ namespace Emby.Server.Implementations.Plugins } using var metaStream = File.OpenRead(metafile); - var localManifest = await JsonSerializer.DeserializeAsync<PluginManifest>(metaStream, _jsonOptions); + var localManifest = await JsonSerializer.DeserializeAsync<PluginManifest>(metaStream, _jsonOptions).ConfigureAwait(false); localManifest ??= new PluginManifest(); if (!Equals(localManifest.Id, manifest.Id)) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index 6ad6c4cbd..5d15c3a21 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -115,7 +115,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks { try { - previouslyFailedImages = File.ReadAllText(failHistoryPath) + previouslyFailedImages = (await File.ReadAllTextAsync(failHistoryPath, cancellationToken).ConfigureAwait(false)) .Split('|', StringSplitOptions.RemoveEmptyEntries) .ToList(); } @@ -156,7 +156,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks } string text = string.Join('|', previouslyFailedImages); - File.WriteAllText(failHistoryPath, text); + await File.WriteAllTextAsync(failHistoryPath, text, cancellationToken).ConfigureAwait(false); } numComplete++; diff --git a/Emby.Server.Implementations/SystemManager.cs b/Emby.Server.Implementations/SystemManager.cs new file mode 100644 index 000000000..2c477218f --- /dev/null +++ b/Emby.Server.Implementations/SystemManager.cs @@ -0,0 +1,104 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Updates; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Model.System; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Hosting; + +namespace Emby.Server.Implementations; + +/// <inheritdoc /> +public class SystemManager : ISystemManager +{ + private readonly IHostApplicationLifetime _applicationLifetime; + private readonly IServerApplicationHost _applicationHost; + private readonly IServerApplicationPaths _applicationPaths; + private readonly IServerConfigurationManager _configurationManager; + private readonly IStartupOptions _startupOptions; + private readonly IInstallationManager _installationManager; + + /// <summary> + /// Initializes a new instance of the <see cref="SystemManager"/> class. + /// </summary> + /// <param name="applicationLifetime">Instance of <see cref="IHostApplicationLifetime"/>.</param> + /// <param name="applicationHost">Instance of <see cref="IServerApplicationHost"/>.</param> + /// <param name="applicationPaths">Instance of <see cref="IServerApplicationPaths"/>.</param> + /// <param name="configurationManager">Instance of <see cref="IServerConfigurationManager"/>.</param> + /// <param name="startupOptions">Instance of <see cref="IStartupOptions"/>.</param> + /// <param name="installationManager">Instance of <see cref="IInstallationManager"/>.</param> + public SystemManager( + IHostApplicationLifetime applicationLifetime, + IServerApplicationHost applicationHost, + IServerApplicationPaths applicationPaths, + IServerConfigurationManager configurationManager, + IStartupOptions startupOptions, + IInstallationManager installationManager) + { + _applicationLifetime = applicationLifetime; + _applicationHost = applicationHost; + _applicationPaths = applicationPaths; + _configurationManager = configurationManager; + _startupOptions = startupOptions; + _installationManager = installationManager; + } + + /// <inheritdoc /> + public SystemInfo GetSystemInfo(HttpRequest request) + { + return new SystemInfo + { + HasPendingRestart = _applicationHost.HasPendingRestart, + IsShuttingDown = _applicationLifetime.ApplicationStopping.IsCancellationRequested, + Version = _applicationHost.ApplicationVersionString, + WebSocketPortNumber = _applicationHost.HttpPort, + CompletedInstallations = _installationManager.CompletedInstallations.ToArray(), + Id = _applicationHost.SystemId, + ProgramDataPath = _applicationPaths.ProgramDataPath, + WebPath = _applicationPaths.WebPath, + LogPath = _applicationPaths.LogDirectoryPath, + ItemsByNamePath = _applicationPaths.InternalMetadataPath, + InternalMetadataPath = _applicationPaths.InternalMetadataPath, + CachePath = _applicationPaths.CachePath, + TranscodingTempPath = _configurationManager.GetTranscodePath(), + ServerName = _applicationHost.FriendlyName, + LocalAddress = _applicationHost.GetSmartApiUrl(request), + SupportsLibraryMonitor = true, + PackageName = _startupOptions.PackageName, + CastReceiverApplications = _configurationManager.Configuration.CastReceiverApplications + }; + } + + /// <inheritdoc /> + public PublicSystemInfo GetPublicSystemInfo(HttpRequest request) + { + return new PublicSystemInfo + { + Version = _applicationHost.ApplicationVersionString, + ProductName = _applicationHost.Name, + Id = _applicationHost.SystemId, + ServerName = _applicationHost.FriendlyName, + LocalAddress = _applicationHost.GetSmartApiUrl(request), + StartupWizardCompleted = _configurationManager.CommonConfiguration.IsStartupWizardCompleted + }; + } + + /// <inheritdoc /> + public void Restart() => ShutdownInternal(true); + + /// <inheritdoc /> + public void Shutdown() => ShutdownInternal(false); + + private void ShutdownInternal(bool restart) + { + Task.Run(async () => + { + await Task.Delay(100).ConfigureAwait(false); + _applicationHost.ShouldRestart = restart; + _applicationLifetime.StopApplication(); + }); + } +} diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index a3bbd6df0..2d806c146 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -27,9 +27,9 @@ namespace Emby.Server.Implementations.Udp private readonly byte[] _receiveBuffer = new byte[8192]; - private Socket _udpSocket; - private IPEndPoint _endpoint; - private bool _disposed = false; + private readonly Socket _udpSocket; + private readonly IPEndPoint _endpoint; + private bool _disposed; /// <summary> /// Initializes a new instance of the <see cref="UdpServer" /> class. @@ -52,7 +52,10 @@ namespace Emby.Server.Implementations.Udp _endpoint = new IPEndPoint(bindAddress, port); - _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); + _udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp) + { + MulticastLoopback = false, + }; _udpSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true); } @@ -74,6 +77,7 @@ namespace Emby.Server.Implementations.Udp try { + _logger.LogDebug("Sending AutoDiscovery response"); await _udpSocket.SendToAsync(JsonSerializer.SerializeToUtf8Bytes(response), SocketFlags.None, endpoint, cancellationToken).ConfigureAwait(false); } catch (SocketException ex) @@ -99,7 +103,8 @@ namespace Emby.Server.Implementations.Udp { try { - var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint, cancellationToken).ConfigureAwait(false); + var endpoint = (EndPoint)new IPEndPoint(IPAddress.Any, 0); + var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, endpoint, cancellationToken).ConfigureAwait(false); var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes); if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase)) { @@ -112,7 +117,7 @@ namespace Emby.Server.Implementations.Udp } catch (OperationCanceledException) { - // Don't throw + _logger.LogDebug("Broadcast socket operation cancelled"); } } } @@ -125,9 +130,8 @@ namespace Emby.Server.Implementations.Udp return; } - _udpSocket?.Dispose(); - - GC.SuppressFinalize(this); + _udpSocket.Dispose(); + _disposed = true; } } } diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 6c198b6f9..c717744b1 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -504,8 +504,7 @@ namespace Emby.Server.Implementations.Updates private async Task PerformPackageInstallation(InstallationInfo package, PluginStatus status, CancellationToken cancellationToken) { - var extension = Path.GetExtension(package.SourceUrl); - if (!string.Equals(extension, ".zip", StringComparison.OrdinalIgnoreCase)) + if (!Path.GetExtension(package.SourceUrl.AsSpan()).Equals(".zip", StringComparison.OrdinalIgnoreCase)) { _logger.LogError("Only zip packages are supported. {SourceUrl} is not a zip archive.", package.SourceUrl); return; @@ -521,10 +520,9 @@ namespace Emby.Server.Implementations.Updates // CA5351: Do Not Use Broken Cryptographic Algorithms #pragma warning disable CA5351 - using var md5 = MD5.Create(); cancellationToken.ThrowIfCancellationRequested(); - var hash = Convert.ToHexString(md5.ComputeHash(stream)); + var hash = Convert.ToHexString(await MD5.HashDataAsync(stream, cancellationToken).ConfigureAwait(false)); if (!string.Equals(package.Checksum, hash, StringComparison.OrdinalIgnoreCase)) { _logger.LogError( @@ -557,7 +555,7 @@ namespace Emby.Server.Implementations.Updates reader.ExtractToDirectory(targetDir, true); // Ensure we create one or populate existing ones with missing data. - await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status); + await _pluginManager.PopulateManifest(package.PackageInfo, package.Version, targetDir, status).ConfigureAwait(false); _pluginManager.ImportPluginFrom(targetDir); } |
