diff options
Diffstat (limited to 'Emby.Server.Implementations')
59 files changed, 677 insertions, 814 deletions
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 8ed51a194..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; } @@ -973,7 +974,7 @@ namespace Emby.Server.Implementations yield return typeof(IServerApplicationHost).Assembly; // Include composable parts in the Providers assembly - yield return typeof(ProviderUtils).Assembly; + yield return typeof(ProviderManager).Assembly; // Include composable parts in the Photos assembly yield return typeof(PhotoProvider).Assembly; @@ -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..09429c73f 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. @@ -264,11 +265,10 @@ namespace Emby.Server.Implementations.Channels } } - return new QueryResult<Channel> - { - Items = all, - TotalRecordCount = totalCount - }; + return new QueryResult<Channel>( + query.StartIndex, + totalCount, + all); } /// <inheritdoc /> @@ -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) @@ -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; } @@ -786,11 +784,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 +1214,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/Data/SqliteItemRepository.cs b/Emby.Server.Implementations/Data/SqliteItemRepository.cs index 5ab9e02fe..b3b383bfd 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; @@ -6253,7 +6019,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..2b2190b16 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); } } @@ -1323,7 +1314,7 @@ namespace Emby.Server.Implementations.Dto if (image != null) { - dto.ParentLogoItemId = GetDtoId(parent); + dto.ParentLogoItemId = parent.Id; dto.ParentLogoImageTag = GetTagAndFillBlurhash(dto, parent, image); } } @@ -1334,7 +1325,7 @@ namespace Emby.Server.Implementations.Dto if (image != null) { - dto.ParentArtItemId = GetDtoId(parent); + dto.ParentArtItemId = parent.Id; dto.ParentArtImageTag = GetTagAndFillBlurhash(dto, parent, image); } } @@ -1345,7 +1336,7 @@ namespace Emby.Server.Implementations.Dto 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..a5cc125ec 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.2" /> <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/UdpServerEntryPoint.cs b/Emby.Server.Implementations/EntryPoints/UdpServerEntryPoint.cs index feaccf9fa..34fdfbe8d 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/IO/ManagedFileSystem.cs b/Emby.Server.Implementations/IO/ManagedFileSystem.cs index 5c86dbbb7..4f8a52f41 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 /> 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..0770bdbc3 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); @@ -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> @@ -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) @@ -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) @@ -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> @@ -2469,24 +2431,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 +2438,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 +2630,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 +2728,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..eb95977ef 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)) { 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/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 3d06ceb5e..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 @@ -20,14 +21,15 @@ namespace Emby.Server.Implementations.Library.Resolvers private readonly IItemResolver[] _videoResolvers; /// <summary> - /// Initializes an new instance of the <see cref="ExtraResolver"/> class. + /// 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..70d9cbc98 100644 --- a/Emby.Server.Implementations/Library/SearchEngine.cs +++ b/Emby.Server.Implementations/Library/SearchEngine.cs @@ -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) 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/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..bba584854 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,76 +1944,84 @@ 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); @@ -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); } } } 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..fbce7af2d 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) { diff --git a/Emby.Server.Implementations/LiveTv/LiveTvManager.cs b/Emby.Server.Implementations/LiveTv/LiveTvManager.cs index aa3598c8b..71a29e3cb 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, @@ -312,7 +310,7 @@ namespace Emby.Server.Implementations.LiveTv { if (isVideo) { - mediaSource.MediaStreams.AddRange(new List<MediaStream> + mediaSource.MediaStreams = new MediaStream[] { new MediaStream { @@ -329,11 +327,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 +339,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 +855,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 +907,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) @@ -1541,11 +1536,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) @@ -1615,11 +1609,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) @@ -1701,11 +1691,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 +1787,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 +1837,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 +2090,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..7ee8d1040 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", diff --git a/Emby.Server.Implementations/Localization/Core/fa.json b/Emby.Server.Implementations/Localization/Core/fa.json index 6960ff007..8e8eef867 100644 --- a/Emby.Server.Implementations/Localization/Core/fa.json +++ b/Emby.Server.Implementations/Localization/Core/fa.json @@ -119,5 +119,6 @@ "TaskCleanActivityLogDescription": "ورودیهای قدیمیتر از سن تنظیم شده در سیاهه فعالیت را حذف میکند.", "TaskCleanActivityLog": "پاکسازی سیاهه فعالیت", "Undefined": "تعریف نشده", - "TaskOptimizeDatabase": "بهینه سازی پایگاه داده" + "TaskOptimizeDatabase": "بهینه سازی پایگاه داده", + "TaskOptimizeDatabaseDescription": "فشرده سازی پایگاه داده و باز کردن فضای آزاد.اجرای این گزینه بعد از اسکن کردن کتابخانه یا تغییرات دیگر که روی پایگاه داده تأثیر میگذارند میتواند کارایی را بهبود ببخشد." } diff --git a/Emby.Server.Implementations/Localization/Core/fr.json b/Emby.Server.Implementations/Localization/Core/fr.json index bfafe7650..e56ae6071 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", 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/th.json b/Emby.Server.Implementations/Localization/Core/th.json index 89fbb84b6..bed67fa4f 100644 --- a/Emby.Server.Implementations/Localization/Core/th.json +++ b/Emby.Server.Implementations/Localization/Core/th.json @@ -118,5 +118,6 @@ "TaskCleanActivityLog": "ล้างบันทึกกิจกรรม", "Undefined": "ไม่ได้กำหนด", "Forced": "บังคับใช้", - "TaskOptimizeDatabase": "ปรับฐานข้อมูลให้เหมาะสม" + "TaskOptimizeDatabase": "ปรับปรุงประสิทธิภาพฐานข้อมูล", + "TaskOptimizeDatabaseDescription": "ลดขนาดการจัดเก็บฐานข้อมูล ใช้งานคำสั่งนี้หลังจากสแกนไลบรารีหรือหลังจากการเปลี่ยนแปลงฐานข้อมูล อาจจะทำให้ระบบทำงานเร็วขึ้น" } 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..d0e08d8ee 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", 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/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/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/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index c994ffc90..a47650a32 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -140,7 +140,7 @@ namespace Emby.Server.Implementations.TV var currentUser = user; var allNextUp = seriesKeys - .Select(i => GetNextUp(i, currentUser, dtoOptions)); + .Select(i => GetNextUp(i, currentUser, dtoOptions, request.Rewatching)); // 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 +186,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 +202,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 +248,7 @@ namespace Emby.Server.Implementations.TV SeriesPresentationUniqueKey = seriesKey, ParentIndexNumber = 0, IncludeItemTypes = new[] { BaseItemKind.Episode }, - IsPlayed = false, + IsPlayed = rewatching, IsVirtualItem = false, DtoOptions = dtoOptions }) @@ -304,11 +324,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; } |
