diff options
Diffstat (limited to 'Emby.Server.Implementations')
90 files changed, 1072 insertions, 939 deletions
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 814c10196..82294644b 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -44,9 +44,9 @@ using Emby.Server.Implementations.Serialization; using Emby.Server.Implementations.Session; using Emby.Server.Implementations.SyncPlay; using Emby.Server.Implementations.TV; -using Emby.Server.Implementations.Udp; using Emby.Server.Implementations.Updates; using Jellyfin.Api.Helpers; +using Jellyfin.MediaEncoding.Hls.Playlist; using Jellyfin.Networking.Configuration; using Jellyfin.Networking.Manager; using MediaBrowser.Common; @@ -103,6 +103,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Prometheus.DotNetRuntime; +using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; using WebSocketManager = Emby.Server.Implementations.HttpServer.WebSocketManager; namespace Emby.Server.Implementations @@ -149,7 +150,7 @@ namespace Emby.Server.Implementations /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/> interface.</param> /// <param name="options">Instance of the <see cref="IStartupOptions"/> interface.</param> /// <param name="startupConfig">The <see cref="IConfiguration" /> interface.</param> - public ApplicationHost( + protected ApplicationHost( IServerApplicationPaths applicationPaths, ILoggerFactory loggerFactory, IStartupOptions options, @@ -184,6 +185,11 @@ namespace Emby.Server.Implementations public event EventHandler HasPendingRestartChanged; /// <summary> + /// Gets the value of the PublishedServerUrl setting. + /// </summary> + private string PublishedServerUrl => _startupConfig[AddressOverrideKey]; + + /// <summary> /// Gets a value indicating whether this instance can self restart. /// </summary> public bool CanSelfRestart => _startupOptions.RestartPath != null; @@ -259,11 +265,6 @@ namespace Emby.Server.Implementations /// </summary> public int HttpsPort { get; private set; } - /// <summary> - /// Gets the value of the PublishedServerUrl setting. - /// </summary> - public string PublishedServerUrl => _startupOptions.PublishedServerUrl ?? _startupConfig[UdpServer.AddressOverrideConfigKey]; - /// <inheritdoc /> public Version ApplicationVersion { get; } @@ -999,6 +1000,9 @@ namespace Emby.Server.Implementations // Network yield return typeof(NetworkManager).Assembly; + // Hls + yield return typeof(DynamicHlsPlaylistGenerator).Assembly; + foreach (var i in GetAssembliesWithPartsInternal()) { yield return i; diff --git a/Emby.Server.Implementations/Channels/ChannelManager.cs b/Emby.Server.Implementations/Channels/ChannelManager.cs index 43c8a451b..92a85e862 100644 --- a/Emby.Server.Implementations/Channels/ChannelManager.cs +++ b/Emby.Server.Implementations/Channels/ChannelManager.cs @@ -39,7 +39,7 @@ namespace Emby.Server.Implementations.Channels /// <summary> /// The LiveTV channel manager. /// </summary> - public class ChannelManager : IChannelManager + public class ChannelManager : IChannelManager, IDisposable { private readonly IUserManager _userManager; private readonly IUserDataManager _userDataManager; @@ -52,6 +52,7 @@ namespace Emby.Server.Implementations.Channels private readonly IMemoryCache _memoryCache; private readonly SemaphoreSlim _resourcePool = new SemaphoreSlim(1, 1); private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; + private bool _disposed = false; /// <summary> /// Initializes a new instance of the <see cref="ChannelManager"/> class. @@ -161,7 +162,7 @@ namespace Emby.Server.Implementations.Channels /// <inheritdoc /> public QueryResult<Channel> GetChannelsInternal(ChannelQuery query) { - var user = query.UserId.Equals(Guid.Empty) + var user = query.UserId.Equals(default) ? null : _userManager.GetUserById(query.UserId); @@ -264,17 +265,16 @@ namespace Emby.Server.Implementations.Channels } } - return new QueryResult<Channel> - { - Items = all, - TotalRecordCount = totalCount - }; + return new QueryResult<Channel>( + query.StartIndex, + totalCount, + all); } /// <inheritdoc /> public QueryResult<BaseItemDto> GetChannels(ChannelQuery query) { - var user = query.UserId.Equals(Guid.Empty) + var user = query.UserId.Equals(default) ? null : _userManager.GetUserById(query.UserId); @@ -285,11 +285,10 @@ namespace Emby.Server.Implementations.Channels // TODO Fix The co-variant conversion (internalResult.Items) between Folder[] and BaseItem[], this can generate runtime issues. var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, dtoOptions, user); - var result = new QueryResult<BaseItemDto> - { - Items = returnItems, - TotalRecordCount = internalResult.TotalRecordCount - }; + var result = new QueryResult<BaseItemDto>( + query.StartIndex, + internalResult.TotalRecordCount, + returnItems); return result; } @@ -333,7 +332,7 @@ namespace Emby.Server.Implementations.Channels private Channel GetChannelEntity(IChannel channel) { - return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).Result; + return GetChannel(GetInternalChannelId(channel.Name)) ?? GetChannel(channel, CancellationToken.None).GetAwaiter().GetResult(); } private MediaSourceInfo[] GetSavedMediaSources(BaseItem item) @@ -475,7 +474,7 @@ namespace Emby.Server.Implementations.Channels item.ChannelId = id; - if (item.ParentId != parentFolderId) + if (!item.ParentId.Equals(parentFolderId)) { forceUpdate = true; } @@ -620,11 +619,10 @@ namespace Emby.Server.Implementations.Channels var returnItems = _dtoService.GetBaseItemDtos(items, query.DtoOptions, query.User); - var result = new QueryResult<BaseItemDto> - { - Items = returnItems, - TotalRecordCount = totalRecordCount - }; + var result = new QueryResult<BaseItemDto>( + query.StartIndex, + totalRecordCount, + returnItems); return result; } @@ -717,7 +715,9 @@ namespace Emby.Server.Implementations.Channels // Find the corresponding channel provider plugin var channelProvider = GetChannelProvider(channel); - var parentItem = query.ParentId == Guid.Empty ? channel : _libraryManager.GetItemById(query.ParentId); + var parentItem = query.ParentId.Equals(default) + ? channel + : _libraryManager.GetItemById(query.ParentId); var itemsResult = await GetChannelItems( channelProvider, @@ -728,7 +728,7 @@ namespace Emby.Server.Implementations.Channels cancellationToken) .ConfigureAwait(false); - if (query.ParentId == Guid.Empty) + if (query.ParentId.Equals(default)) { query.Parent = channel; } @@ -786,11 +786,10 @@ namespace Emby.Server.Implementations.Channels var returnItems = _dtoService.GetBaseItemDtos(internalResult.Items, query.DtoOptions, query.User); - var result = new QueryResult<BaseItemDto> - { - Items = returnItems, - TotalRecordCount = internalResult.TotalRecordCount - }; + var result = new QueryResult<BaseItemDto>( + query.StartIndex, + internalResult.TotalRecordCount, + returnItems); return result; } @@ -1217,5 +1216,31 @@ namespace Emby.Server.Implementations.Channels return result; } + + /// <inheritdoc /> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Releases unmanaged and optionally managed resources. + /// </summary> + /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + _resourcePool?.Dispose(); + } + + _disposed = true; + } } } diff --git a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs b/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs index e5dde48d8..cfd08e653 100644 --- a/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs +++ b/Emby.Server.Implementations/Channels/RefreshChannelsScheduledTask.cs @@ -62,7 +62,7 @@ namespace Emby.Server.Implementations.Channels public string Key => "RefreshInternetChannels"; /// <inheritdoc /> - public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress) + public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) { var manager = (ChannelManager)_channelManager; diff --git a/Emby.Server.Implementations/Collections/CollectionManager.cs b/Emby.Server.Implementations/Collections/CollectionManager.cs index b5b8fea65..5fc2e39a7 100644 --- a/Emby.Server.Implementations/Collections/CollectionManager.cs +++ b/Emby.Server.Implementations/Collections/CollectionManager.cs @@ -265,7 +265,7 @@ namespace Emby.Server.Implementations.Collections { var childItem = _libraryManager.GetItemById(guidId); - var child = collection.LinkedChildren.FirstOrDefault(i => (i.ItemId.HasValue && i.ItemId.Value == guidId) || (childItem != null && string.Equals(childItem.Path, i.Path, StringComparison.OrdinalIgnoreCase))); + var child = collection.LinkedChildren.FirstOrDefault(i => (i.ItemId.HasValue && i.ItemId.Value.Equals(guidId)) || (childItem != null && string.Equals(childItem.Path, i.Path, StringComparison.OrdinalIgnoreCase))); if (child == null) { diff --git a/Emby.Server.Implementations/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 5ab9e02fe..85fa79cba 100644 --- a/Emby.Server.Implementations/Data/SqliteItemRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteItemRepository.cs @@ -317,11 +317,6 @@ namespace Emby.Server.Implementations.Data IImageProcessor imageProcessor) : base(logger) { - if (config == null) - { - throw new ArgumentNullException(nameof(config)); - } - _config = config; _appHost = appHost; _localization = localization; @@ -334,9 +329,6 @@ namespace Emby.Server.Implementations.Data } /// <inheritdoc /> - public string Name => "SQLite"; - - /// <inheritdoc /> protected override int? CacheSize => 20000; /// <inheritdoc /> @@ -573,22 +565,6 @@ namespace Emby.Server.Implementations.Data userDataRepo.Initialize(userManager, WriteLock, WriteConnection); } - /// <summary> - /// Save a standard item in the repo. - /// </summary> - /// <param name="item">The item.</param> - /// <param name="cancellationToken">The cancellation token.</param> - /// <exception cref="ArgumentNullException"><paramref name="item"/> is <c>null</c>.</exception> - public void SaveItem(BaseItem item, CancellationToken cancellationToken) - { - if (item == null) - { - throw new ArgumentNullException(nameof(item)); - } - - SaveItems(new[] { item }, cancellationToken); - } - public void SaveImages(BaseItem item) { if (item == null) @@ -605,7 +581,7 @@ namespace Emby.Server.Implementations.Data { using (var saveImagesStatement = PrepareStatement(db, "Update TypedBaseItems set Images=@Images where guid=@Id")) { - saveImagesStatement.TryBind("@Id", item.Id.ToByteArray()); + saveImagesStatement.TryBind("@Id", item.Id); saveImagesStatement.TryBind("@Images", SerializeImages(item.ImageInfos)); saveImagesStatement.MoveNext(); @@ -750,7 +726,7 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBindNull("@EndDate"); } - saveItemStatement.TryBind("@ChannelId", item.ChannelId.Equals(Guid.Empty) ? null : item.ChannelId.ToString("N", CultureInfo.InvariantCulture)); + saveItemStatement.TryBind("@ChannelId", item.ChannelId.Equals(default) ? null : item.ChannelId.ToString("N", CultureInfo.InvariantCulture)); if (item is IHasProgramAttributes hasProgramAttributes) { @@ -780,7 +756,7 @@ namespace Emby.Server.Implementations.Data saveItemStatement.TryBind("@ProductionYear", item.ProductionYear); var parentId = item.ParentId; - if (parentId.Equals(Guid.Empty)) + if (parentId.Equals(default)) { saveItemStatement.TryBindNull("@ParentId"); } @@ -975,7 +951,7 @@ namespace Emby.Server.Implementations.Data { saveItemStatement.TryBind("@SeasonName", episode.SeasonName); - var nullableSeasonId = episode.SeasonId == Guid.Empty ? (Guid?)null : episode.SeasonId; + var nullableSeasonId = episode.SeasonId.Equals(default) ? (Guid?)null : episode.SeasonId; saveItemStatement.TryBind("@SeasonId", nullableSeasonId); } @@ -987,7 +963,7 @@ namespace Emby.Server.Implementations.Data if (item is IHasSeries hasSeries) { - var nullableSeriesId = hasSeries.SeriesId.Equals(Guid.Empty) ? (Guid?)null : hasSeries.SeriesId; + var nullableSeriesId = hasSeries.SeriesId.Equals(default) ? (Guid?)null : hasSeries.SeriesId; saveItemStatement.TryBind("@SeriesId", nullableSeriesId); saveItemStatement.TryBind("@SeriesPresentationUniqueKey", hasSeries.SeriesPresentationUniqueKey); @@ -1060,7 +1036,7 @@ namespace Emby.Server.Implementations.Data } Guid ownerId = item.OwnerId; - if (ownerId == Guid.Empty) + if (ownerId.Equals(default)) { saveItemStatement.TryBindNull("@OwnerId"); } @@ -1198,13 +1174,15 @@ namespace Emby.Server.Implementations.Data bldr.Append(Delimiter) // Replace delimiters with other characters. // This can be removed when we migrate to a proper DB. - .Append(hash.Replace('*', '/').Replace('|', '\\')); + .Append(hash.Replace(Delimiter, '/').Replace('|', '\\')); } } internal ItemImageInfo ItemImageInfoFromValueString(ReadOnlySpan<char> value) { - var nextSegment = value.IndexOf('*'); + const char Delimiter = '*'; + + var nextSegment = value.IndexOf(Delimiter); if (nextSegment == -1) { return null; @@ -1212,7 +1190,7 @@ namespace Emby.Server.Implementations.Data ReadOnlySpan<char> path = value[..nextSegment]; value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf('*'); + nextSegment = value.IndexOf(Delimiter); if (nextSegment == -1) { return null; @@ -1220,7 +1198,7 @@ namespace Emby.Server.Implementations.Data ReadOnlySpan<char> dateModified = value[..nextSegment]; value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf('*'); + nextSegment = value.IndexOf(Delimiter); if (nextSegment == -1) { nextSegment = value.Length; @@ -1257,7 +1235,7 @@ namespace Emby.Server.Implementations.Data if (nextSegment + 1 < value.Length - 1) { value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf('*'); + nextSegment = value.IndexOf(Delimiter); if (nextSegment == -1 || nextSegment == value.Length) { return image; @@ -1266,7 +1244,7 @@ namespace Emby.Server.Implementations.Data ReadOnlySpan<char> widthSpan = value[..nextSegment]; value = value[(nextSegment + 1)..]; - nextSegment = value.IndexOf('*'); + nextSegment = value.IndexOf(Delimiter); if (nextSegment == -1) { nextSegment = value.Length; @@ -1292,7 +1270,7 @@ namespace Emby.Server.Implementations.Data var c = value[i]; blurHashSpan[i] = c switch { - '/' => '*', + '/' => Delimiter, '\\' => '|', _ => c }; @@ -1314,7 +1292,7 @@ namespace Emby.Server.Implementations.Data /// <exception cref="ArgumentException"><paramr name="id"/> is <seealso cref="Guid.Empty"/>.</exception> public BaseItem RetrieveItem(Guid id) { - if (id == Guid.Empty) + if (id.Equals(default)) { throw new ArgumentException("Guid can't be empty", nameof(id)); } @@ -2086,7 +2064,7 @@ namespace Emby.Server.Implementations.Data { CheckDisposed(); - if (id.Equals(Guid.Empty)) + if (id.Equals(default)) { throw new ArgumentNullException(nameof(id)); } @@ -2492,12 +2470,12 @@ namespace Emby.Server.Implementations.Data searchTerm = GetCleanValue(searchTerm); var commandText = statement.SQL; - if (commandText.IndexOf("@SearchTermStartsWith", StringComparison.OrdinalIgnoreCase) != -1) + if (commandText.Contains("@SearchTermStartsWith", StringComparison.OrdinalIgnoreCase)) { statement.TryBind("@SearchTermStartsWith", searchTerm + "%"); } - if (commandText.IndexOf("@SearchTermContains", StringComparison.OrdinalIgnoreCase) != -1) + if (commandText.Contains("@SearchTermContains", StringComparison.OrdinalIgnoreCase)) { statement.TryBind("@SearchTermContains", "%" + searchTerm + "%"); } @@ -2514,17 +2492,17 @@ namespace Emby.Server.Implementations.Data var commandText = statement.SQL; - if (commandText.IndexOf("@ItemOfficialRating", StringComparison.OrdinalIgnoreCase) != -1) + if (commandText.Contains("@ItemOfficialRating", StringComparison.OrdinalIgnoreCase)) { statement.TryBind("@ItemOfficialRating", item.OfficialRating); } - if (commandText.IndexOf("@ItemProductionYear", StringComparison.OrdinalIgnoreCase) != -1) + if (commandText.Contains("@ItemProductionYear", StringComparison.OrdinalIgnoreCase)) { statement.TryBind("@ItemProductionYear", item.ProductionYear ?? 0); } - if (commandText.IndexOf("@SimilarItemId", StringComparison.OrdinalIgnoreCase) != -1) + if (commandText.Contains("@SimilarItemId", StringComparison.OrdinalIgnoreCase)) { statement.TryBind("@SimilarItemId", item.Id); } @@ -2758,12 +2736,12 @@ namespace Emby.Server.Implementations.Data foreach (var providerId in newItem.ProviderIds) { - if (providerId.Key == MetadataProvider.TmdbCollection.ToString()) + if (string.Equals(providerId.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.Ordinal)) { continue; } - if (item.GetProviderId(providerId.Key) == providerId.Value) + if (string.Equals(item.GetProviderId(providerId.Key), providerId.Value, StringComparison.Ordinal)) { if (newItem.SourceType == SourceType.Library) { @@ -2810,11 +2788,10 @@ namespace Emby.Server.Implementations.Data if (!query.EnableTotalRecordCount || (!query.Limit.HasValue && (query.StartIndex ?? 0) == 0)) { var returnList = GetItemList(query); - return new QueryResult<BaseItem> - { - Items = returnList, - TotalRecordCount = returnList.Count - }; + return new QueryResult<BaseItem>( + query.StartIndex, + returnList.Count, + returnList); } var now = DateTime.UtcNow; @@ -2978,6 +2955,7 @@ namespace Emby.Server.Implementations.Data ReadTransactionMode); } + result.StartIndex = query.StartIndex ?? 0; result.Items = list; return result; } @@ -3185,220 +3163,6 @@ namespace Emby.Server.Implementations.Data return list; } - public List<Tuple<Guid, string>> GetItemIdsWithPath(InternalItemsQuery query) - { - if (query == null) - { - throw new ArgumentNullException(nameof(query)); - } - - CheckDisposed(); - - var now = DateTime.UtcNow; - - var columns = new List<string> { "guid", "path" }; - SetFinalColumnsToSelect(query, columns); - var commandText = "select " + string.Join(',', columns) + FromText; - - var whereClauses = GetWhereClauses(query, null); - if (whereClauses.Count != 0) - { - commandText += " where " + string.Join(" AND ", whereClauses); - } - - commandText += GetGroupBy(query) - + GetOrderByText(query); - - if (query.Limit.HasValue || query.StartIndex.HasValue) - { - var offset = query.StartIndex ?? 0; - - if (query.Limit.HasValue || offset > 0) - { - commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); - } - - if (offset > 0) - { - commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); - } - } - - var list = new List<Tuple<Guid, string>>(); - using (var connection = GetConnection(true)) - { - using (var statement = PrepareStatement(connection, commandText)) - { - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } - - // Running this again will bind the params - GetWhereClauses(query, statement); - - foreach (var row in statement.ExecuteQuery()) - { - var id = row.GetGuid(0); - - row.TryGetString(1, out var path); - - list.Add(new Tuple<Guid, string>(id, path)); - } - } - } - - LogQueryTime("GetItemIdsWithPath", commandText, now); - - return list; - } - - public QueryResult<Guid> GetItemIds(InternalItemsQuery query) - { - if (query == null) - { - throw new ArgumentNullException(nameof(query)); - } - - CheckDisposed(); - - if (!query.EnableTotalRecordCount || (!query.Limit.HasValue && (query.StartIndex ?? 0) == 0)) - { - var returnList = GetItemIdsList(query); - return new QueryResult<Guid> - { - Items = returnList, - TotalRecordCount = returnList.Count - }; - } - - var now = DateTime.UtcNow; - - var columns = new List<string> { "guid" }; - SetFinalColumnsToSelect(query, columns); - var commandText = "select " - + string.Join(',', columns) - + FromText - + GetJoinUserDataText(query); - - var whereClauses = GetWhereClauses(query, null); - - var whereText = whereClauses.Count == 0 ? - string.Empty : - " where " + string.Join(" AND ", whereClauses); - - commandText += whereText - + GetGroupBy(query) - + GetOrderByText(query); - - if (query.Limit.HasValue || query.StartIndex.HasValue) - { - var offset = query.StartIndex ?? 0; - - if (query.Limit.HasValue || offset > 0) - { - commandText += " LIMIT " + (query.Limit ?? int.MaxValue).ToString(CultureInfo.InvariantCulture); - } - - if (offset > 0) - { - commandText += " OFFSET " + offset.ToString(CultureInfo.InvariantCulture); - } - } - - var isReturningZeroItems = query.Limit.HasValue && query.Limit <= 0; - - var statementTexts = new List<string>(); - if (!isReturningZeroItems) - { - statementTexts.Add(commandText); - } - - if (query.EnableTotalRecordCount) - { - commandText = string.Empty; - - List<string> columnsToSelect; - if (EnableGroupByPresentationUniqueKey(query)) - { - columnsToSelect = new List<string> { "count (distinct PresentationUniqueKey)" }; - } - else if (query.GroupBySeriesPresentationUniqueKey) - { - columnsToSelect = new List<string> { "count (distinct SeriesPresentationUniqueKey)" }; - } - else - { - columnsToSelect = new List<string> { "count (guid)" }; - } - - SetFinalColumnsToSelect(query, columnsToSelect); - commandText += " select " + string.Join(',', columnsToSelect) + FromText; - - commandText += GetJoinUserDataText(query) - + whereText; - statementTexts.Add(commandText); - } - - var list = new List<Guid>(); - var result = new QueryResult<Guid>(); - using (var connection = GetConnection(true)) - { - connection.RunInTransaction( - db => - { - var statements = PrepareAll(db, statementTexts); - - if (!isReturningZeroItems) - { - using (var statement = statements[0]) - { - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } - - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - - // Running this again will bind the params - GetWhereClauses(query, statement); - - foreach (var row in statement.ExecuteQuery()) - { - list.Add(row[0].ReadGuidFromBlob()); - } - } - } - - if (query.EnableTotalRecordCount) - { - using (var statement = statements[statements.Length - 1]) - { - if (EnableJoinUserData(query)) - { - statement.TryBind("@UserId", query.User.InternalId); - } - - BindSimilarParams(query, statement); - BindSearchParams(query, statement); - - // Running this again will bind the params - GetWhereClauses(query, statement); - - result.TotalRecordCount = statement.ExecuteQuery().SelectScalarInt().First(); - } - } - }, - ReadTransactionMode); - } - - LogQueryTime("GetItemIds", commandText, now); - - result.Items = list; - return result; - } - private bool IsAlphaNumeric(string str) { if (string.IsNullOrWhiteSpace(str)) @@ -3665,7 +3429,7 @@ namespace Emby.Server.Implementations.Data whereClauses.Add($"ChannelId in ({inClause})"); } - if (!query.ParentId.Equals(Guid.Empty)) + if (!query.ParentId.Equals(default)) { whereClauses.Add("ParentId=@ParentId"); statement?.TryBind("@ParentId", query.ParentId); @@ -4025,7 +3789,7 @@ namespace Emby.Server.Implementations.Data clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))"); if (statement != null) { - statement.TryBind(paramName, artistId.ToByteArray()); + statement.TryBind(paramName, artistId); } index++; @@ -4046,7 +3810,7 @@ namespace Emby.Server.Implementations.Data clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=1))"); if (statement != null) { - statement.TryBind(paramName, artistId.ToByteArray()); + statement.TryBind(paramName, artistId); } index++; @@ -4067,7 +3831,7 @@ namespace Emby.Server.Implementations.Data clauses.Add("((select CleanName from TypedBaseItems where guid=" + paramName + ") in (select CleanValue from itemvalues where ItemId=Guid and Type=0) AND (select CleanName from TypedBaseItems where guid=" + paramName + ") not in (select CleanValue from itemvalues where ItemId=Guid and Type=1))"); if (statement != null) { - statement.TryBind(paramName, artistId.ToByteArray()); + statement.TryBind(paramName, artistId); } index++; @@ -4088,7 +3852,7 @@ namespace Emby.Server.Implementations.Data clauses.Add("Album in (select Name from typedbaseitems where guid=" + paramName + ")"); if (statement != null) { - statement.TryBind(paramName, albumId.ToByteArray()); + statement.TryBind(paramName, albumId); } index++; @@ -4109,7 +3873,7 @@ namespace Emby.Server.Implementations.Data clauses.Add("(guid not in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type<=1))"); if (statement != null) { - statement.TryBind(paramName, artistId.ToByteArray()); + statement.TryBind(paramName, artistId); } index++; @@ -4130,7 +3894,7 @@ namespace Emby.Server.Implementations.Data clauses.Add("(guid in (select itemid from itemvalues where CleanValue = (select CleanName from TypedBaseItems where guid=" + paramName + ") and Type=2))"); if (statement != null) { - statement.TryBind(paramName, genreId.ToByteArray()); + statement.TryBind(paramName, genreId); } index++; @@ -4209,7 +3973,7 @@ namespace Emby.Server.Implementations.Data if (statement != null) { - statement.TryBind(paramName, studioId.ToByteArray()); + statement.TryBind(paramName, studioId); } index++; @@ -4494,7 +4258,7 @@ namespace Emby.Server.Implementations.Data var index = 0; foreach (var pair in query.ExcludeProviderIds) { - if (string.Equals(pair.Key, MetadataProvider.TmdbCollection.ToString(), StringComparison.OrdinalIgnoreCase)) + if (string.Equals(pair.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.OrdinalIgnoreCase)) { continue; } @@ -4524,7 +4288,7 @@ namespace Emby.Server.Implementations.Data var index = 0; foreach (var pair in query.HasAnyProviderId) { - if (string.Equals(pair.Key, MetadataProvider.TmdbCollection.ToString(), StringComparison.OrdinalIgnoreCase)) + if (string.Equals(pair.Key, nameof(MetadataProvider.TmdbCollection), StringComparison.OrdinalIgnoreCase)) { continue; } @@ -4942,7 +4706,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type public void DeleteItem(Guid id) { - if (id == Guid.Empty) + if (id.Equals(default)) { throw new ArgumentNullException(nameof(id)); } @@ -4954,7 +4718,8 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type connection.RunInTransaction( db => { - var idBlob = id.ToByteArray(); + Span<byte> idBlob = stackalloc byte[16]; + id.TryWriteBytes(idBlob); // Delete people ExecuteWithSingleParam(db, "delete from People where ItemId=@Id", idBlob); @@ -5003,7 +4768,7 @@ where AncestorIdText not null and ItemValues.Value not null and ItemValues.Type if (whereClauses.Count != 0) { - commandText.Append(" where ").Append(string.Join(" AND ", whereClauses)); + commandText.Append(" where ").AppendJoin(" AND ", whereClauses); } commandText.Append(" order by ListOrder"); @@ -5089,16 +4854,16 @@ AND Type = @InternalPersonType)"); statement?.TryBind("@InternalPersonType", typeof(Person).FullName); } - if (!query.ItemId.Equals(Guid.Empty)) + if (!query.ItemId.Equals(default)) { whereClauses.Add("ItemId=@ItemId"); - statement?.TryBind("@ItemId", query.ItemId.ToByteArray()); + statement?.TryBind("@ItemId", query.ItemId); } - if (!query.AppearsInItemId.Equals(Guid.Empty)) + if (!query.AppearsInItemId.Equals(default)) { whereClauses.Add("p.Name in (Select Name from People where ItemId=@AppearsInItemId)"); - statement?.TryBind("@AppearsInItemId", query.AppearsInItemId.ToByteArray()); + statement?.TryBind("@AppearsInItemId", query.AppearsInItemId); } var queryPersonTypes = query.PersonTypes.Where(IsValidPersonType).ToList(); @@ -5151,7 +4916,7 @@ AND Type = @InternalPersonType)"); private void UpdateAncestors(Guid itemId, List<Guid> ancestorIds, IDatabaseConnection db, IStatement deleteAncestorsStatement) { - if (itemId.Equals(Guid.Empty)) + if (itemId.Equals(default)) { throw new ArgumentNullException(nameof(itemId)); } @@ -5599,6 +5364,7 @@ AND Type = @InternalPersonType)"); result.TotalRecordCount = list.Count; } + result.StartIndex = query.StartIndex ?? 0; result.Items = list; return result; @@ -5682,7 +5448,7 @@ AND Type = @InternalPersonType)"); private void UpdateItemValues(Guid itemId, List<(int MagicNumber, string Value)> values, IDatabaseConnection db) { - if (itemId.Equals(Guid.Empty)) + if (itemId.Equals(default)) { throw new ArgumentNullException(nameof(itemId)); } @@ -5758,7 +5524,7 @@ AND Type = @InternalPersonType)"); public void UpdatePeople(Guid itemId, List<PersonInfo> people) { - if (itemId.Equals(Guid.Empty)) + if (itemId.Equals(default)) { throw new ArgumentNullException(nameof(itemId)); } @@ -5891,7 +5657,7 @@ AND Type = @InternalPersonType)"); using (var statement = PrepareStatement(connection, cmdText)) { - statement.TryBind("@ItemId", query.ItemId.ToByteArray()); + statement.TryBind("@ItemId", query.ItemId); if (query.Type.HasValue) { @@ -5913,11 +5679,11 @@ AND Type = @InternalPersonType)"); } } - public void SaveMediaStreams(Guid id, List<MediaStream> streams, CancellationToken cancellationToken) + public void SaveMediaStreams(Guid id, IReadOnlyList<MediaStream> streams, CancellationToken cancellationToken) { CheckDisposed(); - if (id == Guid.Empty) + if (id.Equals(default)) { throw new ArgumentNullException(nameof(id)); } @@ -5945,7 +5711,7 @@ AND Type = @InternalPersonType)"); } } - private void InsertMediaStreams(byte[] idBlob, List<MediaStream> streams, IDatabaseConnection db) + private void InsertMediaStreams(byte[] idBlob, IReadOnlyList<MediaStream> streams, IDatabaseConnection db) { const int Limit = 10; var startIndex = 0; @@ -6204,6 +5970,7 @@ AND Type = @InternalPersonType)"); item.LocalizedUndefined = _localization.GetLocalizedString("Undefined"); item.LocalizedDefault = _localization.GetLocalizedString("Default"); item.LocalizedForced = _localization.GetLocalizedString("Forced"); + item.LocalizedExternal = _localization.GetLocalizedString("External"); } return item; @@ -6253,7 +6020,7 @@ AND Type = @InternalPersonType)"); CancellationToken cancellationToken) { CheckDisposed(); - if (id == Guid.Empty) + if (id.Equals(default)) { throw new ArgumentException("Guid can't be empty.", nameof(id)); } diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs index 80b8f9ebf..ba86dc156 100644 --- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs +++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs @@ -26,9 +26,6 @@ namespace Emby.Server.Implementations.Data DbFilePath = Path.Combine(appPaths.DataPath, "library.db"); } - /// <inheritdoc /> - public string Name => "SQLite"; - /// <summary> /// Opens the connection to the database. /// </summary> @@ -102,7 +99,7 @@ namespace Emby.Server.Implementations.Data continue; } - statement.TryBind("@UserId", user.Id.ToByteArray()); + statement.TryBind("@UserId", user.Id); statement.TryBind("@InternalUserId", user.InternalId); statement.MoveNext(); @@ -390,6 +387,7 @@ namespace Emby.Server.Implementations.Data return userData; } +#pragma warning disable CA2215 /// <inheritdoc/> /// <remarks> /// There is nothing to dispose here since <see cref="BaseSqliteRepository.WriteLock"/> and @@ -398,6 +396,10 @@ namespace Emby.Server.Implementations.Data /// </remarks> protected override void Dispose(bool dispose) { + // The write lock and connection for the item repository are shared with the user data repository + // since they point to the same database. The item repo has responsibility for disposing these two objects, + // so the user data repo should not attempt to dispose them as well } +#pragma warning restore CA2215 } } diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs index 7ba34e74a..efcfccafe 100644 --- a/Emby.Server.Implementations/Dto/DtoService.cs +++ b/Emby.Server.Implementations/Dto/DtoService.cs @@ -21,7 +21,6 @@ using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Playlists; using MediaBrowser.Controller.Providers; -using MediaBrowser.Model.Drawing; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Querying; @@ -458,11 +457,6 @@ namespace Emby.Server.Implementations.Dto } } - private string GetDtoId(BaseItem item) - { - return item.Id.ToString("N", CultureInfo.InvariantCulture); - } - private void SetMusicVideoProperties(BaseItemDto dto, MusicVideo item) { if (!string.IsNullOrEmpty(item.Album)) @@ -584,7 +578,7 @@ namespace Emby.Server.Implementations.Dto if (dictionary.TryGetValue(person.Name, out Person entity)) { baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary); - baseItemPerson.Id = entity.Id.ToString("N", CultureInfo.InvariantCulture); + baseItemPerson.Id = entity.Id; if (dto.ImageBlurHashes != null) { // Only add BlurHash for the person's image. @@ -743,8 +737,7 @@ namespace Emby.Server.Implementations.Dto dto.Tags = item.Tags; } - var hasAspectRatio = item as IHasAspectRatio; - if (hasAspectRatio != null) + if (item is IHasAspectRatio hasAspectRatio) { dto.AspectRatio = hasAspectRatio.AspectRatio; } @@ -894,15 +887,13 @@ namespace Emby.Server.Implementations.Dto dto.CommunityRating = item.CommunityRating; } - var supportsPlaceHolders = item as ISupportsPlaceHolders; - if (supportsPlaceHolders != null && supportsPlaceHolders.IsPlaceHolder) + if (item is ISupportsPlaceHolders supportsPlaceHolders && supportsPlaceHolders.IsPlaceHolder) { dto.IsPlaceHolder = supportsPlaceHolders.IsPlaceHolder; } // Add audio info - var audio = item as Audio; - if (audio != null) + if (item is Audio audio) { dto.Album = audio.Album; if (audio.ExtraType.HasValue) @@ -975,8 +966,7 @@ namespace Emby.Server.Implementations.Dto }).Where(i => i != null).ToArray(); } - var hasAlbumArtist = item as IHasAlbumArtist; - if (hasAlbumArtist != null) + if (item is IHasAlbumArtist hasAlbumArtist) { dto.AlbumArtist = hasAlbumArtist.AlbumArtists.FirstOrDefault(); @@ -1102,12 +1092,13 @@ namespace Emby.Server.Implementations.Dto if (options.ContainsField(ItemFields.LocalTrailerCount)) { - allExtras ??= item.GetExtras().ToArray(); - dto.LocalTrailerCount = allExtras.Count(i => i.ExtraType == ExtraType.Trailer); - if (item is IHasTrailers hasTrailers) { - dto.LocalTrailerCount += hasTrailers.GetTrailerCount(); + dto.LocalTrailerCount = hasTrailers.GetTrailerCount(); + } + else + { + dto.LocalTrailerCount = (allExtras ?? item.GetExtras()).Count(i => i.ExtraType == ExtraType.Trailer); } } @@ -1317,35 +1308,35 @@ namespace Emby.Server.Implementations.Dto var allImages = parent.ImageInfos; - if (logoLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && dto.ParentLogoItemId == null) + if (logoLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Logo)) && dto.ParentLogoItemId is null) { var image = allImages.FirstOrDefault(i => i.Type == ImageType.Logo); if (image != null) { - dto.ParentLogoItemId = GetDtoId(parent); + dto.ParentLogoItemId = parent.Id; dto.ParentLogoImageTag = GetTagAndFillBlurhash(dto, parent, image); } } - if (artLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && dto.ParentArtItemId == null) + if (artLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Art)) && dto.ParentArtItemId is null) { var image = allImages.FirstOrDefault(i => i.Type == ImageType.Art); if (image != null) { - dto.ParentArtItemId = GetDtoId(parent); + dto.ParentArtItemId = parent.Id; dto.ParentArtImageTag = GetTagAndFillBlurhash(dto, parent, image); } } - if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId == null || parent is Series) && parent is not ICollectionFolder && parent is not UserView) + if (thumbLimit > 0 && !(imageTags != null && imageTags.ContainsKey(ImageType.Thumb)) && (dto.ParentThumbItemId is null || parent is Series) && parent is not ICollectionFolder && parent is not UserView) { var image = allImages.FirstOrDefault(i => i.Type == ImageType.Thumb); if (image != null) { - dto.ParentThumbItemId = GetDtoId(parent); + dto.ParentThumbItemId = parent.Id; dto.ParentThumbImageTag = GetTagAndFillBlurhash(dto, parent, image); } } @@ -1356,7 +1347,7 @@ namespace Emby.Server.Implementations.Dto if (images.Count > 0) { - dto.ParentBackdropItemId = GetDtoId(parent); + dto.ParentBackdropItemId = parent.Id; dto.ParentBackdropImageTags = GetTagsAndFillBlurhashes(dto, parent, ImageType.Backdrop, images); } } diff --git a/Emby.Server.Implementations/Emby.Server.Implementations.csproj b/Emby.Server.Implementations/Emby.Server.Implementations.csproj index 1e09a98cf..886da1390 100644 --- a/Emby.Server.Implementations/Emby.Server.Implementations.csproj +++ b/Emby.Server.Implementations/Emby.Server.Implementations.csproj @@ -26,12 +26,12 @@ <PackageReference Include="DiscUtils.Udf" Version="0.16.13" /> <PackageReference Include="Jellyfin.XmlTv" Version="10.6.2" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" /> - <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.0" /> + <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="6.0.1" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="6.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.1" /> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="6.0.3" /> <PackageReference Include="Mono.Nat" Version="3.0.2" /> - <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.2" /> + <PackageReference Include="prometheus-net.DotNetRuntime" Version="4.2.3" /> <PackageReference Include="sharpcompress" Version="0.30.1" /> <PackageReference Include="SQLitePCL.pretty.netstandard" Version="3.1.0" /> <PackageReference Include="DotNet.Glob" Version="3.1.3" /> @@ -55,6 +55,10 @@ <!-- Code Analyzers--> <ItemGroup Condition=" '$(Configuration)' == 'Debug' "> + <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets> + </PackageReference> <PackageReference Include="SerilogAnalyzer" Version="0.15.0" PrivateAssets="All" /> <PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.376" PrivateAssets="All" /> <PackageReference Include="SmartAnalyzers.MultithreadingAnalyzer" Version="1.1.31" PrivateAssets="All" /> diff --git a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs index d43996c69..9e35d83aa 100644 --- a/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs +++ b/Emby.Server.Implementations/EntryPoints/LibraryChangedNotifier.cs @@ -326,7 +326,7 @@ namespace Emby.Server.Implementations.EntryPoints { var userIds = _sessionManager.Sessions .Select(i => i.UserId) - .Where(i => !i.Equals(Guid.Empty)) + .Where(i => !i.Equals(default)) .Distinct() .ToArray(); diff --git a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index feaccf9fa..e45baedd7 100644 --- a/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs +++ b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs @@ -3,6 +3,8 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Emby.Server.Implementations.Udp; +using Jellyfin.Networking.Configuration; +using MediaBrowser.Common.Configuration; using MediaBrowser.Controller; using MediaBrowser.Controller.Plugins; using Microsoft.Extensions.Configuration; @@ -26,6 +28,7 @@ namespace Emby.Server.Implementations.EntryPoints private readonly ILogger<UdpServerEntryPoint> _logger; private readonly IServerApplicationHost _appHost; private readonly IConfiguration _config; + private readonly IConfigurationManager _configurationManager; /// <summary> /// The UDP server. @@ -40,14 +43,17 @@ namespace Emby.Server.Implementations.EntryPoints /// <param name="logger">Instance of the <see cref="ILogger{UdpServerEntryPoint}"/> interface.</param> /// <param name="appHost">Instance of the <see cref="IServerApplicationHost"/> interface.</param> /// <param name="configuration">Instance of the <see cref="IConfiguration"/> interface.</param> + /// <param name="configurationManager">Instance of the <see cref="IConfigurationManager"/> interface.</param> public UdpServerEntryPoint( ILogger<UdpServerEntryPoint> logger, IServerApplicationHost appHost, - IConfiguration configuration) + IConfiguration configuration, + IConfigurationManager configurationManager) { _logger = logger; _appHost = appHost; _config = configuration; + _configurationManager = configurationManager; } /// <inheritdoc /> @@ -55,6 +61,11 @@ namespace Emby.Server.Implementations.EntryPoints { CheckDisposed(); + if (!_configurationManager.GetNetworkConfiguration().AutoDiscovery) + { + return Task.CompletedTask; + } + try { _udpServer = new UdpServer(_logger, _appHost, _config, PortNumber); diff --git a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs index bb6041f28..15ab363fe 100644 --- a/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs +++ b/Emby.Server.Implementations/HttpServer/Security/SessionContext.cs @@ -47,7 +47,9 @@ namespace Emby.Server.Implementations.HttpServer.Security { var session = await GetSession(requestContext).ConfigureAwait(false); - return session.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(session.UserId); + return session.UserId.Equals(default) + ? null + : _userManager.GetUserById(session.UserId); } public Task<User?> GetUser(object requestContext) diff --git a/Emby.Server.Implementations/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 5c86dbbb7..399ece7fd 100644 --- a/Emby.Server.Implementations/IO/ManagedFileSystem.cs +++ b/Emby.Server.Implementations/IO/ManagedFileSystem.cs @@ -581,7 +581,7 @@ namespace Emby.Server.Implementations.IO } /// <inheritdoc /> - public virtual List<FileSystemMetadata> GetDrives() + public virtual IEnumerable<FileSystemMetadata> GetDrives() { // check for ready state to avoid waiting for drives to timeout // some drives on linux have no actual size or are used for other purposes @@ -595,7 +595,7 @@ namespace Emby.Server.Implementations.IO Name = d.Name, FullName = d.RootDirectory.FullName, IsDirectory = true - }).ToList(); + }); } /// <inheritdoc /> @@ -704,6 +704,18 @@ namespace Emby.Server.Implementations.IO return Directory.EnumerateFileSystemEntries(path, "*", GetEnumerationOptions(recursive)); } + /// <inheritdoc /> + public virtual bool DirectoryExists(string path) + { + return Directory.Exists(path); + } + + /// <inheritdoc /> + public virtual bool FileExists(string path) + { + return File.Exists(path); + } + private EnumerationOptions GetEnumerationOptions(bool recursive) { return new EnumerationOptions diff --git a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs index 758986945..57c2f1a5e 100644 --- a/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseDynamicImageProvider.cs @@ -135,7 +135,7 @@ namespace Emby.Server.Implementations.Images protected virtual IEnumerable<string> GetStripCollageImagePaths(BaseItem primaryItem, IEnumerable<BaseItem> items) { - var useBackdrop = primaryItem is CollectionFolder; + var useBackdrop = primaryItem is CollectionFolder || primaryItem is UserView; return items .Select(i => { diff --git a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs index 1c69056d2..6fc7f1ac3 100644 --- a/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/BaseFolderImageProvider.cs @@ -22,7 +22,7 @@ namespace Emby.Server.Implementations.Images { private readonly ILibraryManager _libraryManager; - public BaseFolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) + protected BaseFolderImageProvider(IFileSystem fileSystem, IProviderManager providerManager, IApplicationPaths applicationPaths, IImageProcessor imageProcessor, ILibraryManager libraryManager) : base(fileSystem, providerManager, applicationPaths, imageProcessor) { _libraryManager = libraryManager; diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs index 7958eb8f5..8a0e627b9 100644 --- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs +++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs @@ -42,6 +42,10 @@ namespace Emby.Server.Implementations.Images { includeItemTypes = new[] { BaseItemKind.MusicAlbum }; } + else if (string.Equals(viewType, CollectionType.MusicVideos, StringComparison.Ordinal)) + { + includeItemTypes = new[] { BaseItemKind.MusicVideo }; + } else if (string.Equals(viewType, CollectionType.Books, StringComparison.Ordinal)) { includeItemTypes = new[] { BaseItemKind.Book, BaseItemKind.AudioBook }; diff --git a/Emby.Server.Implementations/Images/DynamicImageProvider.cs b/Emby.Server.Implementations/Images/DynamicImageProvider.cs index 575680653..9f9a4902a 100644 --- a/Emby.Server.Implementations/Images/DynamicImageProvider.cs +++ b/Emby.Server.Implementations/Images/DynamicImageProvider.cs @@ -84,16 +84,20 @@ namespace Emby.Server.Implementations.Images }).GroupBy(x => x.Id) .Select(x => x.First()); + List<BaseItem> returnItems; if (isUsingCollectionStrip) { - return items + returnItems = items .Where(i => i.HasImage(ImageType.Primary) || i.HasImage(ImageType.Thumb)) .ToList(); + returnItems.Shuffle(); + return returnItems; } - - return items + returnItems = items .Where(i => i.HasImage(ImageType.Primary)) .ToList(); + returnItems.Shuffle(); + return returnItems; } protected override bool Supports(BaseItem item) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index bd0c178fd..a9428ae9b 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -45,8 +45,8 @@ using MediaBrowser.Model.IO; using MediaBrowser.Model.Library; using MediaBrowser.Model.Querying; using MediaBrowser.Model.Tasks; -using MediaBrowser.Providers.MediaInfo; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Episode = MediaBrowser.Controller.Entities.TV.Episode; using EpisodeInfo = Emby.Naming.TV.EpisodeInfo; @@ -100,7 +100,7 @@ namespace Emby.Server.Implementations.Library /// Initializes a new instance of the <see cref="LibraryManager" /> class. /// </summary> /// <param name="appHost">The application host.</param> - /// <param name="logger">The logger.</param> + /// <param name="loggerFactory">The logger factory.</param> /// <param name="taskManager">The task manager.</param> /// <param name="userManager">The user manager.</param> /// <param name="configurationManager">The configuration manager.</param> @@ -116,7 +116,7 @@ namespace Emby.Server.Implementations.Library /// <param name="namingOptions">The naming options.</param> public LibraryManager( IServerApplicationHost appHost, - ILogger<LibraryManager> logger, + ILoggerFactory loggerFactory, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, @@ -132,7 +132,7 @@ namespace Emby.Server.Implementations.Library NamingOptions namingOptions) { _appHost = appHost; - _logger = logger; + _logger = loggerFactory.CreateLogger<LibraryManager>(); _taskManager = taskManager; _userManager = userManager; _configurationManager = configurationManager; @@ -147,7 +147,7 @@ namespace Emby.Server.Implementations.Library _memoryCache = memoryCache; _namingOptions = namingOptions; - _extraResolver = new ExtraResolver(namingOptions); + _extraResolver = new ExtraResolver(loggerFactory.CreateLogger<ExtraResolver>(), namingOptions); _configurationManager.ConfigurationUpdated += ConfigurationUpdated; @@ -680,9 +680,7 @@ namespace Emby.Server.Implementations.Library if (result?.Items.Count > 0) { - var items = new List<BaseItem>(); - items.AddRange(result.Items); - + var items = result.Items; foreach (var item in items) { ResolverHelper.SetInitialItemValues(item, parent, this, directoryService); @@ -758,7 +756,7 @@ namespace Emby.Server.Implementations.Library Path = path }; - if (folder.Id.Equals(Guid.Empty)) + if (folder.Id.Equals(default)) { if (string.IsNullOrEmpty(folder.Path)) { @@ -777,7 +775,7 @@ namespace Emby.Server.Implementations.Library folder = dbItem; } - if (folder.ParentId != rootFolder.Id) + if (!folder.ParentId.Equals(rootFolder.Id)) { folder.ParentId = rootFolder.Id; folder.UpdateToRepositoryAsync(ItemUpdateType.MetadataImport, CancellationToken.None).GetAwaiter().GetResult(); @@ -1007,14 +1005,8 @@ namespace Emby.Server.Implementations.Library return GetNewItemIdInternal(path, typeof(T), forceCaseInsensitiveId); } - /// <summary> - /// Validate and refresh the People sub-set of the IBN. - /// The items are stored in the db but not loaded into memory until actually requested by an operation. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <param name="progress">The progress.</param> - /// <returns>Task.</returns> - public Task ValidatePeople(CancellationToken cancellationToken, IProgress<double> progress) + /// <inheritdoc /> + public Task ValidatePeopleAsync(IProgress<double> progress, CancellationToken cancellationToken) { // Ensure the location is available. Directory.CreateDirectory(_configurationManager.ApplicationPaths.PeoplePath); @@ -1037,15 +1029,6 @@ namespace Emby.Server.Implementations.Library } /// <summary> - /// Queues the library scan. - /// </summary> - public void QueueLibraryScan() - { - // Just run the scheduled task so that the user can see it - _taskManager.QueueScheduledTask<RefreshMediaLibraryTask>(); - } - - /// <summary> /// Validates the media library internal. /// </summary> /// <param name="progress">The progress.</param> @@ -1270,7 +1253,7 @@ namespace Emby.Server.Implementations.Library /// <exception cref="ArgumentNullException"><paramref name="id"/> is <c>null</c>.</exception> public BaseItem GetItemById(Guid id) { - if (id == Guid.Empty) + if (id.Equals(default)) { throw new ArgumentException("Guid can't be empty", nameof(id)); } @@ -1292,7 +1275,7 @@ namespace Emby.Server.Implementations.Library public List<BaseItem> GetItemList(InternalItemsQuery query, bool allowExternalContent) { - if (query.Recursive && query.ParentId != Guid.Empty) + if (query.Recursive && !query.ParentId.Equals(default)) { var parent = GetItemById(query.ParentId); if (parent != null) @@ -1316,7 +1299,7 @@ namespace Emby.Server.Implementations.Library public int GetCount(InternalItemsQuery query) { - if (query.Recursive && !query.ParentId.Equals(Guid.Empty)) + if (query.Recursive && !query.ParentId.Equals(default)) { var parent = GetItemById(query.ParentId); if (parent != null) @@ -1360,10 +1343,10 @@ namespace Emby.Server.Implementations.Library return _itemRepository.GetItems(query); } - return new QueryResult<BaseItem> - { - Items = _itemRepository.GetItemList(query) - }; + return new QueryResult<BaseItem>( + query.StartIndex, + null, + _itemRepository.GetItemList(query)); } public List<Guid> GetItemIds(InternalItemsQuery query) @@ -1474,7 +1457,7 @@ namespace Emby.Server.Implementations.Library public QueryResult<BaseItem> GetItemsResult(InternalItemsQuery query) { - if (query.Recursive && !query.ParentId.Equals(Guid.Empty)) + if (query.Recursive && !query.ParentId.Equals(default)) { var parent = GetItemById(query.ParentId); if (parent != null) @@ -1493,10 +1476,10 @@ namespace Emby.Server.Implementations.Library return _itemRepository.GetItems(query); } - return new QueryResult<BaseItem> - { - Items = _itemRepository.GetItemList(query) - }; + return new QueryResult<BaseItem>( + query.StartIndex, + null, + _itemRepository.GetItemList(query)); } private void SetTopParentIdsOrAncestors(InternalItemsQuery query, List<BaseItem> parents) @@ -1530,7 +1513,7 @@ namespace Emby.Server.Implementations.Library private void AddUserToQuery(InternalItemsQuery query, User user, bool allowExternalContent = true) { if (query.AncestorIds.Length == 0 && - query.ParentId.Equals(Guid.Empty) && + query.ParentId.Equals(default) && query.ChannelIds.Count == 0 && query.TopParentIds.Length == 0 && string.IsNullOrEmpty(query.AncestorWithPresentationUniqueKey) && @@ -1558,7 +1541,7 @@ namespace Emby.Server.Implementations.Library } // Translate view into folders - if (!view.DisplayParentId.Equals(Guid.Empty)) + if (!view.DisplayParentId.Equals(default)) { var displayParent = GetItemById(view.DisplayParentId); if (displayParent != null) @@ -1569,7 +1552,7 @@ namespace Emby.Server.Implementations.Library return Array.Empty<Guid>(); } - if (!view.ParentId.Equals(Guid.Empty)) + if (!view.ParentId.Equals(default)) { var displayParent = GetItemById(view.ParentId); if (displayParent != null) @@ -1651,27 +1634,6 @@ namespace Emby.Server.Implementations.Library } /// <summary> - /// Gets all intro files. - /// </summary> - /// <returns>IEnumerable{System.String}.</returns> - public IEnumerable<string> GetAllIntroFiles() - { - return IntroProviders.SelectMany(i => - { - try - { - return i.GetAllIntroFiles().ToList(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting intro files"); - - return new List<string>(); - } - }); - } - - /// <summary> /// Resolves the intro. /// </summary> /// <param name="info">The info.</param> @@ -2014,16 +1976,16 @@ namespace Emby.Server.Implementations.Library public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken) => UpdateItemsAsync(new[] { item }, parent, updateReason, cancellationToken); - public Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason) + public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason) { if (item.IsFileProtocol) { - ProviderManager.SaveMetadata(item, updateReason); + await ProviderManager.SaveMetadataAsync(item, updateReason).ConfigureAwait(false); } item.DateLastSaved = DateTime.UtcNow; - return UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate); + await UpdateImagesAsync(item, updateReason >= ItemUpdateType.ImageUpdate).ConfigureAwait(false); } /// <summary> @@ -2192,7 +2154,7 @@ namespace Emby.Server.Implementations.Library return null; } - while (!item.ParentId.Equals(Guid.Empty)) + while (!item.ParentId.Equals(default)) { var parent = item.GetParent(); if (parent == null || parent is AggregateFolder) @@ -2270,7 +2232,9 @@ namespace Emby.Server.Implementations.Library string viewType, string sortName) { - var parentIdString = parentId.Equals(Guid.Empty) ? null : parentId.ToString("N", CultureInfo.InvariantCulture); + var parentIdString = parentId.Equals(default) + ? null + : parentId.ToString("N", CultureInfo.InvariantCulture); var idValues = "38_namedview_" + name + user.Id.ToString("N", CultureInfo.InvariantCulture) + (parentIdString ?? string.Empty) + (viewType ?? string.Empty); var id = GetNewItemId(idValues, typeof(UserView)); @@ -2304,7 +2268,7 @@ namespace Emby.Server.Implementations.Library var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; - if (!refresh && !item.DisplayParentId.Equals(Guid.Empty)) + if (!refresh && !item.DisplayParentId.Equals(default)) { var displayParent = GetItemById(item.DisplayParentId); refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; @@ -2371,7 +2335,7 @@ namespace Emby.Server.Implementations.Library var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; - if (!refresh && !item.DisplayParentId.Equals(Guid.Empty)) + if (!refresh && !item.DisplayParentId.Equals(default)) { var displayParent = GetItemById(item.DisplayParentId); refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; @@ -2404,7 +2368,9 @@ namespace Emby.Server.Implementations.Library throw new ArgumentNullException(nameof(name)); } - var parentIdString = parentId.Equals(Guid.Empty) ? null : parentId.ToString("N", CultureInfo.InvariantCulture); + var parentIdString = parentId.Equals(default) + ? null + : parentId.ToString("N", CultureInfo.InvariantCulture); var idValues = "37_namedview_" + name + (parentIdString ?? string.Empty) + (viewType ?? string.Empty); if (!string.IsNullOrEmpty(uniqueId)) { @@ -2448,7 +2414,7 @@ namespace Emby.Server.Implementations.Library var refresh = isNew || DateTime.UtcNow - item.DateLastRefreshed >= _viewRefreshInterval; - if (!refresh && !item.DisplayParentId.Equals(Guid.Empty)) + if (!refresh && !item.DisplayParentId.Equals(default)) { var displayParent = GetItemById(item.DisplayParentId); refresh = displayParent != null && displayParent.DateLastSaved > item.DateLastRefreshed; @@ -2469,24 +2435,6 @@ namespace Emby.Server.Implementations.Library return item; } - public void AddExternalSubtitleStreams( - List<MediaStream> streams, - string videoPath, - string[] files) - { - new SubtitleResolver(BaseItem.LocalizationManager).AddExternalSubtitleStreams(streams, videoPath, streams.Count, files); - } - - public BaseItem GetParentItem(string parentId, Guid? userId) - { - if (string.IsNullOrEmpty(parentId)) - { - return GetParentItem((Guid?)null, userId); - } - - return GetParentItem(new Guid(parentId), userId); - } - public BaseItem GetParentItem(Guid? parentId, Guid? userId) { if (parentId.HasValue) @@ -2494,7 +2442,7 @@ namespace Emby.Server.Implementations.Library return GetItemById(parentId.Value); } - if (userId.HasValue && userId != Guid.Empty) + if (userId.HasValue && !userId.Equals(default)) { return GetUserRootFolder(); } @@ -2686,7 +2634,7 @@ namespace Emby.Server.Implementations.Library }; } - public IEnumerable<BaseItem> FindExtras(BaseItem owner, List<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) + public IEnumerable<BaseItem> FindExtras(BaseItem owner, IReadOnlyList<FileSystemMetadata> fileSystemChildren, IDirectoryService directoryService) { var ownerVideoInfo = VideoResolver.Resolve(owner.Path, owner.IsFolder, _namingOptions); if (ownerVideoInfo == null) @@ -2784,16 +2732,6 @@ namespace Emby.Server.Implementations.Library return path; } - public string SubstitutePath(string path, string from, string to) - { - if (path.TryReplaceSubPath(from, to, out var newPath)) - { - return newPath; - } - - return path; - } - public List<PersonInfo> GetPeople(InternalPeopleQuery query) { return _itemRepository.GetPeople(query); diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index a414e7e16..c9202c264 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -344,7 +344,7 @@ namespace Emby.Server.Implementations.Library return sources; } - private string[] NormalizeLanguage(string language) + private IReadOnlyList<string> NormalizeLanguage(string language) { if (string.IsNullOrEmpty(language)) { @@ -514,10 +514,10 @@ namespace Emby.Server.Implementations.Library _logger.LogInformation("Live stream opened: {@MediaSource}", mediaSource); var clone = JsonSerializer.Deserialize<MediaSourceInfo>(json, _jsonOptions); - if (!request.UserId.Equals(Guid.Empty)) + if (!request.UserId.Equals(default)) { var user = _userManager.GetUserById(request.UserId); - var item = request.ItemId.Equals(Guid.Empty) + var item = request.ItemId.Equals(default) ? null : _libraryManager.GetItemById(request.ItemId); SetDefaultAudioAndSubtitleStreamIndexes(item, clone, user); diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs index da0c89c13..c5abb9a0a 100644 --- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs +++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -13,10 +11,9 @@ namespace Emby.Server.Implementations.Library { public static class MediaStreamSelector { - public static int? GetDefaultAudioStreamIndex(List<MediaStream> streams, string[] preferredLanguages, bool preferDefaultTrack) + public static int? GetDefaultAudioStreamIndex(IReadOnlyList<MediaStream> streams, IReadOnlyList<string> preferredLanguages, bool preferDefaultTrack) { - streams = GetSortedStreams(streams, MediaStreamType.Audio, preferredLanguages) - .ToList(); + var sortedStreams = GetSortedStreams(streams, MediaStreamType.Audio, preferredLanguages); if (preferDefaultTrack) { @@ -28,24 +25,15 @@ namespace Emby.Server.Implementations.Library } } - var stream = streams.FirstOrDefault(); - - if (stream != null) - { - return stream.Index; - } - - return null; + return sortedStreams.FirstOrDefault()?.Index; } public static int? GetDefaultSubtitleStreamIndex( IEnumerable<MediaStream> streams, - string[] preferredLanguages, + IReadOnlyList<string> preferredLanguages, SubtitlePlaybackMode mode, string audioTrackLanguage) { - MediaStream stream = null; - if (mode == SubtitlePlaybackMode.None) { return null; @@ -59,6 +47,7 @@ namespace Emby.Server.Implementations.Library .ThenByDescending(x => x.IsDefault) .ToList(); + MediaStream? stream = null; if (mode == SubtitlePlaybackMode.Default) { // Prefer embedded metadata over smart logic @@ -95,26 +84,27 @@ namespace Emby.Server.Implementations.Library return stream?.Index; } - private static IEnumerable<MediaStream> GetSortedStreams(IEnumerable<MediaStream> streams, MediaStreamType type, string[] languagePreferences) + private static IEnumerable<MediaStream> GetSortedStreams(IEnumerable<MediaStream> streams, MediaStreamType type, IReadOnlyList<string> languagePreferences) { // Give some preference to external text subs for better performance - return streams.Where(i => i.Type == type) + return streams + .Where(i => i.Type == type) .OrderBy(i => - { - var index = FindIndex(languagePreferences, i.Language); - - return index == -1 ? 100 : index; - }) - .ThenBy(i => GetBooleanOrderBy(i.IsDefault)) - .ThenBy(i => GetBooleanOrderBy(i.SupportsExternalStream)) - .ThenBy(i => GetBooleanOrderBy(i.IsTextSubtitleStream)) - .ThenBy(i => GetBooleanOrderBy(i.IsExternal)) - .ThenBy(i => i.Index); + { + var index = languagePreferences.FindIndex(x => string.Equals(x, i.Language, StringComparison.OrdinalIgnoreCase)); + + return index == -1 ? 100 : index; + }) + .ThenBy(i => GetBooleanOrderBy(i.IsDefault)) + .ThenBy(i => GetBooleanOrderBy(i.SupportsExternalStream)) + .ThenBy(i => GetBooleanOrderBy(i.IsTextSubtitleStream)) + .ThenBy(i => GetBooleanOrderBy(i.IsExternal)) + .ThenBy(i => i.Index); } public static void SetSubtitleStreamScores( - List<MediaStream> streams, - string[] preferredLanguages, + IReadOnlyList<MediaStream> streams, + IReadOnlyList<string> preferredLanguages, SubtitlePlaybackMode mode, string audioTrackLanguage) { @@ -123,15 +113,14 @@ namespace Emby.Server.Implementations.Library return; } - streams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages) - .ToList(); + var sortedStreams = GetSortedStreams(streams, MediaStreamType.Subtitle, preferredLanguages); var filteredStreams = new List<MediaStream>(); if (mode == SubtitlePlaybackMode.Default) { // Prefer embedded metadata over smart logic - filteredStreams = streams.Where(s => s.IsForced || s.IsDefault) + filteredStreams = sortedStreams.Where(s => s.IsForced || s.IsDefault) .ToList(); } else if (mode == SubtitlePlaybackMode.Smart) @@ -139,54 +128,37 @@ namespace Emby.Server.Implementations.Library // Prefer smart logic over embedded metadata if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) { - filteredStreams = streams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase)) + filteredStreams = sortedStreams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase)) .ToList(); } } else if (mode == SubtitlePlaybackMode.Always) { // always load the most suitable full subtitles - filteredStreams = streams.Where(s => !s.IsForced) - .ToList(); + filteredStreams = sortedStreams.Where(s => !s.IsForced).ToList(); } else if (mode == SubtitlePlaybackMode.OnlyForced) { // always load the most suitable full subtitles - filteredStreams = streams.Where(s => s.IsForced).ToList(); + filteredStreams = sortedStreams.Where(s => s.IsForced).ToList(); } // load forced subs if we have found no suitable full subtitles - if (filteredStreams.Count == 0) - { - filteredStreams = streams - .Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) - .ToList(); - } + var iterStreams = filteredStreams.Count == 0 + ? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) + : filteredStreams; - foreach (var stream in filteredStreams) + foreach (var stream in iterStreams) { stream.Score = GetSubtitleScore(stream, preferredLanguages); } } - private static int FindIndex(string[] list, string value) - { - for (var i = 0; i < list.Length; i++) - { - if (string.Equals(list[i], value, StringComparison.OrdinalIgnoreCase)) - { - return i; - } - } - - return -1; - } - - private static int GetSubtitleScore(MediaStream stream, string[] languagePreferences) + private static int GetSubtitleScore(MediaStream stream, IReadOnlyList<string> languagePreferences) { var values = new List<int>(); - var index = FindIndex(languagePreferences, stream.Language); + var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase)); values.Add(index == -1 ? 0 : 100 - index); diff --git a/Emby.Server.Implementations/Library/MusicManager.cs b/Emby.Server.Implementations/Library/MusicManager.cs index d35e74e7b..b2439a87e 100644 --- a/Emby.Server.Implementations/Library/MusicManager.cs +++ b/Emby.Server.Implementations/Library/MusicManager.cs @@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.Library { return Guid.Empty; } - }).Where(i => !i.Equals(Guid.Empty)).ToArray(); + }).Where(i => !i.Equals(default)).ToArray(); return GetInstantMixFromGenreIds(genreIds, user, dtoOptions); } diff --git a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs index 9222a9479..3d6b9f3b6 100644 --- a/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/BaseVideoResolver.cs @@ -12,6 +12,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Entities; +using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Library.Resolvers { @@ -22,8 +23,11 @@ namespace Emby.Server.Implementations.Library.Resolvers public abstract class BaseVideoResolver<T> : MediaBrowser.Controller.Resolvers.ItemResolver<T> where T : Video, new() { - protected BaseVideoResolver(NamingOptions namingOptions) + private readonly ILogger _logger; + + protected BaseVideoResolver(ILogger logger, NamingOptions namingOptions) { + _logger = logger; NamingOptions = namingOptions; } @@ -156,19 +160,26 @@ namespace Emby.Server.Implementations.Library.Resolvers } else { - // use disc-utils, both DVDs and BDs use UDF filesystem - using (var videoFileStream = File.Open(video.Path, FileMode.Open, FileAccess.Read)) - using (UdfReader udfReader = new UdfReader(videoFileStream)) + try { - if (udfReader.DirectoryExists("VIDEO_TS")) - { - video.IsoType = IsoType.Dvd; - } - else if (udfReader.DirectoryExists("BDMV")) + // use disc-utils, both DVDs and BDs use UDF filesystem + using (var videoFileStream = File.Open(video.Path, FileMode.Open, FileAccess.Read)) + using (UdfReader udfReader = new UdfReader(videoFileStream)) { - video.IsoType = IsoType.BluRay; + if (udfReader.DirectoryExists("VIDEO_TS")) + { + video.IsoType = IsoType.Dvd; + } + else if (udfReader.DirectoryExists("BDMV")) + { + video.IsoType = IsoType.BluRay; + } } } + catch (Exception ex) + { + _logger.LogError(ex, "Error opening UDF/ISO image: {Value}", video.Path ?? video.Name); + } } } } diff --git a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs index 807913b5d..408e640f9 100644 --- a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs @@ -6,6 +6,7 @@ using Emby.Naming.Video; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.Entities; +using Microsoft.Extensions.Logging; using static Emby.Naming.Video.ExtraRuleResolver; namespace Emby.Server.Implementations.Library.Resolvers @@ -22,12 +23,13 @@ namespace Emby.Server.Implementations.Library.Resolvers /// <summary> /// Initializes a new instance of the <see cref="ExtraResolver"/> class. /// </summary> + /// <param name="logger">The logger.</param> /// <param name="namingOptions">An instance of <see cref="NamingOptions"/>.</param> - public ExtraResolver(NamingOptions namingOptions) + public ExtraResolver(ILogger<ExtraResolver> logger, NamingOptions namingOptions) { _namingOptions = namingOptions; - _trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(namingOptions) }; - _videoResolvers = new IItemResolver[] { new GenericVideoResolver<Video>(namingOptions) }; + _trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions) }; + _videoResolvers = new IItemResolver[] { new GenericVideoResolver<Video>(logger, namingOptions) }; } /// <summary> diff --git a/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs index b8554bd51..5e33b402d 100644 --- a/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs @@ -1,7 +1,8 @@ -#nullable disable +#nullable disable using Emby.Naming.Common; using MediaBrowser.Controller.Entities; +using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Library.Resolvers { @@ -15,9 +16,10 @@ namespace Emby.Server.Implementations.Library.Resolvers /// <summary> /// Initializes a new instance of the <see cref="GenericVideoResolver{T}"/> class. /// </summary> + /// <param name="logger">The logger.</param> /// <param name="namingOptions">The naming options.</param> - public GenericVideoResolver(NamingOptions namingOptions) - : base(namingOptions) + public GenericVideoResolver(ILogger logger, NamingOptions namingOptions) + : base(logger, namingOptions) { } } diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs index 1a9295dc8..140c4272e 100644 --- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs @@ -17,6 +17,7 @@ using MediaBrowser.Controller.Providers; using MediaBrowser.Controller.Resolvers; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; +using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Library.Resolvers.Movies { @@ -32,7 +33,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies CollectionType.Movies, CollectionType.HomeVideos, CollectionType.MusicVideos, - CollectionType.Movies, + CollectionType.TvShows, CollectionType.Photos }; @@ -40,9 +41,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies /// Initializes a new instance of the <see cref="MovieResolver"/> class. /// </summary> /// <param name="imageProcessor">The image processor.</param> + /// <param name="logger">The logger.</param> /// <param name="namingOptions">The naming options.</param> - public MovieResolver(IImageProcessor imageProcessor, NamingOptions namingOptions) - : base(namingOptions) + public MovieResolver(IImageProcessor imageProcessor, ILogger<MovieResolver> logger, NamingOptions namingOptions) + : base(logger, namingOptions) { _imageProcessor = imageProcessor; } @@ -128,10 +130,9 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return movie?.ExtraType == null ? movie : null; } - // Owned items will be caught by the video extra resolver if (args.Parent == null) { - return null; + return base.Resolve(args); } if (IsInvalid(args.Parent, collectionType)) @@ -222,6 +223,11 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies return ResolveVideos<Movie>(parent, files, true, collectionType, true); } + if (string.Equals(collectionType, CollectionType.TvShows, StringComparison.OrdinalIgnoreCase)) + { + return ResolveVideos<Episode>(parent, files, true, collectionType, true); + } + return null; } diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs index be9905647..bfa73af2f 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/EpisodeResolver.cs @@ -6,6 +6,7 @@ using Emby.Naming.Common; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Model.Entities; +using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.Library.Resolvers.TV { @@ -17,9 +18,10 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// <summary> /// Initializes a new instance of the <see cref="EpisodeResolver"/> class. /// </summary> + /// <param name="logger">The logger.</param> /// <param name="namingOptions">The naming options.</param> - public EpisodeResolver(NamingOptions namingOptions) - : base(namingOptions) + public EpisodeResolver(ILogger<EpisodeResolver> logger, NamingOptions namingOptions) + : base(logger, namingOptions) { } diff --git a/Emby.Server.Implementations/Library/SearchEngine.cs b/Emby.Server.Implementations/Library/SearchEngine.cs index 55911933a..96702d152 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -30,7 +30,7 @@ namespace Emby.Server.Implementations.Library public QueryResult<SearchHintInfo> GetSearchHints(SearchQuery query) { User user = null; - if (query.UserId != Guid.Empty) + if (!query.UserId.Equals(default)) { user = _userManager.GetUserById(query.UserId); } @@ -48,12 +48,10 @@ namespace Emby.Server.Implementations.Library results = results.GetRange(0, Math.Min(query.Limit.Value, results.Count)); } - return new QueryResult<SearchHintInfo> - { - TotalRecordCount = totalRecordCount, - - Items = results - }; + return new QueryResult<SearchHintInfo>( + query.StartIndex, + totalRecordCount, + results); } private static void AddIfMissing(List<BaseItemKind> list, BaseItemKind value) @@ -170,10 +168,10 @@ namespace Emby.Server.Implementations.Library { Fields = new ItemFields[] { - ItemFields.AirTime, - ItemFields.DateCreated, - ItemFields.ChannelInfo, - ItemFields.ParentId + ItemFields.AirTime, + ItemFields.DateCreated, + ItemFields.ChannelInfo, + ItemFields.ParentId } } }; @@ -182,12 +180,12 @@ namespace Emby.Server.Implementations.Library if (searchQuery.IncludeItemTypes.Length == 1 && searchQuery.IncludeItemTypes[0] == BaseItemKind.MusicArtist) { - if (!searchQuery.ParentId.Equals(Guid.Empty)) + if (!searchQuery.ParentId.Equals(default)) { searchQuery.AncestorIds = new[] { searchQuery.ParentId }; + searchQuery.ParentId = Guid.Empty; } - searchQuery.ParentId = Guid.Empty; searchQuery.IncludeItemsByName = true; searchQuery.IncludeItemTypes = Array.Empty<BaseItemKind>(); mediaItems = _libraryManager.GetAllArtists(searchQuery).Items.Select(i => i.Item).ToList(); diff --git a/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs new file mode 100644 index 000000000..320685b1f --- /dev/null +++ b/Emby.Server.Implementations/Library/SplashscreenPostScanTask.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller.Drawing; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Persistence; +using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Querying; +using Microsoft.Extensions.Logging; + +namespace Emby.Server.Implementations.Library; + +/// <summary> +/// The splashscreen post scan task. +/// </summary> +public class SplashscreenPostScanTask : ILibraryPostScanTask +{ + private readonly IItemRepository _itemRepository; + private readonly IImageEncoder _imageEncoder; + private readonly ILogger<SplashscreenPostScanTask> _logger; + + /// <summary> + /// Initializes a new instance of the <see cref="SplashscreenPostScanTask"/> class. + /// </summary> + /// <param name="itemRepository">Instance of the <see cref="IItemRepository"/> interface.</param> + /// <param name="imageEncoder">Instance of the <see cref="IImageEncoder"/> interface.</param> + /// <param name="logger">Instance of the <see cref="ILogger{SplashscreenPostScanTask}"/> interface.</param> + public SplashscreenPostScanTask( + IItemRepository itemRepository, + IImageEncoder imageEncoder, + ILogger<SplashscreenPostScanTask> logger) + { + _itemRepository = itemRepository; + _imageEncoder = imageEncoder; + _logger = logger; + } + + /// <inheritdoc /> + public Task Run(IProgress<double> progress, CancellationToken cancellationToken) + { + var posters = GetItemsWithImageType(ImageType.Primary).Select(x => x.GetImages(ImageType.Primary).First().Path).ToList(); + var backdrops = GetItemsWithImageType(ImageType.Thumb).Select(x => x.GetImages(ImageType.Thumb).First().Path).ToList(); + if (backdrops.Count == 0) + { + // Thumb images fit better because they include the title in the image but are not provided with TMDb. + // Using backdrops as a fallback to generate an image at all + _logger.LogDebug("No thumb images found. Using backdrops to generate splashscreen"); + backdrops = GetItemsWithImageType(ImageType.Backdrop).Select(x => x.GetImages(ImageType.Backdrop).First().Path).ToList(); + } + + _imageEncoder.CreateSplashscreen(posters, backdrops); + return Task.CompletedTask; + } + + private IReadOnlyList<BaseItem> GetItemsWithImageType(ImageType imageType) + { + // TODO make included libraries configurable + return _itemRepository.GetItemList(new InternalItemsQuery + { + CollapseBoxSetItems = false, + Recursive = true, + DtoOptions = new DtoOptions(false), + ImageTypes = new[] { imageType }, + Limit = 30, + // TODO max parental rating configurable + MaxParentalRating = 10, + OrderBy = new[] + { + (ItemSortBy.Random, SortOrder.Ascending) + }, + IncludeItemTypes = new[] { BaseItemKind.Movie, BaseItemKind.Series } + }); + } +} diff --git a/Emby.Server.Implementations/Library/UserViewManager.cs b/Emby.Server.Implementations/Library/UserViewManager.cs index b00bc72e6..ec411aa3b 100644 --- a/Emby.Server.Implementations/Library/UserViewManager.cs +++ b/Emby.Server.Implementations/Library/UserViewManager.cs @@ -142,7 +142,7 @@ namespace Emby.Server.Implementations.Library if (index == -1 && i is UserView view - && view.DisplayParentId != Guid.Empty) + && !view.DisplayParentId.Equals(default)) { index = Array.IndexOf(orders, view.DisplayParentId); } @@ -214,7 +214,7 @@ namespace Emby.Server.Implementations.Library } else { - var current = list.FirstOrDefault(i => i.Item1 != null && i.Item1.Id == container.Id); + var current = list.FirstOrDefault(i => i.Item1 != null && i.Item1.Id.Equals(container.Id)); if (current != null) { @@ -244,7 +244,7 @@ namespace Emby.Server.Implementations.Library var parents = new List<BaseItem>(); - if (!parentId.Equals(Guid.Empty)) + if (!parentId.Equals(default)) { var parentItem = _libraryManager.GetItemById(parentId); if (parentItem is Channel) diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs index 6937cc097..b2d25fdae 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/DirectRecorder.cs @@ -46,7 +46,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { Directory.CreateDirectory(Path.GetDirectoryName(targetFile) ?? throw new ArgumentException("Path can't be a root directory.", nameof(targetFile))); - using (var output = new FileStream(targetFile, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous)) + await using (var output = new FileStream(targetFile, FileMode.CreateNew, FileAccess.Write, FileShare.Read, IODefaults.FileStreamBufferSize, FileOptions.Asynchronous)) { onStarted(); @@ -56,14 +56,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV using var durationToken = new CancellationTokenSource(duration); using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, durationToken.Token); var linkedCancellationToken = cancellationTokenSource.Token; - - await using var fileStream = new ProgressiveFileStream(directStreamProvider.GetStream()); - await _streamHelper.CopyToAsync( - fileStream, - output, - IODefaults.CopyToBufferSize, - 1000, - linkedCancellationToken).ConfigureAwait(false); + var fileStream = new ProgressiveFileStream(directStreamProvider.GetStream()); + await using (fileStream.ConfigureAwait(false)) + { + await _streamHelper.CopyToAsync( + fileStream, + output, + IODefaults.CopyToBufferSize, + 1000, + linkedCancellationToken).ConfigureAwait(false); + } } _logger.LogInformation("Recording completed: {FilePath}", targetFile); diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs index e7834ffd6..2753cf177 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EmbyTV.cs @@ -1819,16 +1819,16 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (timer.IsProgramSeries) { - SaveSeriesNfo(timer, seriesPath); - SaveVideoNfo(timer, recordingPath, program, false); + await SaveSeriesNfoAsync(timer, seriesPath).ConfigureAwait(false); + await SaveVideoNfoAsync(timer, recordingPath, program, false).ConfigureAwait(false); } else if (!timer.IsMovie || timer.IsSports || timer.IsNews) { - SaveVideoNfo(timer, recordingPath, program, true); + await SaveVideoNfoAsync(timer, recordingPath, program, true).ConfigureAwait(false); } else { - SaveVideoNfo(timer, recordingPath, program, false); + await SaveVideoNfoAsync(timer, recordingPath, program, false).ConfigureAwait(false); } await SaveRecordingImages(recordingPath, program).ConfigureAwait(false); @@ -1839,7 +1839,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV } } - private void SaveSeriesNfo(TimerInfo timer, string seriesPath) + private async Task SaveSeriesNfoAsync(TimerInfo timer, string seriesPath) { var nfoPath = Path.Combine(seriesPath, "tvshow.nfo"); @@ -1848,61 +1848,62 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return; } - using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) + await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { var settings = new XmlWriterSettings { Indent = true, - Encoding = Encoding.UTF8 + Encoding = Encoding.UTF8, + Async = true }; - using (var writer = XmlWriter.Create(stream, settings)) + await using (var writer = XmlWriter.Create(stream, settings)) { - writer.WriteStartDocument(true); - writer.WriteStartElement("tvshow"); + await writer.WriteStartDocumentAsync(true).ConfigureAwait(false); + await writer.WriteStartElementAsync(null, "tvshow", null).ConfigureAwait(false); string id; if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tvdb.ToString(), out id)) { - writer.WriteElementString("id", id); + await writer.WriteElementStringAsync(null, "id", null, id).ConfigureAwait(false); } if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Imdb.ToString(), out id)) { - writer.WriteElementString("imdb_id", id); + await writer.WriteElementStringAsync(null, "imdb_id", null, id).ConfigureAwait(false); } if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Tmdb.ToString(), out id)) { - writer.WriteElementString("tmdbid", id); + await writer.WriteElementStringAsync(null, "tmdbid", null, id).ConfigureAwait(false); } if (timer.SeriesProviderIds.TryGetValue(MetadataProvider.Zap2It.ToString(), out id)) { - writer.WriteElementString("zap2itid", id); + await writer.WriteElementStringAsync(null, "zap2itid", null, id).ConfigureAwait(false); } if (!string.IsNullOrWhiteSpace(timer.Name)) { - writer.WriteElementString("title", timer.Name); + await writer.WriteElementStringAsync(null, "title", null, timer.Name).ConfigureAwait(false); } if (!string.IsNullOrWhiteSpace(timer.OfficialRating)) { - writer.WriteElementString("mpaa", timer.OfficialRating); + await writer.WriteElementStringAsync(null, "mpaa", null, timer.OfficialRating).ConfigureAwait(false); } foreach (var genre in timer.Genres) { - writer.WriteElementString("genre", genre); + await writer.WriteElementStringAsync(null, "genre", null, genre).ConfigureAwait(false); } - writer.WriteEndElement(); - writer.WriteEndDocument(); + await writer.WriteEndElementAsync().ConfigureAwait(false); + await writer.WriteEndDocumentAsync().ConfigureAwait(false); } } } - private void SaveVideoNfo(TimerInfo timer, string recordingPath, BaseItem item, bool lockData) + private async Task SaveVideoNfoAsync(TimerInfo timer, string recordingPath, BaseItem item, bool lockData) { var nfoPath = Path.ChangeExtension(recordingPath, ".nfo"); @@ -1911,29 +1912,30 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV return; } - using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) + await using (var stream = new FileStream(nfoPath, FileMode.CreateNew, FileAccess.Write, FileShare.None)) { var settings = new XmlWriterSettings { Indent = true, - Encoding = Encoding.UTF8 + Encoding = Encoding.UTF8, + Async = true }; var options = _config.GetNfoConfiguration(); var isSeriesEpisode = timer.IsProgramSeries; - using (var writer = XmlWriter.Create(stream, settings)) + await using (var writer = XmlWriter.Create(stream, settings)) { - writer.WriteStartDocument(true); + await writer.WriteStartDocumentAsync(true).ConfigureAwait(false); if (isSeriesEpisode) { - writer.WriteStartElement("episodedetails"); + await writer.WriteStartElementAsync(null, "episodedetails", null).ConfigureAwait(false); if (!string.IsNullOrWhiteSpace(timer.EpisodeTitle)) { - writer.WriteElementString("title", timer.EpisodeTitle); + await writer.WriteElementStringAsync(null, "title", null, timer.EpisodeTitle).ConfigureAwait(false); } var premiereDate = item.PremiereDate ?? (!timer.IsRepeat ? DateTime.UtcNow : null); @@ -1942,79 +1944,87 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { var formatString = options.ReleaseDateFormat; - writer.WriteElementString( + await writer.WriteElementStringAsync( + null, "aired", - premiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture)); + null, + premiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture)).ConfigureAwait(false); } if (item.IndexNumber.HasValue) { - writer.WriteElementString("episode", item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture)); + await writer.WriteElementStringAsync(null, "episode", null, item.IndexNumber.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); } if (item.ParentIndexNumber.HasValue) { - writer.WriteElementString("season", item.ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture)); + await writer.WriteElementStringAsync(null, "season", null, item.ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); } } else { - writer.WriteStartElement("movie"); + await writer.WriteStartElementAsync(null, "movie", null); if (!string.IsNullOrWhiteSpace(item.Name)) { - writer.WriteElementString("title", item.Name); + await writer.WriteElementStringAsync(null, "title", null, item.Name).ConfigureAwait(false); } if (!string.IsNullOrWhiteSpace(item.OriginalTitle)) { - writer.WriteElementString("originaltitle", item.OriginalTitle); + await writer.WriteElementStringAsync(null, "originaltitle", null, item.OriginalTitle).ConfigureAwait(false); } if (item.PremiereDate.HasValue) { var formatString = options.ReleaseDateFormat; - writer.WriteElementString( + await writer.WriteElementStringAsync( + null, "premiered", - item.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture)); - writer.WriteElementString( + null, + item.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture)).ConfigureAwait(false); + await writer.WriteElementStringAsync( + null, "releasedate", - item.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture)); + null, + item.PremiereDate.Value.ToLocalTime().ToString(formatString, CultureInfo.InvariantCulture)).ConfigureAwait(false); } } - writer.WriteElementString( + await writer.WriteElementStringAsync( + null, "dateadded", - DateTime.Now.ToString(DateAddedFormat, CultureInfo.InvariantCulture)); + null, + DateTime.Now.ToString(DateAddedFormat, CultureInfo.InvariantCulture)).ConfigureAwait(false); if (item.ProductionYear.HasValue) { - writer.WriteElementString("year", item.ProductionYear.Value.ToString(CultureInfo.InvariantCulture)); + await writer.WriteElementStringAsync(null, "year", null, item.ProductionYear.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); } if (!string.IsNullOrEmpty(item.OfficialRating)) { - writer.WriteElementString("mpaa", item.OfficialRating); + await writer.WriteElementStringAsync(null, "mpaa", null, item.OfficialRating).ConfigureAwait(false); } var overview = (item.Overview ?? string.Empty) .StripHtml() .Replace(""", "'", StringComparison.Ordinal); - writer.WriteElementString("plot", overview); + await writer.WriteElementStringAsync(null, "plot", null, overview).ConfigureAwait(false); if (item.CommunityRating.HasValue) { - writer.WriteElementString("rating", item.CommunityRating.Value.ToString(CultureInfo.InvariantCulture)); + await writer.WriteElementStringAsync(null, "rating", null, item.CommunityRating.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); } foreach (var genre in item.Genres) { - writer.WriteElementString("genre", genre); + await writer.WriteElementStringAsync(null, "genre", null, genre).ConfigureAwait(false); } - var people = item.Id.Equals(Guid.Empty) ? new List<PersonInfo>() : _libraryManager.GetPeople(item); + var people = item.Id.Equals(default) ? new List<PersonInfo>() : _libraryManager.GetPeople(item); var directors = people .Where(i => IsPersonType(i, PersonType.Director)) @@ -2023,7 +2033,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV foreach (var person in directors) { - writer.WriteElementString("director", person); + await writer.WriteElementStringAsync(null, "director", null, person).ConfigureAwait(false); } var writers = people @@ -2034,19 +2044,19 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV foreach (var person in writers) { - writer.WriteElementString("writer", person); + await writer.WriteElementStringAsync(null, "writer", null, person).ConfigureAwait(false); } foreach (var person in writers) { - writer.WriteElementString("credits", person); + await writer.WriteElementStringAsync(null, "credits", null, person).ConfigureAwait(false); } var tmdbCollection = item.GetProviderId(MetadataProvider.TmdbCollection); if (!string.IsNullOrEmpty(tmdbCollection)) { - writer.WriteElementString("collectionnumber", tmdbCollection); + await writer.WriteElementStringAsync(null, "collectionnumber", null, tmdbCollection).ConfigureAwait(false); } var imdb = item.GetProviderId(MetadataProvider.Imdb); @@ -2054,10 +2064,10 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { if (!isSeriesEpisode) { - writer.WriteElementString("id", imdb); + await writer.WriteElementStringAsync(null, "id", null, imdb).ConfigureAwait(false); } - writer.WriteElementString("imdbid", imdb); + await writer.WriteElementStringAsync(null, "imdbid", null, imdb).ConfigureAwait(false); // No need to lock if we have identified the content already lockData = false; @@ -2066,7 +2076,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var tvdb = item.GetProviderId(MetadataProvider.Tvdb); if (!string.IsNullOrEmpty(tvdb)) { - writer.WriteElementString("tvdbid", tvdb); + await writer.WriteElementStringAsync(null, "tvdbid", null, tvdb).ConfigureAwait(false); // No need to lock if we have identified the content already lockData = false; @@ -2075,7 +2085,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV var tmdb = item.GetProviderId(MetadataProvider.Tmdb); if (!string.IsNullOrEmpty(tmdb)) { - writer.WriteElementString("tmdbid", tmdb); + await writer.WriteElementStringAsync(null, "tmdbid", null, tmdb).ConfigureAwait(false); // No need to lock if we have identified the content already lockData = false; @@ -2083,26 +2093,26 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV if (lockData) { - writer.WriteElementString("lockdata", true.ToString(CultureInfo.InvariantCulture).ToLowerInvariant()); + await writer.WriteElementStringAsync(null, "lockdata", null, "true").ConfigureAwait(false); } if (item.CriticRating.HasValue) { - writer.WriteElementString("criticrating", item.CriticRating.Value.ToString(CultureInfo.InvariantCulture)); + await writer.WriteElementStringAsync(null, "criticrating", null, item.CriticRating.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false); } if (!string.IsNullOrWhiteSpace(item.Tagline)) { - writer.WriteElementString("tagline", item.Tagline); + await writer.WriteElementStringAsync(null, "tagline", null, item.Tagline).ConfigureAwait(false); } foreach (var studio in item.Studios) { - writer.WriteElementString("studio", studio); + await writer.WriteElementStringAsync(null, "studio", null, studio).ConfigureAwait(false); } - writer.WriteEndElement(); - writer.WriteEndDocument(); + await writer.WriteEndElementAsync().ConfigureAwait(false); + await writer.WriteEndDocumentAsync().ConfigureAwait(false); } } } @@ -2372,7 +2382,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { string channelId = seriesTimer.RecordAnyChannel ? null : seriesTimer.ChannelId; - if (string.IsNullOrWhiteSpace(channelId) && !parent.ChannelId.Equals(Guid.Empty)) + if (string.IsNullOrWhiteSpace(channelId) && !parent.ChannelId.Equals(default)) { if (!tempChannelCache.TryGetValue(parent.ChannelId, out LiveTvChannel channel)) { @@ -2431,7 +2441,7 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV { string channelId = null; - if (!programInfo.ChannelId.Equals(Guid.Empty)) + if (!programInfo.ChannelId.Equals(default)) { if (!tempChannelCache.TryGetValue(programInfo.ChannelId, out LiveTvChannel channel)) { diff --git a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs index 7fa47e7db..582e61d79 100644 --- a/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs +++ b/Emby.Server.Implementations/LiveTv/EmbyTV/EncodedRecorder.cs @@ -24,18 +24,19 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.LiveTv.EmbyTV { - public class EncodedRecorder : IRecorder + public class EncodedRecorder : IRecorder, IDisposable { private readonly ILogger _logger; private readonly IMediaEncoder _mediaEncoder; private readonly IServerApplicationPaths _appPaths; - private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>(); + private readonly TaskCompletionSource<bool> _taskCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); private readonly IServerConfigurationManager _serverConfigurationManager; private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; private bool _hasExited; private Stream _logFileStream; private string _targetPath; private Process _process; + private bool _disposed = false; public EncodedRecorder( ILogger logger, @@ -323,5 +324,35 @@ namespace Emby.Server.Implementations.LiveTv.EmbyTV _logger.LogError(ex, "Error reading ffmpeg recording log"); } } + + /// <inheritdoc /> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Releases unmanaged and optionally managed resources. + /// </summary> + /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + _logFileStream?.Dispose(); + _process?.Dispose(); + } + + _logFileStream = null; + _process = null; + + _disposed = true; + } } } diff --git a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs index a8440102d..ffa0d9b6a 100644 --- a/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs +++ b/Emby.Server.Implementations/LiveTv/Listings/SchedulesDirect.cs @@ -28,7 +28,7 @@ using Microsoft.Extensions.Logging; namespace Emby.Server.Implementations.LiveTv.Listings { - public class SchedulesDirect : IListingsProvider + public class SchedulesDirect : IListingsProvider, IDisposable { private const string ApiUrl = "https://json.schedulesdirect.org/20141201"; @@ -39,6 +39,7 @@ namespace Emby.Server.Implementations.LiveTv.Listings private readonly ConcurrentDictionary<string, NameValuePair> _tokens = new ConcurrentDictionary<string, NameValuePair>(); private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options; private DateTime _lastErrorResponse; + private bool _disposed = false; public SchedulesDirect( ILogger<SchedulesDirect> logger, @@ -58,8 +59,8 @@ namespace Emby.Server.Implementations.LiveTv.Listings { var dates = new List<string>(); - var start = new List<DateTime> { startDateUtc, startDateUtc.ToLocalTime() }.Min().Date; - var end = new List<DateTime> { endDateUtc, endDateUtc.ToLocalTime() }.Max().Date; + var start = new[] { startDateUtc, startDateUtc.ToLocalTime() }.Min().Date; + var end = new[] { endDateUtc, endDateUtc.ToLocalTime() }.Max().Date; while (start <= end) { @@ -822,5 +823,31 @@ namespace Emby.Server.Implementations.LiveTv.Listings return list; } + + /// <inheritdoc /> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Releases unmanaged and optionally managed resources. + /// </summary> + /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + protected virtual void Dispose(bool disposing) + { + if (_disposed) + { + return; + } + + if (disposing) + { + _tokenSemaphore?.Dispose(); + } + + _disposed = true; + } } } diff --git a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs index 323b96021..c09f9cf8d 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvDtoService.cs @@ -176,7 +176,7 @@ namespace Emby.Server.Implementations.LiveTv try { dto.ParentThumbImageTag = _imageProcessor.GetImageCacheTag(librarySeries, image); - dto.ParentThumbItemId = librarySeries.Id.ToString("N", CultureInfo.InvariantCulture); + dto.ParentThumbItemId = librarySeries.Id; } catch (Exception ex) { @@ -193,7 +193,7 @@ namespace Emby.Server.Implementations.LiveTv { _imageProcessor.GetImageCacheTag(librarySeries, image) }; - dto.ParentBackdropItemId = librarySeries.Id.ToString("N", CultureInfo.InvariantCulture); + dto.ParentBackdropItemId = librarySeries.Id; } catch (Exception ex) { @@ -240,7 +240,7 @@ namespace Emby.Server.Implementations.LiveTv _imageProcessor.GetImageCacheTag(program, image) }; - dto.ParentBackdropItemId = program.Id.ToString("N", CultureInfo.InvariantCulture); + dto.ParentBackdropItemId = program.Id; } catch (Exception ex) { @@ -456,7 +456,7 @@ namespace Emby.Server.Implementations.LiveTv info.Id = timer.ExternalId; } - if (!dto.ChannelId.Equals(Guid.Empty) && string.IsNullOrEmpty(info.ChannelId)) + if (!dto.ChannelId.Equals(default) && string.IsNullOrEmpty(info.ChannelId)) { var channel = _libraryManager.GetItemById(dto.ChannelId); @@ -522,7 +522,7 @@ namespace Emby.Server.Implementations.LiveTv info.Id = timer.ExternalId; } - if (!dto.ChannelId.Equals(Guid.Empty) && string.IsNullOrEmpty(info.ChannelId)) + if (!dto.ChannelId.Equals(default) && string.IsNullOrEmpty(info.ChannelId)) { var channel = _libraryManager.GetItemById(dto.ChannelId); diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index aa3598c8b..97c2e6e30 100644 --- a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs +++ b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs @@ -39,7 +39,7 @@ namespace Emby.Server.Implementations.LiveTv /// <summary> /// Class LiveTvManager. /// </summary> - public class LiveTvManager : ILiveTvManager, IDisposable + public class LiveTvManager : ILiveTvManager { private const int MaxGuideDays = 14; private const string ExternalServiceTag = "ExternalServiceId"; @@ -63,8 +63,6 @@ namespace Emby.Server.Implementations.LiveTv private ITunerHost[] _tunerHosts = Array.Empty<ITunerHost>(); private IListingsProvider[] _listingProviders = Array.Empty<IListingsProvider>(); - private bool _disposed = false; - public LiveTvManager( IServerConfigurationManager config, ILogger<LiveTvManager> logger, @@ -178,7 +176,9 @@ namespace Emby.Server.Implementations.LiveTv public QueryResult<BaseItem> GetInternalChannels(LiveTvChannelQuery query, DtoOptions dtoOptions, CancellationToken cancellationToken) { - var user = query.UserId == Guid.Empty ? null : _userManager.GetUserById(query.UserId); + var user = query.UserId.Equals(default) + ? null + : _userManager.GetUserById(query.UserId); var topFolder = GetInternalLiveTvFolder(cancellationToken); @@ -312,7 +312,7 @@ namespace Emby.Server.Implementations.LiveTv { if (isVideo) { - mediaSource.MediaStreams.AddRange(new List<MediaStream> + mediaSource.MediaStreams = new MediaStream[] { new MediaStream { @@ -329,11 +329,11 @@ namespace Emby.Server.Implementations.LiveTv // Set the index to -1 because we don't know the exact index of the audio stream within the container Index = -1 } - }); + }; } else { - mediaSource.MediaStreams.AddRange(new List<MediaStream> + mediaSource.MediaStreams = new MediaStream[] { new MediaStream { @@ -341,7 +341,7 @@ namespace Emby.Server.Implementations.LiveTv // Set the index to -1 because we don't know the exact index of the audio stream within the container Index = -1 } - }); + }; } } @@ -857,11 +857,10 @@ namespace Emby.Server.Implementations.LiveTv var returnArray = _dtoService.GetBaseItemDtos(queryResult.Items, options, user); - return new QueryResult<BaseItemDto> - { - Items = returnArray, - TotalRecordCount = queryResult.TotalRecordCount - }; + return new QueryResult<BaseItemDto>( + query.StartIndex, + queryResult.TotalRecordCount, + returnArray); } public QueryResult<BaseItem> GetRecommendedProgramsInternal(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken) @@ -910,29 +909,27 @@ namespace Emby.Server.Implementations.LiveTv programs = programs.Take(query.Limit.Value); } - return new QueryResult<BaseItem> - { - Items = programs.ToArray(), - TotalRecordCount = totalCount - }; + return new QueryResult<BaseItem>( + query.StartIndex, + totalCount, + programs.ToArray()); } - public QueryResult<BaseItemDto> GetRecommendedPrograms(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken) + public Task<QueryResult<BaseItemDto>> GetRecommendedProgramsAsync(InternalItemsQuery query, DtoOptions options, CancellationToken cancellationToken) { if (!(query.IsAiring ?? false)) { - return GetPrograms(query, options, cancellationToken).Result; + return GetPrograms(query, options, cancellationToken); } RemoveFields(options); var internalResult = GetRecommendedProgramsInternal(query, options, cancellationToken); - return new QueryResult<BaseItemDto> - { - Items = _dtoService.GetBaseItemDtos(internalResult.Items, options, query.User), - TotalRecordCount = internalResult.TotalRecordCount - }; + return Task.FromResult(new QueryResult<BaseItemDto>( + query.StartIndex, + internalResult.TotalRecordCount, + _dtoService.GetBaseItemDtos(internalResult.Items, options, query.User))); } private int GetRecommendationScore(LiveTvProgram program, User user, bool factorChannelWatchCount) @@ -1273,7 +1270,7 @@ namespace Emby.Server.Implementations.LiveTv { cancellationToken.ThrowIfCancellationRequested(); - if (itemId.Equals(Guid.Empty)) + if (itemId.Equals(default)) { // Somehow some invalid data got into the db. It probably predates the boundary checking continue; @@ -1533,7 +1530,9 @@ namespace Emby.Server.Implementations.LiveTv public QueryResult<BaseItemDto> GetRecordings(RecordingQuery query, DtoOptions options) { - var user = query.UserId.Equals(Guid.Empty) ? null : _userManager.GetUserById(query.UserId); + var user = query.UserId.Equals(default) + ? null + : _userManager.GetUserById(query.UserId); RemoveFields(options); @@ -1541,11 +1540,10 @@ namespace Emby.Server.Implementations.LiveTv var returnArray = _dtoService.GetBaseItemDtos(internalResult.Items, options, user); - return new QueryResult<BaseItemDto> - { - Items = returnArray, - TotalRecordCount = internalResult.TotalRecordCount - }; + return new QueryResult<BaseItemDto>( + query.StartIndex, + internalResult.TotalRecordCount, + returnArray); } private async Task<QueryResult<TimerInfo>> GetTimersInternal(TimerQuery query, CancellationToken cancellationToken) @@ -1593,7 +1591,7 @@ namespace Emby.Server.Implementations.LiveTv if (!string.IsNullOrEmpty(query.ChannelId)) { var guid = new Guid(query.ChannelId); - timers = timers.Where(i => guid == _tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId)); + timers = timers.Where(i => _tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId).Equals(guid)); } if (!string.IsNullOrEmpty(query.SeriesTimerId)) @@ -1601,7 +1599,7 @@ namespace Emby.Server.Implementations.LiveTv var guid = new Guid(query.SeriesTimerId); timers = timers - .Where(i => _tvDtoService.GetInternalSeriesTimerId(i.Item1.SeriesTimerId) == guid); + .Where(i => _tvDtoService.GetInternalSeriesTimerId(i.Item1.SeriesTimerId).Equals(guid)); } if (!string.IsNullOrEmpty(query.Id)) @@ -1615,11 +1613,7 @@ namespace Emby.Server.Implementations.LiveTv .OrderBy(i => i.StartDate) .ToArray(); - return new QueryResult<TimerInfo> - { - Items = returnArray, - TotalRecordCount = returnArray.Length - }; + return new QueryResult<TimerInfo>(returnArray); } public async Task<QueryResult<TimerInfoDto>> GetTimers(TimerQuery query, CancellationToken cancellationToken) @@ -1667,7 +1661,7 @@ namespace Emby.Server.Implementations.LiveTv if (!string.IsNullOrEmpty(query.ChannelId)) { var guid = new Guid(query.ChannelId); - timers = timers.Where(i => guid == _tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId)); + timers = timers.Where(i => _tvDtoService.GetInternalChannelId(i.Item2.Name, i.Item1.ChannelId).Equals(guid)); } if (!string.IsNullOrEmpty(query.SeriesTimerId)) @@ -1675,7 +1669,7 @@ namespace Emby.Server.Implementations.LiveTv var guid = new Guid(query.SeriesTimerId); timers = timers - .Where(i => _tvDtoService.GetInternalSeriesTimerId(i.Item1.SeriesTimerId) == guid); + .Where(i => _tvDtoService.GetInternalSeriesTimerId(i.Item1.SeriesTimerId).Equals(guid)); } if (!string.IsNullOrEmpty(query.Id)) @@ -1701,11 +1695,7 @@ namespace Emby.Server.Implementations.LiveTv .OrderBy(i => i.StartDate) .ToArray(); - return new QueryResult<TimerInfoDto> - { - Items = returnArray, - TotalRecordCount = returnArray.Length - }; + return new QueryResult<TimerInfoDto>(returnArray); } public async Task CancelTimer(string id) @@ -1801,11 +1791,7 @@ namespace Emby.Server.Implementations.LiveTv .Select(i => i.Item1) .ToArray(); - return new QueryResult<SeriesTimerInfo> - { - Items = returnArray, - TotalRecordCount = returnArray.Length - }; + return new QueryResult<SeriesTimerInfo>(returnArray); } public async Task<QueryResult<SeriesTimerInfoDto>> GetSeriesTimers(SeriesTimerQuery query, CancellationToken cancellationToken) @@ -1855,11 +1841,7 @@ namespace Emby.Server.Implementations.LiveTv }) .ToArray(); - return new QueryResult<SeriesTimerInfoDto> - { - Items = returnArray, - TotalRecordCount = returnArray.Length - }; + return new QueryResult<SeriesTimerInfoDto>(returnArray); } public BaseItem GetLiveTvChannel(TimerInfo timer, ILiveTvService service) @@ -2112,36 +2094,6 @@ namespace Emby.Server.Implementations.LiveTv }; } - /// <inheritdoc /> - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// <summary> - /// Releases unmanaged and - optionally - managed resources. - /// </summary> - /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> - protected virtual void Dispose(bool dispose) - { - if (_disposed) - { - return; - } - - if (dispose) - { - // TODO: Dispose stuff - } - - _services = null; - _listingProviders = null; - _tunerHosts = null; - - _disposed = true; - } - private LiveTvServiceInfo[] GetServiceInfos() { return Services.Select(GetServiceInfo).ToArray(); diff --git a/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs b/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs index 15df0dcf1..72bbdd14a 100644 --- a/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs +++ b/Emby.Server.Implementations/LiveTv/RefreshGuideScheduledTask.cs @@ -50,7 +50,7 @@ namespace Emby.Server.Implementations.LiveTv public string Key => "RefreshGuide"; /// <inheritdoc /> - public Task Execute(CancellationToken cancellationToken, IProgress<double> progress) + public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) { var manager = (LiveTvManager)_liveTvManager; diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs index 532790019..e0eaa8e58 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunHost.cs @@ -446,7 +446,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { Path = url, Protocol = MediaProtocol.Udp, - MediaStreams = new List<MediaStream> + MediaStreams = new MediaStream[] { new MediaStream { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs index a5edd35cc..6195c7648 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs @@ -133,7 +133,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun } } - var taskCompletionSource = new TaskCompletionSource<bool>(); + var taskCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); _ = StartStreaming( udpClient, @@ -186,7 +186,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun { var resolved = false; - using (var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read)) + var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write, FileShare.Read); + await using (fileStream.ConfigureAwait(false)) { while (true) { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs index 5581ba87c..2748794b3 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/LiveStream.cs @@ -97,7 +97,14 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts public Stream GetStream() { - var stream = GetInputStream(TempFilePath); + var stream = new FileStream( + TempFilePath, + FileMode.Open, + FileAccess.Read, + FileShare.ReadWrite, + IODefaults.FileStreamBufferSize, + FileOptions.SequentialScan | FileOptions.Asynchronous); + bool seekFile = (DateTime.UtcNow - DateOpened).TotalSeconds > 10; if (seekFile) { @@ -107,15 +114,6 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts return stream; } - protected FileStream GetInputStream(string path) - => new FileStream( - path, - FileMode.Open, - FileAccess.Read, - FileShare.ReadWrite, - IODefaults.FileStreamBufferSize, - FileOptions.SequentialScan | FileOptions.Asynchronous); - protected async Task DeleteTempFiles(string path, int retryCount = 0) { if (retryCount == 0) diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs index dd83f9a53..2a468e14d 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/M3UTunerHost.cs @@ -170,7 +170,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts { Path = path, Protocol = protocol, - MediaStreams = new List<MediaStream> + MediaStreams = new MediaStream[] { new MediaStream { diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs index ab4beb15b..e84e1e074 100644 --- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs +++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs @@ -73,7 +73,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts SetTempFilePath("ts"); - var taskCompletionSource = new TaskCompletionSource<bool>(); + var taskCompletionSource = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously); _ = StartStreaming(response, taskCompletionSource, LiveStreamCancellationTokenSource.Token); diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json index 25f51db16..01a9969b4 100644 --- a/Emby.Server.Implementations/Localization/Core/cs.json +++ b/Emby.Server.Implementations/Localization/Core/cs.json @@ -23,7 +23,7 @@ "HeaderFavoriteShows": "Oblíbené seriály", "HeaderFavoriteSongs": "Oblíbená hudba", "HeaderLiveTV": "Televize", - "HeaderNextUp": "Nadcházející", + "HeaderNextUp": "Další díly", "HeaderRecordingGroups": "Skupiny nahrávek", "HomeVideos": "Domácí videa", "Inherit": "Zdědit", @@ -77,7 +77,7 @@ "SubtitleDownloadFailureFromForItem": "Stažení titulků pro {1} z {0} selhalo", "Sync": "Synchronizace", "System": "Systém", - "TvShows": "TV seriály", + "TvShows": "Seriály", "User": "Uživatel", "UserCreatedWithName": "Uživatel {0} byl vytvořen", "UserDeletedWithName": "Uživatel {0} byl smazán", @@ -120,5 +120,7 @@ "Forced": "Vynucené", "Default": "Výchozí", "TaskOptimizeDatabaseDescription": "Zmenší databázi a odstraní prázdné místo. Spuštění této úlohy po skenování knihovny či jiných změnách databáze může zlepšit výkon.", - "TaskOptimizeDatabase": "Optimalizovat databázi" + "TaskOptimizeDatabase": "Optimalizovat databázi", + "TaskKeyframeExtractorDescription": "Vytahuje klíčové snímky ze souborů videa za účelem vytváření přesnějších seznamů přehrávání HLS. Tento úkol může trvat velmi dlouho.", + "TaskKeyframeExtractor": "Vytahovač klíčových snímků" } diff --git a/Emby.Server.Implementations/Localization/Core/cy.json b/Emby.Server.Implementations/Localization/Core/cy.json index 7fc27e18a..981614005 100644 --- a/Emby.Server.Implementations/Localization/Core/cy.json +++ b/Emby.Server.Implementations/Localization/Core/cy.json @@ -54,5 +54,57 @@ "Undefined": "Heb ddiffiniad", "TvShows": "Rhaglenni teledu", "HeaderLiveTV": "Teledu Byw", - "User": "Defnyddiwr" + "User": "Defnyddiwr", + "TaskCleanLogsDescription": "Dileu ffeiliau log sy'n fwy na {0} diwrnod oed.", + "TaskCleanLogs": "Glanhau ffolder log", + "TaskRefreshLibraryDescription": "Sganio'ch llyfrgell gyfryngau am ffeiliau newydd ac yn adnewyddu metaddata.", + "TaskRefreshLibrary": "Sganwich Llyfrgell Cyfryngau", + "TaskCleanActivityLogDescription": "Yn dileu cofnodion log gweithgaredd sy'n hŷn na'r oedran a nodwyd.", + "TaskCleanActivityLog": "Glanhau Log Gweithgaredd", + "SubtitleDownloadFailureFromForItem": "Methodd is-deitlau lawrlwytho o {0} ar gyfer {1}", + "NotificationOptionPluginError": "Methodd ategyn", + "NotificationOptionAudioPlaybackStopped": "Stopiwyd chwarae sain", + "NotificationOptionAudioPlayback": "Dechreuwyd chwarae sain", + "MessageServerConfigurationUpdated": "Mae ffurfweddiad gweinydd wedi'i ddiweddaru", + "MessageNamedServerConfigurationUpdatedWithValue": "Mae adran ffurfweddu gweinydd {0} wedi'i diweddaru", + "FailedLoginAttemptWithUserName": "Cais mewngofnodi wedi methu gan {0}", + "ValueHasBeenAddedToLibrary": "{0} wedi'i hychwanegu at eich llyfrgell gyfryngau", + "UserStoppedPlayingItemWithValues": "{0} wedi gorffen chwarae {1} ar {2}", + "UserStartedPlayingItemWithValues": "{0} yn chwarae {1} ar {2}", + "UserPolicyUpdatedWithName": "Polisi defnyddiwr wedi'i newid ar gyfer {0}", + "UserPasswordChangedWithName": "Cyfrinair wedi'i newid ar gyfer defnyddiwr {0}", + "UserOnlineFromDevice": "Mae {0} ar-lein o {1}", + "UserOfflineFromDevice": "Mae {0} wedi datgysylltu o {1}", + "UserLockedOutWithName": "Mae defnyddiwr {0} wedi'i gloi allan", + "UserDownloadingItemWithValues": "Mae {0} yn lawrlwytho {1}", + "UserDeletedWithName": "Defnyddiwr {0} wedi'i ddileu", + "UserCreatedWithName": "Defnyddiwr {0} wedi'i greu", + "StartupEmbyServerIsLoading": "Gweinydd Jellyfin yn llwytho. Triwch eto mewn ychydig.", + "ServerNameNeedsToBeRestarted": "Mae angen ailddechrau {0}", + "PluginUpdatedWithName": "{0} wedi'i ddiweddaru", + "PluginUninstalledWithName": "{0} wedi'i ddadosod", + "PluginInstalledWithName": "{0} wedi'i osod", + "NotificationOptionVideoPlaybackStopped": "Stopiwyd chwarae fideo", + "NotificationOptionVideoPlayback": "Dechreuwyd chwarae fideo", + "NotificationOptionUserLockedOut": "Defnyddiwr wedi'i gloi allan", + "NotificationOptionTaskFailed": "Methwyd cyflawni y dasg a drefnwyd", + "NotificationOptionServerRestartRequired": "Mae angen ailgychwyn y gweinydd", + "NotificationOptionPluginUpdateInstalled": "Diweddariad ategyn wedi'i osod", + "NotificationOptionPluginUninstalled": "Ategyn wedi'i ddadosod", + "NotificationOptionPluginInstalled": "Ategyn wedi'i osod", + "NotificationOptionNewLibraryContent": "Cynnwys newydd ar gael", + "NotificationOptionCameraImageUploaded": "Llun camera wedi'i huwchlwytho", + "NotificationOptionApplicationUpdateInstalled": "Diweddariad ap wedi'i osod", + "NotificationOptionApplicationUpdateAvailable": "Diweddariad ap ar gael", + "NewVersionIsAvailable": "Mae fersiwn diweddarach o'r gweinydd Jellyfin ar gael.", + "NameInstallFailed": "Gosodiad {0} wedi methu", + "MessageApplicationUpdatedTo": "Gweinydd Jellyfin wedi'i ddiweddaru i {0}", + "MessageApplicationUpdated": "Gweinydd Jellyfin wedi'i ddiweddaru", + "LabelIpAddressValue": "Cyfeiriad IP: {0}", + "ItemRemovedWithName": "{0} wedi'i dynnu o'r llyfrgell", + "ItemAddedWithName": "{0} wedi'i adio i'r llyfrgell", + "HeaderRecordingGroups": "Grwpiau Recordio", + "HeaderFavoriteSongs": "Ffefryn Ganeuon", + "HeaderFavoriteShows": "Ffefryn Shoeau", + "HeaderFavoriteEpisodes": "Ffefryn Rhaglenni" } diff --git a/Emby.Server.Implementations/Localization/Core/de.json b/Emby.Server.Implementations/Localization/Core/de.json index 115f36e7c..c538d08c7 100644 --- a/Emby.Server.Implementations/Localization/Core/de.json +++ b/Emby.Server.Implementations/Localization/Core/de.json @@ -120,5 +120,7 @@ "Forced": "Erzwungen", "Default": "Standard", "TaskOptimizeDatabaseDescription": "Komprimiert die Datenbank und trimmt den freien Speicherplatz. Die Ausführung dieser Aufgabe nach dem Scannen der Bibliothek oder nach anderen Änderungen, die Datenbankänderungen implizieren, kann die Leistung verbessern.", - "TaskOptimizeDatabase": "Datenbank optimieren" + "TaskOptimizeDatabase": "Datenbank optimieren", + "TaskKeyframeExtractorDescription": "Extrahiere Keyframes aus Videodateien, um präzisere HLS-Playlisten zu erzeugen. Diese Aufgabe kann sehr lange dauern.", + "TaskKeyframeExtractor": "Keyframe Extraktor" } diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index 9952c05ca..09222dc47 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -120,5 +120,7 @@ "Forced": "Εξαναγκασμένο", "Default": "Προεπιλογή", "TaskOptimizeDatabaseDescription": "Συμπιέζει τη βάση δεδομένων και δημιουργεί ελεύθερο χώρο. Η εκτέλεση αυτής της εργασίας μετά τη σάρωση της βιβλιοθήκης ή την πραγματοποίηση άλλων αλλαγών που συνεπάγονται τροποποιήσεις της βάσης δεδομένων μπορεί να βελτιώσει την απόδοση.", - "TaskOptimizeDatabase": "Βελτιστοποίηση βάσης δεδομένων" + "TaskOptimizeDatabase": "Βελτιστοποίηση βάσης δεδομένων", + "TaskKeyframeExtractorDescription": "Εξάγει τα βασικά καρέ από αρχεία βίντεο για να δημιουργήσει πιο ακριβείς HLS λίστες αναπαραγωγής. Αυτή η εργασία μπορεί να διαρκέσει πολλή ώρα.", + "TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο" } diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json index 568a8e447..d8c33d51b 100644 --- a/Emby.Server.Implementations/Localization/Core/en-US.json +++ b/Emby.Server.Implementations/Localization/Core/en-US.json @@ -12,6 +12,7 @@ "Default": "Default", "DeviceOfflineWithName": "{0} has disconnected", "DeviceOnlineWithName": "{0} is connected", + "External": "External", "FailedLoginAttemptWithUserName": "Failed login try from {0}", "Favorites": "Favorites", "Folders": "Folders", @@ -119,5 +120,7 @@ "TaskDownloadMissingSubtitles": "Download missing subtitles", "TaskDownloadMissingSubtitlesDescription": "Searches the internet for missing subtitles based on metadata configuration.", "TaskOptimizeDatabase": "Optimize database", - "TaskOptimizeDatabaseDescription": "Compacts database and truncates free space. Running this task after scanning the library or doing other changes that imply database modifications might improve performance." + "TaskOptimizeDatabaseDescription": "Compacts database and truncates free space. Running this task after scanning the library or doing other changes that imply database modifications might improve performance.", + "TaskKeyframeExtractor": "Keyframe Extractor", + "TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time." } diff --git a/Emby.Server.Implementations/Localization/Core/eo.json b/Emby.Server.Implementations/Localization/Core/eo.json index 8abf7fa66..7d0fca47f 100644 --- a/Emby.Server.Implementations/Localization/Core/eo.json +++ b/Emby.Server.Implementations/Localization/Core/eo.json @@ -119,5 +119,7 @@ "HeaderRecordingGroups": "Rikordadaj Grupoj", "FailedLoginAttemptWithUserName": "Malsukcesa ensaluta provo de {0}", "CameraImageUploadedFrom": "Nova kamera bildo estis alŝutita de {0}", - "AuthenticationSucceededWithUserName": "{0} sukcese aŭtentikigis" + "AuthenticationSucceededWithUserName": "{0} sukcese aŭtentikigis", + "TaskKeyframeExtractorDescription": "Eltiras ĉefkadrojn el videodosieroj por krei pli precizajn HLS-ludlistojn. Ĉi tiu tasko povas funkcii dum longa tempo.", + "TaskKeyframeExtractor": "Eltiri Ĉefkadrojn" } diff --git a/Emby.Server.Implementations/Localization/Core/es-MX.json b/Emby.Server.Implementations/Localization/Core/es-MX.json index 432814dac..80ae16c5c 100644 --- a/Emby.Server.Implementations/Localization/Core/es-MX.json +++ b/Emby.Server.Implementations/Localization/Core/es-MX.json @@ -120,5 +120,7 @@ "Forced": "Forzado", "Default": "Predeterminado", "TaskOptimizeDatabase": "Optimizar base de datos", - "TaskOptimizeDatabaseDescription": "Compacta la base de datos y trunca el espacio libre. Puede mejorar el rendimiento si se realiza esta tarea después de escanear la biblioteca o después de realizar otros cambios que impliquen modificar la base de datos." + "TaskOptimizeDatabaseDescription": "Compacta la base de datos y trunca el espacio libre. Puede mejorar el rendimiento si se realiza esta tarea después de escanear la biblioteca o después de realizar otros cambios que impliquen modificar la base de datos.", + "TaskKeyframeExtractorDescription": "Extrae los cuadros clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar un buen rato.", + "TaskKeyframeExtractor": "Extractor de Cuadros Clave" } diff --git a/Emby.Server.Implementations/Localization/Core/es.json b/Emby.Server.Implementations/Localization/Core/es.json index f8c69712e..4918f468b 100644 --- a/Emby.Server.Implementations/Localization/Core/es.json +++ b/Emby.Server.Implementations/Localization/Core/es.json @@ -120,5 +120,7 @@ "Forced": "Forzado", "Default": "Predeterminado", "TaskOptimizeDatabase": "Optimizar la base de datos", - "TaskOptimizeDatabaseDescription": "Compacta y libera el espacio libre en la base de datos. Ejecutar esta tarea tras escanear la biblioteca o hacer cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento." + "TaskOptimizeDatabaseDescription": "Compacta y libera el espacio libre en la base de datos. Ejecutar esta tarea tras escanear la biblioteca o hacer cambios que impliquen modificaciones en la base de datos puede mejorar el rendimiento.", + "TaskKeyframeExtractorDescription": "Extrae los fotogramas clave de los archivos de vídeo para crear listas HLS más precisas. Esta tarea puede tardar mucho tiempo.", + "TaskKeyframeExtractor": "Extractor de Fotogramas Clave" } diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json index 6960ff007..7fb560be8 100644 --- a/Emby.Server.Implementations/Localization/Core/fa.json +++ b/Emby.Server.Implementations/Localization/Core/fa.json @@ -119,5 +119,8 @@ "TaskCleanActivityLogDescription": "ورودیهای قدیمیتر از سن تنظیم شده در سیاهه فعالیت را حذف میکند.", "TaskCleanActivityLog": "پاکسازی سیاهه فعالیت", "Undefined": "تعریف نشده", - "TaskOptimizeDatabase": "بهینه سازی پایگاه داده" + "TaskOptimizeDatabase": "بهینه سازی پایگاه داده", + "TaskOptimizeDatabaseDescription": "فشرده سازی پایگاه داده و باز کردن فضای آزاد.اجرای این گزینه بعد از اسکن کردن کتابخانه یا تغییرات دیگر که روی پایگاه داده تأثیر میگذارند میتواند کارایی را بهبود ببخشد.", + "TaskKeyframeExtractorDescription": "فریم های کلیدی را از فایل های ویدئویی استخراج می کند تا لیست های پخش HLS دقیق تری ایجاد کند. این کار ممکن است برای مدت طولانی اجرا شود.", + "TaskKeyframeExtractor": "استخراج کننده فریم کلیدی" } diff --git a/Emby.Server.Implementations/Localization/Core/fi.json b/Emby.Server.Implementations/Localization/Core/fi.json index 4a1f4f1d5..435de7363 100644 --- a/Emby.Server.Implementations/Localization/Core/fi.json +++ b/Emby.Server.Implementations/Localization/Core/fi.json @@ -119,5 +119,7 @@ "TaskCleanActivityLog": "Tyhjennä toimintahistoria", "Undefined": "Määrittelemätön", "TaskOptimizeDatabaseDescription": "Tiivistää ja puhdistaa tietokannan. Tämän toiminnon suorittaminen kirjastojen skannauksen tai muiden tietokantaan liittyvien muutoksien jälkeen voi parantaa suorituskykyä.", - "TaskOptimizeDatabase": "Optimoi tietokanta" + "TaskOptimizeDatabase": "Optimoi tietokanta", + "TaskKeyframeExtractorDescription": "Purkaa videotiedostojen avainkuvat tarkempien HLS-toistolistojen luomiseksi. Tehtävä saattaa kestää huomattavan pitkään.", + "TaskKeyframeExtractor": "Avainkuvien purkain" } diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index bfafe7650..2a329e74d 100644 --- a/Emby.Server.Implementations/Localization/Core/fr.json +++ b/Emby.Server.Implementations/Localization/Core/fr.json @@ -1,7 +1,7 @@ { "Albums": "Albums", "AppDeviceValues": "Application : {0}, Appareil : {1}", - "Application": "Applications", + "Application": "Application", "Artists": "Artistes", "AuthenticationSucceededWithUserName": "{0} authentifié avec succès", "Books": "Livres", @@ -120,5 +120,7 @@ "Forced": "Forcé", "Default": "Par défaut", "TaskOptimizeDatabaseDescription": "Réduit les espaces vides/inutiles et compacte la base de données. Utiliser cette fonction après une mise à jour de la bibliothèque ou toute autre modification de la base de données peut améliorer les performances du serveur.", - "TaskOptimizeDatabase": "Optimiser la base de données" + "TaskOptimizeDatabase": "Optimiser la base de données", + "TaskKeyframeExtractorDescription": "Extrait les images clés des fichiers vidéo pour créer des listes de lecture HLS plus précises. Cette tâche peut durer très longtemps.", + "TaskKeyframeExtractor": "Extracteur d'image clé" } diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json index 85de5925e..781cfcfa2 100644 --- a/Emby.Server.Implementations/Localization/Core/hi.json +++ b/Emby.Server.Implementations/Localization/Core/hi.json @@ -61,5 +61,10 @@ "LabelRunningTimeValue": "चलने का समय: {0}", "ItemAddedWithName": "{0} को लाइब्रेरी में जोड़ा गया", "Inherit": "इनहेरिट", - "NotificationOptionVideoPlaybackStopped": "चलचित्र रुका हुआ" + "NotificationOptionVideoPlaybackStopped": "चलचित्र रुका हुआ", + "PluginUninstalledWithName": "{0} अनइंस्टॉल हुए", + "PluginInstalledWithName": "{0} इंस्टॉल हुए", + "Plugin": "प्लग-इन", + "Playlists": "प्लेलिस्ट", + "Photos": "तस्वीरें" } diff --git a/Emby.Server.Implementations/Localization/Core/hu.json b/Emby.Server.Implementations/Localization/Core/hu.json index acde84aaf..2da936cff 100644 --- a/Emby.Server.Implementations/Localization/Core/hu.json +++ b/Emby.Server.Implementations/Localization/Core/hu.json @@ -120,5 +120,7 @@ "Forced": "Kényszerített", "Default": "Alapértelmezett", "TaskOptimizeDatabaseDescription": "Tömöríti az adatbázist és csonkolja a szabad helyet. A feladat futtatása a könyvtár beolvasása után, vagy egyéb, adatbázis-módosítást igénylő változtatások végrehajtása javíthatja a teljesítményt.", - "TaskOptimizeDatabase": "Adatbázis optimalizálása" + "TaskOptimizeDatabase": "Adatbázis optimalizálása", + "TaskKeyframeExtractor": "Kulcskockák kibontása", + "TaskKeyframeExtractorDescription": "Kulcskockákat bont ki a videofájlokból, hogy pontosabb HLS lejátszási listákat hozzon létre. Ez a feladat hosszú ideig tarthat." } diff --git a/Emby.Server.Implementations/Localization/Core/kk.json b/Emby.Server.Implementations/Localization/Core/kk.json index 1b4a18deb..aaaf04712 100644 --- a/Emby.Server.Implementations/Localization/Core/kk.json +++ b/Emby.Server.Implementations/Localization/Core/kk.json @@ -120,5 +120,7 @@ "TaskCleanCacheDescription": "Jüiede qajet emes keştelgen faildardy joiady.", "TaskCleanActivityLogDescription": "Äreket jūrnalyndağy teñşelgen jasynan asqan jazbalary joiady.", "TaskOptimizeDatabaseDescription": "Derekqordy qysyp, bos oryndy qysqartady. Būl tapsyrmany tasyğyşhanany skanerlegennen keiın nemese derekqorğa meñzeitın basqa özgertuler ıstelgennen keiın oryndau önımdılıktı damytuy mümkın.", - "TaskOptimizeDatabase": "Derekqordy oñtailandyru" + "TaskOptimizeDatabase": "Derekqordy oñtailandyru", + "TaskKeyframeExtractorDescription": "Naqtyraq HLS oynatu tızımderın jasau üşın beinefaildardan negızgı kadrlardy şyğarady. Būl tapsyrma ūzaq uaqytqa sozyluy mümkın.", + "TaskKeyframeExtractor": "Negızgı kadrlardy şyğaru" } diff --git a/Emby.Server.Implementations/Localization/Core/mr.json b/Emby.Server.Implementations/Localization/Core/mr.json index fdb4171b5..5aad4b0ed 100644 --- a/Emby.Server.Implementations/Localization/Core/mr.json +++ b/Emby.Server.Implementations/Localization/Core/mr.json @@ -58,5 +58,47 @@ "Application": "अॅप्लिकेशन", "AppDeviceValues": "अॅप: {0}, यंत्र: {1}", "Collections": "संग्रह", - "ChapterNameValue": "धडा {0}" + "ChapterNameValue": "धडा {0}", + "TaskDownloadMissingSubtitlesDescription": "नसलेल्या उपशिर्षकांचा मेटाडॅटा कॉन्फिग्युरेशनप्रमाणे इन्टरनेटवर शोध घेतो.", + "TaskRefreshChannelsDescription": "इन्टरनेट वाहिन्यांची माहिती ताजी करतो.", + "TaskUpdatePluginsDescription": "आपोआप अपडेट करण्यासाठी कॉन्फिगर केलेल्या प्लगइनसाठी अपडेट डाउनलोड करून इन्स्टॉल करतो.", + "TaskRefreshChannels": "वाहिन्या ताज्या करा", + "TaskRefreshPeopleDescription": "आपल्या माध्यम संग्रहातील अभिनेत्यांचा व दिग्दर्शकांचा मेटाडॅटा ताजा करतो.", + "TaskRefreshPeople": "लोकांची माहिती ताजी करा", + "TaskRefreshLibraryDescription": "माध्यम संग्रह स्कॅन करून नवीन फायली शोधतो व मेटाडॅटा ताजे करतो.", + "TaskRefreshLibrary": "माध्यम संग्रह स्कॅन करा", + "TaskRefreshChapterImagesDescription": "अध्याय असलेल्या व्हिडियोंसाठी थंबनेल चित्र बनवतो.", + "TaskRefreshChapterImages": "अध्याय चित्र काढून घ्या", + "TasksMaintenanceCategory": "देखरेख", + "ValueHasBeenAddedToLibrary": "{0} हे तुमच्या माध्यम संग्रहात जोडण्यात आले आहे", + "UserStoppedPlayingItemWithValues": "{0} यांचं {2} वर {1} पूर्णपणे प्ले करून झालं आहे", + "UserStartedPlayingItemWithValues": "{0} हे {2} वर {1} प्ले करत आहे", + "UserDownloadingItemWithValues": "{0} हे {1} डाउनलोड करत आहे", + "System": "प्रणाली", + "Undefined": "अव्याख्यात", + "Sync": "सिंक", + "ServerNameNeedsToBeRestarted": "{0} याला बंद करून पुन्हा सुरू करायची गरज आहे", + "SubtitleDownloadFailureFromForItem": "{0} येथून {1} यासाठी उपशिर्षक डाउनलोड करण्यात अपयश", + "ScheduledTaskStartedWithName": "{0} सुरू झाले", + "ScheduledTaskFailedWithName": "{0} अपयशी झाले", + "ProviderValue": "पुरवणारा: {0}", + "PluginUpdatedWithName": "{0} अपडेट केले", + "PluginUninstalledWithName": "{0} अनिन्स्टॉल केले", + "PluginInstalledWithName": "{0} इन्स्टॉल केले", + "NotificationOptionVideoPlaybackStopped": "व्हिडियो प्लेबॅक बंद केले", + "NotificationOptionVideoPlayback": "व्हिडियो प्लेबॅक सुरू केले", + "NotificationOptionTaskFailed": "अनुसूचित कार्यात अपयश", + "NotificationOptionServerRestartRequired": "सर्व्हर बंद करून पुन्हा सुरू करावा लागेल", + "NotificationOptionPluginUpdateInstalled": "प्लगइन अपडेट इन्स्टॉल झाले", + "NotificationOptionPluginUninstalled": "प्लगइन अनिन्स्टॉल झाले", + "NotificationOptionPluginInstalled": "प्लगइन इन्स्टॉल झाले", + "NotificationOptionPluginError": "प्लगइनमध्ये अपयश", + "NotificationOptionNewLibraryContent": "नवीन सामग्री जोडली गेली", + "NotificationOptionInstallationFailed": "इन्स्टॉल करण्यात अपयश", + "NotificationOptionAudioPlayback": "ऑडियो प्लेबॅक सुरू झाले", + "NotificationOptionAudioPlaybackStopped": "ऑडियो प्लेबॅक बंद झाले", + "MixedContent": "मिश्रित सामग्री", + "LabelRunningTimeValue": "चालू काल: {0}", + "HeaderContinueWatching": "बघणे चालू ठेवा", + "Default": "डीफॉल्ट" } diff --git a/Emby.Server.Implementations/Localization/Core/pr.json b/Emby.Server.Implementations/Localization/Core/pr.json index 81aa996d9..401e68b2a 100644 --- a/Emby.Server.Implementations/Localization/Core/pr.json +++ b/Emby.Server.Implementations/Localization/Core/pr.json @@ -1,7 +1,16 @@ { - "Books": "Libros", - "AuthenticationSucceededWithUserName": "{0} autentificado correctamente", + "Books": "Scrolls", + "AuthenticationSucceededWithUserName": "{0} passed yer trial", "Artists": "Artistas", "Songs": "Shantees", - "Albums": "Ships" + "Albums": "Tomes", + "Photos": "Paintings", + "NotificationOptionUserLockedOut": "Crewmate sent to the brig", + "HeaderContinueWatching": "Continue Yer Journey", + "Folders": "Chests", + "Application": "Captain", + "DeviceOnlineWithName": "{0} joined yer crew", + "DeviceOfflineWithName": "{0} abandoned ship", + "AppDeviceValues": "Captain: {0}, Ship: {1}", + "CameraImageUploadedFrom": "Yer looking glass has glimpsed another painting from {0}" } diff --git a/Emby.Server.Implementations/Localization/Core/ro.json b/Emby.Server.Implementations/Localization/Core/ro.json index f8fad7b63..8af5449a7 100644 --- a/Emby.Server.Implementations/Localization/Core/ro.json +++ b/Emby.Server.Implementations/Localization/Core/ro.json @@ -117,5 +117,7 @@ "TaskCleanActivityLog": "Curăță Jurnalul de Activitate", "Undefined": "Nedefinit", "Forced": "Forțat", - "Default": "Implicit" + "Default": "Implicit", + "TaskOptimizeDatabaseDescription": "Compactează baza de date și trunchiază spațiul liber. Rularea acestei sarcini după scanarea bibliotecii sau după efectuarea altor modificări care implică modificări ale bazei de date poate îmbunătăți performanța.", + "TaskOptimizeDatabase": "Optimizează baza de date" } diff --git a/Emby.Server.Implementations/Localization/Core/ru.json b/Emby.Server.Implementations/Localization/Core/ru.json index dc3793f1b..dd1e5d0ee 100644 --- a/Emby.Server.Implementations/Localization/Core/ru.json +++ b/Emby.Server.Implementations/Localization/Core/ru.json @@ -120,5 +120,7 @@ "Forced": "Форсир-ые", "Default": "По умолчанию", "TaskOptimizeDatabaseDescription": "Сжимает базу данных и вырезает свободные места. Выполнение этой задачи после сканирования библиотеки или внесения других изменений, предполагающих модификации базы данных, может повысить производительность.", - "TaskOptimizeDatabase": "Оптимизация базы данных" + "TaskOptimizeDatabase": "Оптимизация базы данных", + "TaskKeyframeExtractorDescription": "Извлекаются ключевые кадры из видеофайлов для создания более точных списков плей-листов HLS. Эта задача может выполняться в течение длительного времени.", + "TaskKeyframeExtractor": "Извлечение ключевых кадров" } diff --git a/Emby.Server.Implementations/Localization/Core/sv.json b/Emby.Server.Implementations/Localization/Core/sv.json index 5d05361b0..10c6db63b 100644 --- a/Emby.Server.Implementations/Localization/Core/sv.json +++ b/Emby.Server.Implementations/Localization/Core/sv.json @@ -120,5 +120,7 @@ "Forced": "Tvingad", "Default": "Standard", "TaskOptimizeDatabase": "Optimera databasen", - "TaskOptimizeDatabaseDescription": "Komprimerar databasen och trunkerar ledigt utrymme. Prestandan kan förbättras genom att köra denna task efter att du har skannat biblioteket eller gjort andra förändringar som indikerar att databasen har modifierats." + "TaskOptimizeDatabaseDescription": "Komprimerar databasen och trunkerar ledigt utrymme. Prestandan kan förbättras genom att köra denna task efter att du har skannat biblioteket eller gjort andra förändringar som indikerar att databasen har modifierats.", + "TaskKeyframeExtractorDescription": "Expoterar nyckelram från video filer för att skapa mer exakta HLS-spellistor. Denna uppgift kan pågå under lång tid.", + "TaskKeyframeExtractor": "Nyckelram Extraktor" } diff --git a/Emby.Server.Implementations/Localization/Core/ta.json b/Emby.Server.Implementations/Localization/Core/ta.json index 98d763fcd..5548a74d2 100644 --- a/Emby.Server.Implementations/Localization/Core/ta.json +++ b/Emby.Server.Implementations/Localization/Core/ta.json @@ -119,5 +119,7 @@ "Forced": "கட்டாயப்படுத்தப்பட்டது", "Default": "இயல்புநிலை", "TaskOptimizeDatabaseDescription": "தரவுத்தளத்தை சுருக்கி, இலவச இடத்தை குறைக்கிறது. நூலகத்தை ஸ்கேன் செய்தபின் அல்லது தரவுத்தள மாற்றங்களைக் குறிக்கும் பிற மாற்றங்களைச் செய்தபின் இந்த பணியை இயக்குவது செயல்திறனை மேம்படுத்தக்கூடும்.", - "TaskOptimizeDatabase": "தரவுத்தளத்தை மேம்படுத்தவும்" + "TaskOptimizeDatabase": "தரவுத்தளத்தை மேம்படுத்தவும்", + "TaskKeyframeExtractorDescription": "மிகவும் துல்லியமான HLS பிளேலிஸ்ட்களை உருவாக்க வீடியோ கோப்புகளிலிருந்து கீஃப்ரேம்களைப் பிரித்தெடுக்கிறது. இந்த பணி நீண்ட காலமாக இருக்கலாம்.", + "TaskKeyframeExtractor": "கீஃப்ரேம் எக்ஸ்ட்ராக்டர்" } diff --git a/Emby.Server.Implementations/Localization/Core/ug.json b/Emby.Server.Implementations/Localization/Core/ug.json new file mode 100644 index 000000000..aea93c7fa --- /dev/null +++ b/Emby.Server.Implementations/Localization/Core/ug.json @@ -0,0 +1,9 @@ +{ + "ChapterNameValue": "باب {0}", + "Channels": "قانال", + "CameraImageUploadedFrom": "{0} ئورۇندىن يېڭى سۈرەت چىقىرىلدى", + "Books": "كىتاب", + "AuthenticationSucceededWithUserName": "تىزىملىتىش مۇۋەپپەقىيەتلىك بول", + "Artists": "سەنئەتكار", + "Albums": "پىلاستىنكا" +} diff --git a/Emby.Server.Implementations/Localization/Core/vi.json b/Emby.Server.Implementations/Localization/Core/vi.json index d80f1760d..a9268c7d5 100644 --- a/Emby.Server.Implementations/Localization/Core/vi.json +++ b/Emby.Server.Implementations/Localization/Core/vi.json @@ -13,7 +13,7 @@ "Songs": "Bài Hát", "Sync": "Đồng Bộ", "ValueSpecialEpisodeName": "Đặc Biệt - {0}", - "Albums": "", + "Albums": "Album", "Artists": "Ca Sĩ", "TaskDownloadMissingSubtitlesDescription": "Tìm kiếm phụ đề bị thiếu trên Internet dựa trên cấu hình dữ liệu mô tả.", "TaskDownloadMissingSubtitles": "Tải Xuống Phụ Đề Bị Thiếu", @@ -119,5 +119,7 @@ "Forced": "Bắt Buộc", "Default": "Mặc Định", "TaskOptimizeDatabaseDescription": "Thu gọn cơ sở dữ liệu và cắt bớt dung lượng trống. Chạy tác vụ này sau khi quét thư viện hoặc thực hiện các thay đổi khác ngụ ý sửa đổi cơ sở dữ liệu có thể cải thiện hiệu suất.", - "TaskOptimizeDatabase": "Tối ưu hóa cơ sở dữ liệu" + "TaskOptimizeDatabase": "Tối ưu hóa cơ sở dữ liệu", + "TaskKeyframeExtractor": "Trích Xuất Khung Hình", + "TaskKeyframeExtractorDescription": "Trích xuất khung hình chính từ các tệp video để tạo danh sách phát HLS chính xác hơn. Tác vụ này có thể chạy trong một thời gian dài." } diff --git a/Emby.Server.Implementations/Localization/Core/zh-CN.json b/Emby.Server.Implementations/Localization/Core/zh-CN.json index ac4eb644b..23d2819c3 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-CN.json +++ b/Emby.Server.Implementations/Localization/Core/zh-CN.json @@ -120,5 +120,7 @@ "Forced": "强制的", "Default": "默认", "TaskOptimizeDatabaseDescription": "压缩数据库并优化可用空间,在扫描库或执行其他数据库修改后运行此任务可能会提高性能。", - "TaskOptimizeDatabase": "优化数据库" + "TaskOptimizeDatabase": "优化数据库", + "TaskKeyframeExtractorDescription": "从视频文件中提取关键帧以创建更准确的HLS播放列表。这项任务可能需要很长时间。", + "TaskKeyframeExtractor": "关键帧提取器" } diff --git a/Emby.Server.Implementations/Localization/Core/zh-HK.json b/Emby.Server.Implementations/Localization/Core/zh-HK.json index 1cc97bc27..ac74da67d 100644 --- a/Emby.Server.Implementations/Localization/Core/zh-HK.json +++ b/Emby.Server.Implementations/Localization/Core/zh-HK.json @@ -103,7 +103,7 @@ "TaskCleanTranscodeDescription": "刪除超過一天的轉碼文件。", "TaskCleanTranscode": "清理轉碼目錄", "TaskUpdatePluginsDescription": "下載並安裝配置為自動更新的插件的更新。", - "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的metadata。", + "TaskRefreshPeopleDescription": "更新媒體庫中演員和導演的元數據。", "TaskCleanLogsDescription": "刪除超過{0}天的日誌文件。", "TaskCleanLogs": "清理日誌目錄", "TaskRefreshLibrary": "掃描媒體庫", diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index dbd70342a..281dbb00b 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -147,13 +147,7 @@ namespace Emby.Server.Implementations.Localization threeletterNames = new[] { parts[0], parts[1] }; } - list.Add(new CultureDto - { - DisplayName = name, - Name = name, - ThreeLetterISOLanguageNames = threeletterNames, - TwoLetterISOLanguageName = twoCharName - }); + list.Add(new CultureDto(name, name, twoCharName, threeletterNames)); } } diff --git a/Emby.Server.Implementations/Localization/Ratings/au.csv b/Emby.Server.Implementations/Localization/Ratings/au.csv index 940375e26..11f4ed94c 100644 --- a/Emby.Server.Implementations/Localization/Ratings/au.csv +++ b/Emby.Server.Implementations/Localization/Ratings/au.csv @@ -2,7 +2,6 @@ AU-G,1 AU-PG,5 AU-M,6 AU-MA15+,7 -AU-M15+,8 AU-R18+,9 AU-X18+,10 AU-RC,11 diff --git a/Emby.Server.Implementations/Net/SocketFactory.cs b/Emby.Server.Implementations/Net/SocketFactory.cs index fd3fc31c9..21795c8f8 100644 --- a/Emby.Server.Implementations/Net/SocketFactory.cs +++ b/Emby.Server.Implementations/Net/SocketFactory.cs @@ -1,5 +1,3 @@ -#nullable disable - #pragma warning disable CS1591 using System; @@ -63,18 +61,13 @@ namespace Emby.Server.Implementations.Net } /// <inheritdoc /> - public ISocket CreateUdpMulticastSocket(string ipAddress, int multicastTimeToLive, int localPort) + public ISocket CreateUdpMulticastSocket(IPAddress ipAddress, int multicastTimeToLive, int localPort) { if (ipAddress == null) { throw new ArgumentNullException(nameof(ipAddress)); } - if (ipAddress.Length == 0) - { - throw new ArgumentException("ipAddress cannot be an empty string.", nameof(ipAddress)); - } - if (multicastTimeToLive <= 0) { throw new ArgumentException("multicastTimeToLive cannot be zero or less.", nameof(multicastTimeToLive)); @@ -87,14 +80,7 @@ namespace Emby.Server.Implementations.Net var retVal = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); - try - { - // not supported on all platforms. throws on ubuntu with .net core 2.0 - retVal.ExclusiveAddressUse = false; - } - catch (SocketException) - { - } + retVal.ExclusiveAddressUse = false; try { @@ -114,7 +100,7 @@ namespace Emby.Server.Implementations.Net var localIp = IPAddress.Any; - retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(IPAddress.Parse(ipAddress), localIp)); + retVal.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(ipAddress, localIp)); retVal.MulticastLoopback = true; return new UdpSocket(retVal, localPort, localIp); diff --git a/Emby.Server.Implementations/Net/UdpSocket.cs b/Emby.Server.Implementations/Net/UdpSocket.cs index 0c451ccb6..bbbca4fc0 100644 --- a/Emby.Server.Implementations/Net/UdpSocket.cs +++ b/Emby.Server.Implementations/Net/UdpSocket.cs @@ -159,7 +159,7 @@ namespace Emby.Server.Implementations.Net { ThrowIfDisposed(); - var taskCompletion = new TaskCompletionSource<SocketReceiveResult>(); + var taskCompletion = new TaskCompletionSource<SocketReceiveResult>(TaskCreationOptions.RunContinuationsAsynchronously); bool isResultSet = false; Action<IAsyncResult> callback = callbackResult => @@ -195,7 +195,7 @@ namespace Emby.Server.Implementations.Net { ThrowIfDisposed(); - var taskCompletion = new TaskCompletionSource<int>(); + var taskCompletion = new TaskCompletionSource<int>(TaskCreationOptions.RunContinuationsAsynchronously); bool isResultSet = false; Action<IAsyncResult> callback = callbackResult => diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index 02df2fffe..9e7035cb3 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -139,7 +139,9 @@ namespace Emby.Server.Implementations.Playlists { new Share { - UserId = options.UserId.Equals(Guid.Empty) ? null : options.UserId.ToString("N", CultureInfo.InvariantCulture), + UserId = options.UserId.Equals(default) + ? null + : options.UserId.ToString("N", CultureInfo.InvariantCulture), CanEdit = true } } @@ -188,7 +190,7 @@ namespace Emby.Server.Implementations.Playlists public Task AddToPlaylistAsync(Guid playlistId, IReadOnlyCollection<Guid> itemIds, Guid userId) { - var user = userId.Equals(Guid.Empty) ? null : _userManager.GetUserById(userId); + var user = userId.Equals(default) ? null : _userManager.GetUserById(userId); return AddToPlaylistInternal(playlistId, itemIds, user, new DtoOptions(false) { diff --git a/Emby.Server.Implementations/Plugins/PluginManager.cs b/Emby.Server.Implementations/Plugins/PluginManager.cs index a805924dd..45ef36441 100644 --- a/Emby.Server.Implementations/Plugins/PluginManager.cs +++ b/Emby.Server.Implementations/Plugins/PluginManager.cs @@ -483,7 +483,7 @@ namespace Emby.Server.Implementations.Plugins var pluginStr = instance.Version.ToString(); bool changed = false; if (string.Equals(manifest.Version, pluginStr, StringComparison.Ordinal) - || manifest.Id != instance.Id) + || !manifest.Id.Equals(instance.Id)) { // If a plugin without a manifest failed to load due to an external issue (eg config), // this updates the manifest to the actual plugin values. diff --git a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs index 299f10544..2c4d6884d 100644 --- a/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs +++ b/Emby.Server.Implementations/ScheduledTasks/ScheduledTaskWorker.cs @@ -414,7 +414,7 @@ namespace Emby.Server.Implementations.ScheduledTasks CurrentCancellationTokenSource.CancelAfter(TimeSpan.FromTicks(options.MaxRuntimeTicks.Value)); } - await ScheduledTask.Execute(CurrentCancellationTokenSource.Token, progress).ConfigureAwait(false); + await ScheduledTask.ExecuteAsync(progress, CurrentCancellationTokenSource.Token).ConfigureAwait(false); status = TaskCompletionStatus.Completed; } @@ -757,6 +757,10 @@ namespace Emby.Server.Implementations.ScheduledTasks var trigger = triggerInfo.Item2; trigger.Triggered -= OnTriggerTriggered; trigger.Stop(); + if (trigger is IDisposable disposable) + { + disposable.Dispose(); + } } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs index a5786a3d7..0bf0838fa 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/ChapterImagesTask.cs @@ -88,13 +88,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks }; } - /// <summary> - /// Returns the task to be executed. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <param name="progress">The progress.</param> - /// <returns>Task.</returns> - public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress) + /// <inheritdoc /> + public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) { var videos = _libraryManager.GetItemList(new InternalItemsQuery { diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs index 79886cb52..776079044 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/CleanActivityLogTask.cs @@ -57,12 +57,12 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks public bool IsLogged => true; /// <inheritdoc /> - public Task Execute(CancellationToken cancellationToken, IProgress<double> progress) + public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) { var retentionDays = _serverConfigurationManager.Configuration.ActivityLogRetentionDays; if (!retentionDays.HasValue || retentionDays < 0) { - throw new Exception($"Activity Log Retention days must be at least 0. Currently: {retentionDays}"); + throw new InvalidOperationException($"Activity Log Retention days must be at least 0. Currently: {retentionDays}"); } var startDate = DateTime.UtcNow.AddDays(-retentionDays.Value); diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs index 0941902fc..03935b384 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteCacheFileTask.cs @@ -79,19 +79,14 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks }; } - /// <summary> - /// Returns the task to be executed. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <param name="progress">The progress.</param> - /// <returns>Task.</returns> - public Task Execute(CancellationToken cancellationToken, IProgress<double> progress) + /// <inheritdoc /> + public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) { var minDateModified = DateTime.UtcNow.AddDays(-30); try { - DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.CachePath, minDateModified, progress); + DeleteCacheFilesFromDirectory(_applicationPaths.CachePath, minDateModified, progress, cancellationToken); } catch (DirectoryNotFoundException) { @@ -104,7 +99,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks try { - DeleteCacheFilesFromDirectory(cancellationToken, _applicationPaths.TempDirectory, minDateModified, progress); + DeleteCacheFilesFromDirectory(_applicationPaths.TempDirectory, minDateModified, progress, cancellationToken); } catch (DirectoryNotFoundException) { @@ -117,11 +112,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// <summary> /// Deletes the cache files from directory with a last write time less than a given date. /// </summary> - /// <param name="cancellationToken">The task cancellation token.</param> /// <param name="directory">The directory.</param> /// <param name="minDateModified">The min date modified.</param> /// <param name="progress">The progress.</param> - private void DeleteCacheFilesFromDirectory(CancellationToken cancellationToken, string directory, DateTime minDateModified, IProgress<double> progress) + /// <param name="cancellationToken">The task cancellation token.</param> + private void DeleteCacheFilesFromDirectory(string directory, DateTime minDateModified, IProgress<double> progress, CancellationToken cancellationToken) { var filesToDelete = _fileSystem.GetFiles(directory, true) .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs index fedb5deb0..9739d7327 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteLogFileTask.cs @@ -69,13 +69,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks }; } - /// <summary> - /// Returns the task to be executed. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <param name="progress">The progress.</param> - /// <returns>Task.</returns> - public Task Execute(CancellationToken cancellationToken, IProgress<double> progress) + /// <inheritdoc /> + public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) { // Delete log files more than n days old var minDateModified = DateTime.UtcNow.AddDays(-_configurationManager.CommonConfiguration.LogFileRetentionDays); diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs index 099d781cd..e4e565c64 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/DeleteTranscodeFileTask.cs @@ -78,18 +78,13 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks }; } - /// <summary> - /// Returns the task to be executed. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <param name="progress">The progress.</param> - /// <returns>Task.</returns> - public Task Execute(CancellationToken cancellationToken, IProgress<double> progress) + /// <inheritdoc /> + public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) { var minDateModified = DateTime.UtcNow.AddDays(-1); progress.Report(50); - DeleteTempFilesFromDirectory(cancellationToken, _configurationManager.GetTranscodePath(), minDateModified, progress); + DeleteTempFilesFromDirectory(_configurationManager.GetTranscodePath(), minDateModified, progress, cancellationToken); return Task.CompletedTask; } @@ -97,11 +92,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// <summary> /// Deletes the transcoded temp files from directory with a last write time less than a given date. /// </summary> - /// <param name="cancellationToken">The task cancellation token.</param> /// <param name="directory">The directory.</param> /// <param name="minDateModified">The min date modified.</param> /// <param name="progress">The progress.</param> - private void DeleteTempFilesFromDirectory(CancellationToken cancellationToken, string directory, DateTime minDateModified, IProgress<double> progress) + /// <param name="cancellationToken">The task cancellation token.</param> + private void DeleteTempFilesFromDirectory(string directory, DateTime minDateModified, IProgress<double> progress, CancellationToken cancellationToken) { var filesToDelete = _fileSystem.GetFiles(directory, true) .Where(f => _fileSystem.GetLastWriteTimeUtc(f) < minDateModified) diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs index 35a4aeef6..98e45fa46 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/OptimizeDatabaseTask.cs @@ -69,13 +69,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks }; } - /// <summary> - /// Returns the task to be executed. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <param name="progress">The progress.</param> - /// <returns>Task.</returns> - public Task Execute(CancellationToken cancellationToken, IProgress<double> progress) + /// <inheritdoc /> + public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) { _logger.LogInformation("Optimizing and vacuuming jellyfin.db..."); diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs index 34780111b..7d60ea731 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PeopleValidationTask.cs @@ -62,15 +62,10 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks }; } - /// <summary> - /// Returns the task to be executed. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <param name="progress">The progress.</param> - /// <returns>Task.</returns> - public Task Execute(CancellationToken cancellationToken, IProgress<double> progress) + /// <inheritdoc /> + public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) { - return _libraryManager.ValidatePeople(cancellationToken, progress); + return _libraryManager.ValidatePeopleAsync(progress, cancellationToken); } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs index b3973cecb..443649e6e 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/PluginUpdateTask.cs @@ -68,13 +68,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks yield return new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks }; } - /// <summary> - /// Update installed plugins. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <param name="progress">The progress.</param> - /// <returns><see cref="Task" />.</returns> - public async Task Execute(CancellationToken cancellationToken, IProgress<double> progress) + /// <inheritdoc /> + public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) { progress.Report(0); diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs index f7b3cfedc..065008157 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/RefreshMediaLibraryTask.cs @@ -25,8 +25,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks /// <summary> /// Initializes a new instance of the <see cref="RefreshMediaLibraryTask" /> class. /// </summary> - /// <param name="libraryManager">The library manager.</param> - /// <param name="localization">The localization manager.</param> + /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> + /// <param name="localization">Instance of the <see cref="ILocalizationManager"/> interface.</param> public RefreshMediaLibraryTask(ILibraryManager libraryManager, ILocalizationManager localization) { _libraryManager = libraryManager; @@ -58,13 +58,8 @@ namespace Emby.Server.Implementations.ScheduledTasks.Tasks }; } - /// <summary> - /// Executes the internal. - /// </summary> - /// <param name="cancellationToken">The cancellation token.</param> - /// <param name="progress">The progress.</param> - /// <returns>Task.</returns> - public Task Execute(CancellationToken cancellationToken, IProgress<double> progress) + /// <inheritdoc /> + public Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs index dc5eb7391..63f11a22c 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/DailyTrigger.cs @@ -8,10 +8,11 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers /// <summary> /// Represents a task trigger that fires everyday. /// </summary> - public sealed class DailyTrigger : ITaskTrigger + public sealed class DailyTrigger : ITaskTrigger, IDisposable { private readonly TimeSpan _timeOfDay; private Timer? _timer; + private bool _disposed = false; /// <summary> /// Initializes a new instance of the <see cref="DailyTrigger"/> class. @@ -71,6 +72,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers private void DisposeTimer() { _timer?.Dispose(); + _timer = null; } /// <summary> @@ -80,5 +82,18 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers { Triggered?.Invoke(this, EventArgs.Empty); } + + /// <inheritdoc /> + public void Dispose() + { + if (_disposed) + { + return; + } + + DisposeTimer(); + + _disposed = true; + } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs index 927f57e95..3eb800199 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/IntervalTrigger.cs @@ -9,11 +9,12 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers /// <summary> /// Represents a task trigger that runs repeatedly on an interval. /// </summary> - public sealed class IntervalTrigger : ITaskTrigger + public sealed class IntervalTrigger : ITaskTrigger, IDisposable { private readonly TimeSpan _interval; private DateTime _lastStartDate; private Timer? _timer; + private bool _disposed = false; /// <summary> /// Initializes a new instance of the <see cref="IntervalTrigger"/> class. @@ -89,6 +90,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers private void DisposeTimer() { _timer?.Dispose(); + _timer = null; } /// <summary> @@ -104,5 +106,18 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers Triggered(this, EventArgs.Empty); } } + + /// <inheritdoc /> + public void Dispose() + { + if (_disposed) + { + return; + } + + DisposeTimer(); + + _disposed = true; + } } } diff --git a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs index 2392b20fd..fab49f2fb 100644 --- a/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs +++ b/Emby.Server.Implementations/ScheduledTasks/Triggers/WeeklyTrigger.cs @@ -8,11 +8,12 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers /// <summary> /// Represents a task trigger that fires on a weekly basis. /// </summary> - public sealed class WeeklyTrigger : ITaskTrigger + public sealed class WeeklyTrigger : ITaskTrigger, IDisposable { private readonly TimeSpan _timeOfDay; private readonly DayOfWeek _dayOfWeek; private Timer? _timer; + private bool _disposed = false; /// <summary> /// Initializes a new instance of the <see cref="WeeklyTrigger"/> class. @@ -94,6 +95,7 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers private void DisposeTimer() { _timer?.Dispose(); + _timer = null; } /// <summary> @@ -103,5 +105,18 @@ namespace Emby.Server.Implementations.ScheduledTasks.Triggers { Triggered?.Invoke(this, EventArgs.Empty); } + + /// <inheritdoc /> + public void Dispose() + { + if (_disposed) + { + return; + } + + DisposeTimer(); + + _disposed = true; + } } } diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 6c679ea20..277fdf87d 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -373,7 +373,7 @@ namespace Emby.Server.Implementations.Session info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture); } - if (!info.ItemId.Equals(Guid.Empty) && info.Item == null && libraryItem != null) + if (!info.ItemId.Equals(default) && info.Item == null && libraryItem != null) { var current = session.NowPlayingItem; @@ -424,9 +424,14 @@ namespace Emby.Server.Implementations.Session var nowPlayingQueue = info.NowPlayingQueue; - if (nowPlayingQueue != null) + if (nowPlayingQueue?.Length > 0) { session.NowPlayingQueue = nowPlayingQueue; + + var itemIds = nowPlayingQueue.Select(queue => queue.Id).ToArray(); + session.NowPlayingQueueFullItems = _dtoService.GetBaseItemDtos( + _libraryManager.GetItemList(new InternalItemsQuery { ItemIds = itemIds }), + new DtoOptions(true)); } } @@ -553,22 +558,24 @@ namespace Emby.Server.Implementations.Session { var users = new List<User>(); - if (session.UserId != Guid.Empty) + if (session.UserId.Equals(default)) { - var user = _userManager.GetUserById(session.UserId); - - if (user == null) - { - throw new InvalidOperationException("User not found"); - } + return users; + } - users.Add(user); + var user = _userManager.GetUserById(session.UserId); - users.AddRange(session.AdditionalUsers - .Select(i => _userManager.GetUserById(i.UserId)) - .Where(i => i != null)); + if (user == null) + { + throw new InvalidOperationException("User not found"); } + users.Add(user); + + users.AddRange(session.AdditionalUsers + .Select(i => _userManager.GetUserById(i.UserId)) + .Where(i => i != null)); + return users; } @@ -660,7 +667,7 @@ namespace Emby.Server.Implementations.Session var session = GetSession(info.SessionId); - var libraryItem = info.ItemId == Guid.Empty + var libraryItem = info.ItemId.Equals(default) ? null : GetNowPlayingItem(session, info.ItemId); @@ -755,7 +762,7 @@ namespace Emby.Server.Implementations.Session var session = GetSession(info.SessionId); - var libraryItem = info.ItemId.Equals(Guid.Empty) + var libraryItem = info.ItemId.Equals(default) ? null : GetNowPlayingItem(session, info.ItemId); @@ -892,7 +899,7 @@ namespace Emby.Server.Implementations.Session session.StopAutomaticProgress(); - var libraryItem = info.ItemId.Equals(Guid.Empty) + var libraryItem = info.ItemId.Equals(default) ? null : GetNowPlayingItem(session, info.ItemId); @@ -902,7 +909,7 @@ namespace Emby.Server.Implementations.Session info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture); } - if (!info.ItemId.Equals(Guid.Empty) && info.Item == null && libraryItem != null) + if (!info.ItemId.Equals(default) && info.Item == null && libraryItem != null) { var current = session.NowPlayingItem; @@ -1122,7 +1129,7 @@ namespace Emby.Server.Implementations.Session var session = GetSessionToRemoteControl(sessionId); - var user = session.UserId == Guid.Empty ? null : _userManager.GetUserById(session.UserId); + var user = session.UserId.Equals(default) ? null : _userManager.GetUserById(session.UserId); List<BaseItem> items; @@ -1177,7 +1184,7 @@ namespace Emby.Server.Implementations.Session EnableImages = false }) .Where(i => !i.IsVirtualItem) - .SkipWhile(i => i.Id != episode.Id) + .SkipWhile(i => !i.Id.Equals(episode.Id)) .ToList(); if (episodes.Count > 0) @@ -1191,7 +1198,7 @@ namespace Emby.Server.Implementations.Session { var controllingSession = GetSession(controllingSessionId); AssertCanControl(session, controllingSession); - if (!controllingSession.UserId.Equals(Guid.Empty)) + if (!controllingSession.UserId.Equals(default)) { command.ControllingUserId = controllingSession.UserId; } @@ -1310,7 +1317,7 @@ namespace Emby.Server.Implementations.Session { var controllingSession = GetSession(controllingSessionId); AssertCanControl(session, controllingSession); - if (!controllingSession.UserId.Equals(Guid.Empty)) + if (!controllingSession.UserId.Equals(default)) { command.ControllingUserId = controllingSession.UserId.ToString("N", CultureInfo.InvariantCulture); } @@ -1383,12 +1390,12 @@ namespace Emby.Server.Implementations.Session var session = GetSession(sessionId); - if (session.UserId == userId) + if (session.UserId.Equals(userId)) { throw new ArgumentException("The requested user is already the primary user of the session."); } - if (session.AdditionalUsers.All(i => i.UserId != userId)) + if (session.AdditionalUsers.All(i => !i.UserId.Equals(userId))) { var user = _userManager.GetUserById(userId); @@ -1458,7 +1465,7 @@ namespace Emby.Server.Implementations.Session CheckDisposed(); User user = null; - if (request.UserId != Guid.Empty) + if (!request.UserId.Equals(default)) { user = _userManager.GetUserById(request.UserId); } @@ -1787,7 +1794,7 @@ namespace Emby.Server.Implementations.Session throw new ArgumentNullException(nameof(info)); } - var user = info.UserId == Guid.Empty + var user = info.UserId.Equals(default) ? null : _userManager.GetUserById(info.UserId); diff --git a/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs b/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs new file mode 100644 index 000000000..e39280a10 --- /dev/null +++ b/Emby.Server.Implementations/Sorting/IndexNumberComparer.cs @@ -0,0 +1,50 @@ +using System; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Sorting; +using MediaBrowser.Model.Querying; + +namespace Emby.Server.Implementations.Sorting +{ + /// <summary> + /// Class IndexNumberComparer. + /// </summary> + public class IndexNumberComparer : IBaseItemComparer + { + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + public string Name => ItemSortBy.IndexNumber; + + /// <summary> + /// Compares the specified x. + /// </summary> + /// <param name="x">The x.</param> + /// <param name="y">The y.</param> + /// <returns>System.Int32.</returns> + public int Compare(BaseItem? x, BaseItem? y) + { + if (x == null) + { + throw new ArgumentNullException(nameof(x)); + } + + if (y == null) + { + throw new ArgumentNullException(nameof(y)); + } + + if (!x.IndexNumber.HasValue) + { + return -1; + } + + if (!y.IndexNumber.HasValue) + { + return 1; + } + + return x.IndexNumber.Value.CompareTo(y.IndexNumber.Value); + } + } +} diff --git a/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs b/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs new file mode 100644 index 000000000..ffc4e0cad --- /dev/null +++ b/Emby.Server.Implementations/Sorting/ParentIndexNumberComparer.cs @@ -0,0 +1,50 @@ +using System; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Sorting; +using MediaBrowser.Model.Querying; + +namespace Emby.Server.Implementations.Sorting +{ + /// <summary> + /// Class ParentIndexNumberComparer. + /// </summary> + public class ParentIndexNumberComparer : IBaseItemComparer + { + /// <summary> + /// Gets the name. + /// </summary> + /// <value>The name.</value> + public string Name => ItemSortBy.ParentIndexNumber; + + /// <summary> + /// Compares the specified x. + /// </summary> + /// <param name="x">The x.</param> + /// <param name="y">The y.</param> + /// <returns>System.Int32.</returns> + public int Compare(BaseItem? x, BaseItem? y) + { + if (x == null) + { + throw new ArgumentNullException(nameof(x)); + } + + if (y == null) + { + throw new ArgumentNullException(nameof(y)); + } + + if (!x.ParentIndexNumber.HasValue) + { + return -1; + } + + if (!y.ParentIndexNumber.HasValue) + { + return 1; + } + + return x.ParentIndexNumber.Value.CompareTo(y.ParentIndexNumber.Value); + } + } +} diff --git a/Emby.Server.Implementations/SyncPlay/Group.cs b/Emby.Server.Implementations/SyncPlay/Group.cs index 75cf890e5..52becfec6 100644 --- a/Emby.Server.Implementations/SyncPlay/Group.cs +++ b/Emby.Server.Implementations/SyncPlay/Group.cs @@ -553,7 +553,7 @@ namespace Emby.Server.Implementations.SyncPlay if (playingItemRemoved) { var itemId = PlayQueue.GetPlayingItemId(); - if (!itemId.Equals(Guid.Empty)) + if (!itemId.Equals(default)) { var item = _libraryManager.GetItemById(itemId); RunTimeTicks = item.RunTimeTicks ?? 0; diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index c994ffc90..727b9d4b5 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -126,7 +126,8 @@ namespace Emby.Server.Implementations.TV parentsFolders.ToList()) .Cast<Episode>() .Where(episode => !string.IsNullOrEmpty(episode.SeriesPresentationUniqueKey)) - .Select(GetUniqueSeriesKey); + .Select(GetUniqueSeriesKey) + .ToList(); // Avoid implicitly captured closure var episodes = GetNextUpEpisodes(request, user, items, options); @@ -134,13 +135,21 @@ namespace Emby.Server.Implementations.TV return GetResult(episodes, request); } - public IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IEnumerable<string> seriesKeys, DtoOptions dtoOptions) + public IEnumerable<Episode> GetNextUpEpisodes(NextUpQuery request, User user, IReadOnlyList<string> seriesKeys, DtoOptions dtoOptions) { // Avoid implicitly captured closure var currentUser = user; var allNextUp = seriesKeys - .Select(i => GetNextUp(i, currentUser, dtoOptions)); + .Select(i => GetNextUp(i, currentUser, dtoOptions, false)); + + if (request.EnableRewatching) + { + allNextUp = allNextUp.Concat( + seriesKeys.Select(i => GetNextUp(i, currentUser, dtoOptions, true)) + ) + .OrderByDescending(i => i.Item1); + } // If viewing all next up for all series, remove first episodes // But if that returns empty, keep those first episodes (avoid completely empty view) @@ -186,9 +195,9 @@ namespace Emby.Server.Implementations.TV /// Gets the next up. /// </summary> /// <returns>Task{Episode}.</returns> - private Tuple<DateTime, Func<Episode>> GetNextUp(string seriesKey, User user, DtoOptions dtoOptions) + private Tuple<DateTime, Func<Episode>> GetNextUp(string seriesKey, User user, DtoOptions dtoOptions, bool rewatching) { - var lastWatchedEpisode = _libraryManager.GetItemList(new InternalItemsQuery(user) + var lastQuery = new InternalItemsQuery(user) { AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, @@ -202,23 +211,43 @@ namespace Emby.Server.Implementations.TV Fields = new[] { ItemFields.SortName }, EnableImages = false } - }).Cast<Episode>().FirstOrDefault(); + }; + + if (rewatching) + { + // find last watched by date played, not by newest episode watched + lastQuery.OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) }; + } + + var lastWatchedEpisode = _libraryManager.GetItemList(lastQuery).Cast<Episode>().FirstOrDefault(); Func<Episode> getEpisode = () => { - var nextEpisode = _libraryManager.GetItemList(new InternalItemsQuery(user) + var nextQuery = new InternalItemsQuery(user) { AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, IncludeItemTypes = new[] { BaseItemKind.Episode }, OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) }, Limit = 1, - IsPlayed = false, + IsPlayed = rewatching, IsVirtualItem = false, ParentIndexNumberNotEquals = 0, MinSortName = lastWatchedEpisode?.SortName, DtoOptions = dtoOptions - }).Cast<Episode>().FirstOrDefault(); + }; + + Episode nextEpisode; + if (rewatching) + { + nextQuery.Limit = 2; + // get watched episode after most recently watched + nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().ElementAtOrDefault(1); + } + else + { + nextEpisode = _libraryManager.GetItemList(nextQuery).Cast<Episode>().FirstOrDefault(); + } if (_configurationManager.Configuration.DisplaySpecialsWithinSeasons) { @@ -228,7 +257,7 @@ namespace Emby.Server.Implementations.TV SeriesPresentationUniqueKey = seriesKey, ParentIndexNumber = 0, IncludeItemTypes = new[] { BaseItemKind.Episode }, - IsPlayed = false, + IsPlayed = rewatching, IsVirtualItem = false, DtoOptions = dtoOptions }) @@ -251,7 +280,7 @@ namespace Emby.Server.Implementations.TV .Cast<Episode>(); if (lastWatchedEpisode != null) { - sortedConsideredEpisodes = sortedConsideredEpisodes.SkipWhile(episode => episode.Id != lastWatchedEpisode.Id).Skip(1); + sortedConsideredEpisodes = sortedConsideredEpisodes.SkipWhile(episode => !episode.Id.Equals(lastWatchedEpisode.Id)).Skip(1); } nextEpisode = sortedConsideredEpisodes.FirstOrDefault(); @@ -304,11 +333,10 @@ namespace Emby.Server.Implementations.TV items = items.Take(query.Limit.Value); } - return new QueryResult<BaseItem> - { - TotalRecordCount = totalCount, - Items = items.ToArray() - }; + return new QueryResult<BaseItem>( + query.StartIndex, + totalCount, + items.ToArray()); } } } diff --git a/Emby.Server.Implementations/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs index c8ab99de4..937e792f5 100644 --- a/Emby.Server.Implementations/Udp/UdpServer.cs +++ b/Emby.Server.Implementations/Udp/UdpServer.cs @@ -9,6 +9,7 @@ using MediaBrowser.Controller; using MediaBrowser.Model.ApiClient; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; +using static MediaBrowser.Controller.Extensions.ConfigurationExtensions; namespace Emby.Server.Implementations.Udp { @@ -18,11 +19,6 @@ namespace Emby.Server.Implementations.Udp public sealed class UdpServer : IDisposable { /// <summary> - /// Address Override Configuration Key. - /// </summary> - public const string AddressOverrideConfigKey = "PublishedServerUrl"; - - /// <summary> /// The _logger. /// </summary> private readonly ILogger _logger; @@ -60,7 +56,7 @@ namespace Emby.Server.Implementations.Udp private async Task RespondToV2Message(EndPoint endpoint, CancellationToken cancellationToken) { - string? localUrl = _config[AddressOverrideConfigKey]; + string? localUrl = _config[AddressOverrideKey]; if (string.IsNullOrEmpty(localUrl)) { localUrl = _appHost.GetSmartApiUrl(((IPEndPoint)endpoint).Address); @@ -68,7 +64,7 @@ namespace Emby.Server.Implementations.Udp if (string.IsNullOrEmpty(localUrl)) { - _logger.LogWarning("Unable to respond to udp request because the local ip address could not be determined."); + _logger.LogWarning("Unable to respond to server discovery request because the local ip address could not be determined."); return; } diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs index 5eb4c9ffa..40c386e82 100644 --- a/Emby.Server.Implementations/Updates/InstallationManager.cs +++ b/Emby.Server.Implementations/Updates/InstallationManager.cs @@ -227,9 +227,9 @@ namespace Emby.Server.Implementations.Updates availablePackages = availablePackages.Where(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); } - if (id != default) + if (!id.Equals(default)) { - availablePackages = availablePackages.Where(x => x.Id == id); + availablePackages = availablePackages.Where(x => x.Id.Equals(id)); } if (specificVersion != null) @@ -399,7 +399,7 @@ namespace Emby.Server.Implementations.Updates { lock (_currentInstallationsLock) { - var install = _currentInstallations.Find(x => x.Info.Id == id); + var install = _currentInstallations.Find(x => x.Info.Id.Equals(id)); if (install == default((InstallationInfo, CancellationTokenSource))) { return false; @@ -498,7 +498,7 @@ namespace Emby.Server.Implementations.Updates var compatibleVersions = GetCompatibleVersions(pluginCatalog, plugin.Name, plugin.Id, minVersion: plugin.Version); var version = compatibleVersions.FirstOrDefault(y => y.Version > plugin.Version); - if (version != null && CompletedInstallations.All(x => x.Id != version.Id)) + if (version != null && CompletedInstallations.All(x => !x.Id.Equals(version.Id))) { yield return version; } |
