aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Emby.Dlna/ContentDirectory/ControlHandler.cs6
-rw-r--r--Emby.Dlna/PlayTo/Device.cs6
-rw-r--r--Emby.Dlna/Service/BaseControlHandler.cs49
-rw-r--r--Emby.Naming/Common/NamingOptions.cs145
-rw-r--r--Emby.Server.Implementations/ApplicationHost.cs2
-rw-r--r--Emby.Server.Implementations/Dto/DtoService.cs9
-rw-r--r--Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs4
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs5
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs2
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs3
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs4
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs48
-rw-r--r--Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs14
-rw-r--r--Emby.Server.Implementations/Localization/Core/cs.json2
-rw-r--r--Emby.Server.Implementations/Localization/Core/he.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/sr.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/th.json3
-rw-r--r--Emby.Server.Implementations/Udp/UdpServer.cs12
-rw-r--r--Jellyfin.Api/Controllers/DlnaServerController.cs7
-rw-r--r--Jellyfin.Api/Controllers/DynamicHlsController.cs18
-rw-r--r--Jellyfin.Api/Controllers/ImageController.cs11
-rw-r--r--Jellyfin.Api/Controllers/ItemLookupController.cs3
-rw-r--r--Jellyfin.Api/Controllers/UserLibraryController.cs13
-rw-r--r--Jellyfin.Api/Helpers/RequestHelpers.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Movies/BoxSet.cs1
-rw-r--r--MediaBrowser.Controller/Entities/Movies/Movie.cs2
-rw-r--r--MediaBrowser.Controller/Entities/TV/Episode.cs19
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs1
-rw-r--r--MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs54
-rw-r--r--MediaBrowser.Controller/Providers/ImageRefreshOptions.cs5
-rw-r--r--MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs1
-rw-r--r--MediaBrowser.Model/Drawing/ImageFormatExtensions.cs27
-rw-r--r--MediaBrowser.Model/Net/MimeTypes.cs1
-rw-r--r--MediaBrowser.Providers/Books/AudioBookMetadataService.cs2
-rw-r--r--MediaBrowser.Providers/Books/BookMetadataService.cs2
-rw-r--r--MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs2
-rw-r--r--MediaBrowser.Providers/Channels/ChannelMetadataService.cs7
-rw-r--r--MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs7
-rw-r--r--MediaBrowser.Providers/Folders/FolderMetadataService.cs7
-rw-r--r--MediaBrowser.Providers/Folders/UserViewMetadataService.cs7
-rw-r--r--MediaBrowser.Providers/Genres/GenreMetadataService.cs7
-rw-r--r--MediaBrowser.Providers/LiveTv/LiveTvMetadataService.cs7
-rw-r--r--MediaBrowser.Providers/Manager/ItemImageProvider.cs136
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs347
-rw-r--r--MediaBrowser.Providers/Manager/ProviderUtils.cs295
-rw-r--r--MediaBrowser.Providers/MediaBrowser.Providers.csproj2
-rw-r--r--MediaBrowser.Providers/Movies/MovieMetadataService.cs2
-rw-r--r--MediaBrowser.Providers/Movies/TrailerMetadataService.cs2
-rw-r--r--MediaBrowser.Providers/Music/AlbumMetadataService.cs2
-rw-r--r--MediaBrowser.Providers/Music/ArtistMetadataService.cs7
-rw-r--r--MediaBrowser.Providers/Music/AudioMetadataService.cs2
-rw-r--r--MediaBrowser.Providers/Music/MusicVideoMetadataService.cs2
-rw-r--r--MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs7
-rw-r--r--MediaBrowser.Providers/People/PersonMetadataService.cs7
-rw-r--r--MediaBrowser.Providers/Photos/PhotoAlbumMetadataService.cs7
-rw-r--r--MediaBrowser.Providers/Photos/PhotoMetadataService.cs7
-rw-r--r--MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs10
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html14
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs7
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs2
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs5
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs7
-rw-r--r--MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs16
-rw-r--r--MediaBrowser.Providers/Studios/StudioMetadataService.cs7
-rw-r--r--MediaBrowser.Providers/TV/EpisodeMetadataService.cs2
-rw-r--r--MediaBrowser.Providers/TV/SeasonMetadataService.cs6
-rw-r--r--MediaBrowser.Providers/TV/SeriesMetadataService.cs2
-rw-r--r--MediaBrowser.Providers/Videos/VideoMetadataService.cs7
-rw-r--r--MediaBrowser.Providers/Years/YearMetadataService.cs7
-rw-r--r--README.md10
-rw-r--r--jellyfin.ruleset4
-rw-r--r--tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj2
-rw-r--r--tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj2
-rw-r--r--tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs33
-rw-r--r--tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj2
-rw-r--r--tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs1
-rw-r--r--tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj2
-rw-r--r--tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs189
-rw-r--r--tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs378
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs46
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs34
82 files changed, 1347 insertions, 808 deletions
diff --git a/Emby.Dlna/ContentDirectory/ControlHandler.cs b/Emby.Dlna/ContentDirectory/ControlHandler.cs
index 010f90c62..0cd1a0daf 100644
--- a/Emby.Dlna/ContentDirectory/ControlHandler.cs
+++ b/Emby.Dlna/ContentDirectory/ControlHandler.cs
@@ -1192,13 +1192,13 @@ namespace Emby.Dlna.ContentDirectory
/// </summary>
/// <param name="result">A <see cref="QueryResult{BaseItem}"/>.</param>
/// <returns>The <see cref="QueryResult{ServerItem}"/>.</returns>
- private static QueryResult<ServerItem> ToResult(QueryResult<(BaseItem, ItemCounts)> result)
+ private static QueryResult<ServerItem> ToResult(QueryResult<(BaseItem Item, ItemCounts ItemCounts)> result)
{
var length = result.Items.Count;
var serverItems = new ServerItem[length];
for (var i = 0; i < length; i++)
{
- serverItems[i] = new ServerItem(result.Items[i].Item1, null);
+ serverItems[i] = new ServerItem(result.Items[i].Item, null);
}
return new QueryResult<ServerItem>
@@ -1213,7 +1213,7 @@ namespace Emby.Dlna.ContentDirectory
/// </summary>
/// <param name="sort">The <see cref="SortCriteria"/>.</param>
/// <param name="isPreSorted">True if pre-sorted.</param>
- private static (string, SortOrder)[] GetOrderBy(SortCriteria sort, bool isPreSorted)
+ private static (string SortName, SortOrder SortOrder)[] GetOrderBy(SortCriteria sort, bool isPreSorted)
{
return isPreSorted ? Array.Empty<(string, SortOrder)>() : new[] { (ItemSortBy.SortName, sort.SortOrder) };
}
diff --git a/Emby.Dlna/PlayTo/Device.cs b/Emby.Dlna/PlayTo/Device.cs
index 34fb8fddd..7815e9293 100644
--- a/Emby.Dlna/PlayTo/Device.cs
+++ b/Emby.Dlna/PlayTo/Device.cs
@@ -535,9 +535,9 @@ namespace Emby.Dlna.PlayTo
{
var tuple = await GetPositionInfo(avCommands, cancellationToken).ConfigureAwait(false);
- var currentObject = tuple.Item2;
+ var currentObject = tuple.Track;
- if (tuple.Item1 && currentObject == null)
+ if (tuple.Success && currentObject == null)
{
currentObject = await GetMediaInfo(avCommands, cancellationToken).ConfigureAwait(false);
}
@@ -797,7 +797,7 @@ namespace Emby.Dlna.PlayTo
return null;
}
- private async Task<(bool, UBaseObject)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
+ private async Task<(bool Success, UBaseObject Track)> GetPositionInfo(TransportCommands avCommands, CancellationToken cancellationToken)
{
var command = avCommands.ServiceActions.FirstOrDefault(c => c.Name == "GetPositionInfo");
if (command == null)
diff --git a/Emby.Dlna/Service/BaseControlHandler.cs b/Emby.Dlna/Service/BaseControlHandler.cs
index 780aad9c1..7bec2eb72 100644
--- a/Emby.Dlna/Service/BaseControlHandler.cs
+++ b/Emby.Dlna/Service/BaseControlHandler.cs
@@ -47,7 +47,7 @@ namespace Emby.Dlna.Service
private async Task<ControlResponse> ProcessControlRequestInternalAsync(ControlRequest request)
{
- ControlRequestInfo? requestInfo = null;
+ ControlRequestInfo requestInfo;
using (var streamReader = new StreamReader(request.InputXml, Encoding.UTF8))
{
@@ -66,6 +66,11 @@ namespace Emby.Dlna.Service
Logger.LogDebug("Received control request {LocalName}, params: {@Headers}", requestInfo.LocalName, requestInfo.Headers);
+ return CreateControlResponse(requestInfo);
+ }
+
+ private ControlResponse CreateControlResponse(ControlRequestInfo requestInfo)
+ {
var settings = new XmlWriterSettings
{
Encoding = Encoding.UTF8,
@@ -112,29 +117,19 @@ namespace Emby.Dlna.Service
{
if (reader.NodeType == XmlNodeType.Element)
{
- switch (reader.LocalName)
+ if (string.Equals(reader.LocalName, "Body", StringComparison.Ordinal))
{
- case "Body":
- {
- if (!reader.IsEmptyElement)
- {
- using var subReader = reader.ReadSubtree();
- return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
- }
- else
- {
- await reader.ReadAsync().ConfigureAwait(false);
- }
-
- break;
- }
-
- default:
- {
- await reader.SkipAsync().ConfigureAwait(false);
- break;
- }
+ if (reader.IsEmptyElement)
+ {
+ await reader.ReadAsync().ConfigureAwait(false);
+ continue;
+ }
+
+ using var subReader = reader.ReadSubtree();
+ return await ParseBodyTagAsync(subReader).ConfigureAwait(false);
}
+
+ await reader.SkipAsync().ConfigureAwait(false);
}
else
{
@@ -160,17 +155,17 @@ namespace Emby.Dlna.Service
localName = reader.LocalName;
namespaceURI = reader.NamespaceURI;
- if (!reader.IsEmptyElement)
+ if (reader.IsEmptyElement)
+ {
+ await reader.ReadAsync().ConfigureAwait(false);
+ }
+ else
{
var result = new ControlRequestInfo(localName, namespaceURI);
using var subReader = reader.ReadSubtree();
await ParseFirstBodyChildAsync(subReader, result.Headers).ConfigureAwait(false);
return result;
}
- else
- {
- await reader.ReadAsync().ConfigureAwait(false);
- }
}
else
{
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index c0be0b7c6..eb211050f 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -411,6 +411,66 @@ namespace Emby.Naming.Common
MediaType.Video),
new ExtraRule(
+ ExtraType.ThemeVideo,
+ ExtraRuleType.DirectoryName,
+ "backdrops",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.ThemeSong,
+ ExtraRuleType.DirectoryName,
+ "theme-music",
+ MediaType.Audio),
+
+ new ExtraRule(
+ ExtraType.BehindTheScenes,
+ ExtraRuleType.DirectoryName,
+ "behind the scenes",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.DeletedScene,
+ ExtraRuleType.DirectoryName,
+ "deleted scenes",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Interview,
+ ExtraRuleType.DirectoryName,
+ "interviews",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Scene,
+ ExtraRuleType.DirectoryName,
+ "scenes",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Sample,
+ ExtraRuleType.DirectoryName,
+ "samples",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Clip,
+ ExtraRuleType.DirectoryName,
+ "shorts",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Clip,
+ ExtraRuleType.DirectoryName,
+ "featurettes",
+ MediaType.Video),
+
+ new ExtraRule(
+ ExtraType.Unknown,
+ ExtraRuleType.DirectoryName,
+ "extras",
+ MediaType.Video),
+
+ new ExtraRule(
ExtraType.Trailer,
ExtraRuleType.Filename,
"trailer",
@@ -471,24 +531,12 @@ namespace Emby.Naming.Common
MediaType.Video),
new ExtraRule(
- ExtraType.ThemeVideo,
- ExtraRuleType.DirectoryName,
- "backdrops",
- MediaType.Video),
-
- new ExtraRule(
ExtraType.ThemeSong,
ExtraRuleType.Filename,
"theme",
MediaType.Audio),
new ExtraRule(
- ExtraType.ThemeSong,
- ExtraRuleType.DirectoryName,
- "theme-music",
- MediaType.Audio),
-
- new ExtraRule(
ExtraType.Scene,
ExtraRuleType.Suffix,
"-scene",
@@ -537,54 +585,16 @@ namespace Emby.Naming.Common
MediaType.Video),
new ExtraRule(
- ExtraType.BehindTheScenes,
- ExtraRuleType.DirectoryName,
- "behind the scenes",
- MediaType.Video),
-
- new ExtraRule(
- ExtraType.DeletedScene,
- ExtraRuleType.DirectoryName,
- "deleted scenes",
- MediaType.Video),
-
- new ExtraRule(
- ExtraType.Interview,
- ExtraRuleType.DirectoryName,
- "interviews",
- MediaType.Video),
-
- new ExtraRule(
- ExtraType.Scene,
- ExtraRuleType.DirectoryName,
- "scenes",
- MediaType.Video),
-
- new ExtraRule(
- ExtraType.Sample,
- ExtraRuleType.DirectoryName,
- "samples",
- MediaType.Video),
-
- new ExtraRule(
- ExtraType.Clip,
- ExtraRuleType.DirectoryName,
- "shorts",
- MediaType.Video),
-
- new ExtraRule(
- ExtraType.Clip,
- ExtraRuleType.DirectoryName,
- "featurettes",
- MediaType.Video),
-
- new ExtraRule(
ExtraType.Unknown,
- ExtraRuleType.DirectoryName,
- "extras",
+ ExtraRuleType.Suffix,
+ "-extra",
MediaType.Video)
};
+ AllExtrasTypesFolderNames = VideoExtraRules
+ .Where(i => i.RuleType == ExtraRuleType.DirectoryName)
+ .ToDictionary(i => i.Token, i => i.ExtraType, StringComparer.OrdinalIgnoreCase);
+
Format3DRules = new[]
{
// Kodi rules:
@@ -673,6 +683,10 @@ namespace Emby.Naming.Common
".mxf"
});
+ VideoFileExtensions = extensions
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToArray();
+
MultipleEpisodeExpressions = new[]
{
@".*(\\|\/)[sS]?(?<seasonnumber>[0-9]{1,4})[xX](?<epnumber>[0-9]{1,3})((-| - )[0-9]{1,4}[eExX](?<endingepnumber>[0-9]{1,3}))+[^\\\/]*$",
@@ -690,25 +704,6 @@ namespace Emby.Naming.Common
IsNamed = true
}).ToArray();
- VideoFileExtensions = extensions
- .Distinct(StringComparer.OrdinalIgnoreCase)
- .ToArray();
-
- AllExtrasTypesFolderNames = new Dictionary<string, ExtraType>(StringComparer.OrdinalIgnoreCase)
- {
- ["trailers"] = ExtraType.Trailer,
- ["theme-music"] = ExtraType.ThemeSong,
- ["backdrops"] = ExtraType.ThemeVideo,
- ["extras"] = ExtraType.Unknown,
- ["behind the scenes"] = ExtraType.BehindTheScenes,
- ["deleted scenes"] = ExtraType.DeletedScene,
- ["interviews"] = ExtraType.Interview,
- ["scenes"] = ExtraType.Scene,
- ["samples"] = ExtraType.Sample,
- ["shorts"] = ExtraType.Clip,
- ["featurettes"] = ExtraType.Clip
- };
-
Compile();
}
diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs
index 571404040..e7efc81d7 100644
--- a/Emby.Server.Implementations/ApplicationHost.cs
+++ b/Emby.Server.Implementations/ApplicationHost.cs
@@ -974,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;
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 7ba34e74a..88d9303a5 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -1102,12 +1102,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);
}
}
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/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 464a621cf..bd0c178fd 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -3007,7 +3007,10 @@ namespace Emby.Server.Implementations.Library
}
}
- CreateItems(personsToSave, null, CancellationToken.None);
+ if (personsToSave.Count > 0)
+ {
+ CreateItems(personsToSave, null, CancellationToken.None);
+ }
}
private void StartScanInBackground()
diff --git a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
index 3d06ceb5e..807913b5d 100644
--- a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
@@ -20,7 +20,7 @@ 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="namingOptions">An instance of <see cref="NamingOptions"/>.</param>
public ExtraResolver(NamingOptions namingOptions)
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 1a9295dc8..122e9654a 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -128,10 +128,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))
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
index f1a6ef344..48d9e316d 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunManager.cs
@@ -80,7 +80,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
_remoteEndPoint = new IPEndPoint(remoteIp, HdHomeRunPort);
_tcpClient = new TcpClient();
- _tcpClient.Connect(_remoteEndPoint);
+ await _tcpClient.ConnectAsync(_remoteEndPoint, cancellationToken).ConfigureAwait(false);
if (!_lockkey.HasValue)
{
@@ -158,7 +158,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
}
using var tcpClient = new TcpClient();
- tcpClient.Connect(_remoteEndPoint);
+ await tcpClient.ConnectAsync(_remoteEndPoint, cancellationToken).ConfigureAwait(false);
using var stream = tcpClient.GetStream();
var commandList = commands.GetCommands();
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
index 9ed0d8d73..a5edd35cc 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/HdHomerun/HdHomerunUdpStream.cs
@@ -165,7 +165,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
{
await CopyTo(udpClient, TempFilePath, openTaskCompletionSource, cancellationToken).ConfigureAwait(false);
}
- catch (OperationCanceledException ex)
+ catch (Exception ex) when (ex is OperationCanceledException || ex is TimeoutException)
{
Logger.LogInformation("HDHR UDP stream cancelled or timed out from {0}", remoteAddress);
openTaskCompletionSource.TrySetException(ex);
@@ -191,36 +191,24 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts.HdHomerun
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
- using (var timeOutSource = new CancellationTokenSource())
- using (var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(
- cancellationToken,
- timeOutSource.Token))
+ var res = await udpClient.ReceiveAsync(cancellationToken)
+ .AsTask()
+ .WaitAsync(TimeSpan.FromMilliseconds(30000), CancellationToken.None)
+ .ConfigureAwait(false);
+ var buffer = res.Buffer;
+
+ var read = buffer.Length - RtpHeaderBytes;
+
+ if (read > 0)
+ {
+ await fileStream.WriteAsync(buffer.AsMemory(RtpHeaderBytes, read), cancellationToken).ConfigureAwait(false);
+ }
+
+ if (!resolved)
{
- var resTask = udpClient.ReceiveAsync(linkedSource.Token).AsTask();
- if (await Task.WhenAny(resTask, Task.Delay(30000, linkedSource.Token)).ConfigureAwait(false) != resTask)
- {
- resTask.Dispose();
- break;
- }
-
- // We don't want all these delay tasks to keep running
- timeOutSource.Cancel();
- var res = await resTask.ConfigureAwait(false);
- var buffer = res.Buffer;
-
- var read = buffer.Length - RtpHeaderBytes;
-
- if (read > 0)
- {
- await fileStream.WriteAsync(buffer.AsMemory(RtpHeaderBytes, read), linkedSource.Token).ConfigureAwait(false);
- }
-
- if (!resolved)
- {
- resolved = true;
- DateOpened = DateTime.UtcNow;
- openTaskCompletionSource.TrySetResult(true);
- }
+ resolved = true;
+ DateOpened = DateTime.UtcNow;
+ openTaskCompletionSource.TrySetResult(true);
}
}
}
diff --git a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
index b1ce7b2b3..ab4beb15b 100644
--- a/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
+++ b/Emby.Server.Implementations/LiveTv/TunerHosts/SharedHttpStream.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -51,7 +49,7 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
var url = mediaSource.Path;
- Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath));
+ Directory.CreateDirectory(Path.GetDirectoryName(TempFilePath) ?? throw new InvalidOperationException("Path can't be a root directory."));
var typeName = GetType().Name;
Logger.LogInformation("Opening {StreamType} Live stream from {Url}", typeName, url);
@@ -94,14 +92,8 @@ namespace Emby.Server.Implementations.LiveTv.TunerHosts
// OpenedMediaSource.SupportsDirectPlay = false;
// OpenedMediaSource.SupportsDirectStream = true;
// OpenedMediaSource.SupportsTranscoding = true;
- await taskCompletionSource.Task.ConfigureAwait(false);
- if (taskCompletionSource.Task.Exception != null)
- {
- // Error happened while opening the stream so raise the exception again to inform the caller
- throw taskCompletionSource.Task.Exception;
- }
-
- if (!taskCompletionSource.Task.Result)
+ var res = await taskCompletionSource.Task.ConfigureAwait(false);
+ if (!res)
{
Logger.LogWarning("Zero bytes copied from stream {StreamType} to {FilePath} but no exception raised", GetType().Name, TempFilePath);
throw new EndOfStreamException(string.Format(CultureInfo.InvariantCulture, "Zero bytes copied from stream {0}", GetType().Name));
diff --git a/Emby.Server.Implementations/Localization/Core/cs.json b/Emby.Server.Implementations/Localization/Core/cs.json
index 25f51db16..65a31e676 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",
diff --git a/Emby.Server.Implementations/Localization/Core/he.json b/Emby.Server.Implementations/Localization/Core/he.json
index 5e299ea0e..e32ab4ca8 100644
--- a/Emby.Server.Implementations/Localization/Core/he.json
+++ b/Emby.Server.Implementations/Localization/Core/he.json
@@ -119,5 +119,6 @@
"Undefined": "לא מוגדר",
"Forced": "כפוי",
"Default": "ברירת מחדל",
- "TaskOptimizeDatabase": "מיטוב מסד נתונים"
+ "TaskOptimizeDatabase": "מיטוב מסד נתונים",
+ "TaskOptimizeDatabaseDescription": "דוחס את מסד הנתונים ומוריד את שטח האחסון שבשימוש. הרצה של פעולה זו לאחר סריקת הספרייה או שינויים אחרים שמשפיעים על מסד הנתונים יכולה לשפר ביצועים."
}
diff --git a/Emby.Server.Implementations/Localization/Core/sr.json b/Emby.Server.Implementations/Localization/Core/sr.json
index e31208e80..72e125dfe 100644
--- a/Emby.Server.Implementations/Localization/Core/sr.json
+++ b/Emby.Server.Implementations/Localization/Core/sr.json
@@ -118,5 +118,6 @@
"Undefined": "Недефинисано",
"Forced": "Принудно",
"Default": "Подразумевано",
- "TaskOptimizeDatabase": "Оптимизуј датабазу"
+ "TaskOptimizeDatabase": "Оптимизуј датабазу",
+ "TaskOptimizeDatabaseDescription": "Сажима базу података и скраћује слободан простор. Покретање овог задатка након скенирања библиотеке или других промена које подразумевају измене базе података које могу побољшати перформансе."
}
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/Udp/UdpServer.cs b/Emby.Server.Implementations/Udp/UdpServer.cs
index 33e4e5651..c8ab99de4 100644
--- a/Emby.Server.Implementations/Udp/UdpServer.cs
+++ b/Emby.Server.Implementations/Udp/UdpServer.cs
@@ -97,21 +97,11 @@ namespace Emby.Server.Implementations.Udp
private async Task BeginReceiveAsync(CancellationToken cancellationToken)
{
- var infiniteTask = Task.Delay(-1, cancellationToken);
while (!cancellationToken.IsCancellationRequested)
{
try
{
- var task = _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint);
- await Task.WhenAny(task, infiniteTask).ConfigureAwait(false);
-
- if (!task.IsCompleted)
- {
- return;
- }
-
- var result = task.Result;
-
+ var result = await _udpSocket.ReceiveFromAsync(_receiveBuffer, SocketFlags.None, _endpoint, cancellationToken).ConfigureAwait(false);
var text = Encoding.UTF8.GetString(_receiveBuffer, 0, result.ReceivedBytes);
if (text.Contains("who is JellyfinServer?", StringComparison.OrdinalIgnoreCase))
{
diff --git a/Jellyfin.Api/Controllers/DlnaServerController.cs b/Jellyfin.Api/Controllers/DlnaServerController.cs
index 4e8c01577..b1c576c33 100644
--- a/Jellyfin.Api/Controllers/DlnaServerController.cs
+++ b/Jellyfin.Api/Controllers/DlnaServerController.cs
@@ -9,6 +9,7 @@ using Emby.Dlna.Main;
using Jellyfin.Api.Attributes;
using Jellyfin.Api.Constants;
using MediaBrowser.Controller.Dlna;
+using MediaBrowser.Model.Net;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
@@ -337,11 +338,7 @@ namespace Jellyfin.Api.Controllers
return NotFound();
}
- var contentType = "image/" + Path.GetExtension(fileName)
- .TrimStart('.')
- .ToLowerInvariant();
-
- return File(icon.Stream, contentType);
+ return File(icon.Stream, MimeTypes.GetMimeType(fileName));
}
private string GetAbsoluteUri()
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 77a1170b1..f2fdeeea5 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -389,6 +389,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
/// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
/// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+ /// <param name="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
+ /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
/// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
/// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
@@ -445,6 +447,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] long? startTimeTicks,
[FromQuery] int? width,
[FromQuery] int? height,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
@@ -497,6 +501,8 @@ namespace Jellyfin.Api.Controllers
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
+ MaxWidth = maxWidth,
+ MaxHeight = maxHeight,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
@@ -721,6 +727,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
/// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
/// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+ /// <param name="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
+ /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
/// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
/// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
@@ -775,6 +783,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] long? startTimeTicks,
[FromQuery] int? width,
[FromQuery] int? height,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
@@ -827,6 +837,8 @@ namespace Jellyfin.Api.Controllers
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
+ MaxWidth = maxWidth,
+ MaxHeight = maxHeight,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
@@ -1054,6 +1066,8 @@ namespace Jellyfin.Api.Controllers
/// <param name="startTimeTicks">Optional. Specify a starting offset, in ticks. 1 tick = 10000 ms.</param>
/// <param name="width">Optional. The fixed horizontal resolution of the encoded video.</param>
/// <param name="height">Optional. The fixed vertical resolution of the encoded video.</param>
+ /// <param name="maxWidth">Optional. The maximum horizontal resolution of the encoded video.</param>
+ /// <param name="maxHeight">Optional. The maximum vertical resolution of the encoded video.</param>
/// <param name="videoBitRate">Optional. Specify a video bitrate to encode to, e.g. 500000. If omitted this will be left to encoder defaults.</param>
/// <param name="subtitleStreamIndex">Optional. The index of the subtitle stream to use. If omitted no subtitles will be used.</param>
/// <param name="subtitleMethod">Optional. Specify the subtitle delivery method.</param>
@@ -1114,6 +1128,8 @@ namespace Jellyfin.Api.Controllers
[FromQuery] long? startTimeTicks,
[FromQuery] int? width,
[FromQuery] int? height,
+ [FromQuery] int? maxWidth,
+ [FromQuery] int? maxHeight,
[FromQuery] int? videoBitRate,
[FromQuery] int? subtitleStreamIndex,
[FromQuery] SubtitleDeliveryMethod? subtitleMethod,
@@ -1168,6 +1184,8 @@ namespace Jellyfin.Api.Controllers
StartTimeTicks = startTimeTicks,
Width = width,
Height = height,
+ MaxWidth = maxWidth,
+ MaxHeight = maxHeight,
VideoBitRate = videoBitRate,
SubtitleStreamIndex = subtitleStreamIndex,
SubtitleMethod = subtitleMethod ?? SubtitleDeliveryMethod.Encode,
diff --git a/Jellyfin.Api/Controllers/ImageController.cs b/Jellyfin.Api/Controllers/ImageController.cs
index 86933074d..e72589cfa 100644
--- a/Jellyfin.Api/Controllers/ImageController.cs
+++ b/Jellyfin.Api/Controllers/ImageController.cs
@@ -1878,8 +1878,8 @@ namespace Jellyfin.Api.Controllers
if (!supportsWebP)
{
var userAgent = Request.Headers[HeaderNames.UserAgent].ToString();
- if (userAgent.IndexOf("crosswalk", StringComparison.OrdinalIgnoreCase) != -1 &&
- userAgent.IndexOf("android", StringComparison.OrdinalIgnoreCase) != -1)
+ if (userAgent.Contains("crosswalk", StringComparison.OrdinalIgnoreCase)
+ && userAgent.Contains("android", StringComparison.OrdinalIgnoreCase))
{
supportsWebP = true;
}
@@ -1905,10 +1905,7 @@ namespace Jellyfin.Api.Controllers
private bool SupportsFormat(IReadOnlyCollection<string> requestAcceptTypes, string acceptParam, ImageFormat format, bool acceptAll)
{
- var normalized = format.ToString().ToLowerInvariant();
- var mimeType = "image/" + normalized;
-
- if (requestAcceptTypes.Contains(mimeType))
+ if (requestAcceptTypes.Contains(format.GetMimeType()))
{
return true;
}
@@ -1918,6 +1915,8 @@ namespace Jellyfin.Api.Controllers
return true;
}
+ // Review if this should be jpeg, jpg or both for ImageFormat.Jpg
+ var normalized = format.ToString().ToLowerInvariant();
return string.Equals(acceptParam, normalized, StringComparison.OrdinalIgnoreCase);
}
diff --git a/Jellyfin.Api/Controllers/ItemLookupController.cs b/Jellyfin.Api/Controllers/ItemLookupController.cs
index 4161e43f6..c49f85616 100644
--- a/Jellyfin.Api/Controllers/ItemLookupController.cs
+++ b/Jellyfin.Api/Controllers/ItemLookupController.cs
@@ -263,7 +263,8 @@ namespace Jellyfin.Api.Controllers
ImageRefreshMode = MetadataRefreshMode.FullRefresh,
ReplaceAllMetadata = true,
ReplaceAllImages = replaceAllImages,
- SearchResult = searchResult
+ SearchResult = searchResult,
+ RemoveOldMetadata = true
},
CancellationToken.None).ConfigureAwait(false);
diff --git a/Jellyfin.Api/Controllers/UserLibraryController.cs b/Jellyfin.Api/Controllers/UserLibraryController.cs
index 8b99170d9..90cb4a74a 100644
--- a/Jellyfin.Api/Controllers/UserLibraryController.cs
+++ b/Jellyfin.Api/Controllers/UserLibraryController.cs
@@ -206,21 +206,16 @@ namespace Jellyfin.Api.Controllers
: _libraryManager.GetItemById(itemId);
var dtoOptions = new DtoOptions().AddClientFields(Request);
- var dtosExtras = item.GetExtras(new[] { ExtraType.Trailer })
- .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item))
- .ToArray();
if (item is IHasTrailers hasTrailers)
{
var trailers = hasTrailers.LocalTrailers;
- var dtosTrailers = _dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item);
- var allTrailers = new BaseItemDto[dtosExtras.Length + dtosTrailers.Count];
- dtosExtras.CopyTo(allTrailers, 0);
- dtosTrailers.CopyTo(allTrailers, dtosExtras.Length);
- return allTrailers;
+ return Ok(_dtoService.GetBaseItemDtos(trailers, dtoOptions, user, item));
}
- return dtosExtras;
+ return Ok(item.GetExtras()
+ .Where(e => e.ExtraType == ExtraType.Trailer)
+ .Select(i => _dtoService.GetBaseItemDto(i, dtoOptions, user, item)));
}
/// <summary>
diff --git a/Jellyfin.Api/Helpers/RequestHelpers.cs b/Jellyfin.Api/Helpers/RequestHelpers.cs
index 1471f5a24..2cfd36d00 100644
--- a/Jellyfin.Api/Helpers/RequestHelpers.cs
+++ b/Jellyfin.Api/Helpers/RequestHelpers.cs
@@ -104,7 +104,7 @@ namespace Jellyfin.Api.Helpers
}
internal static QueryResult<BaseItemDto> CreateQueryResult(
- QueryResult<(BaseItem, ItemCounts)> result,
+ QueryResult<(BaseItem Item, ItemCounts ItemCounts)> result,
DtoOptions dtoOptions,
IDtoService dtoService,
bool includeItemTypes,
diff --git a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
index 6b93d8d87..882abc927 100644
--- a/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
+++ b/MediaBrowser.Controller/Entities/Movies/BoxSet.cs
@@ -33,6 +33,7 @@ namespace MediaBrowser.Controller.Entities.Movies
public override bool SupportsPeople => true;
/// <inheritdoc />
+ [JsonIgnore]
public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
.Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
.ToArray();
diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs
index dfaf03fda..77e70f8fb 100644
--- a/MediaBrowser.Controller/Entities/Movies/Movie.cs
+++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs
@@ -20,12 +20,14 @@ namespace MediaBrowser.Controller.Entities.Movies
public class Movie : Video, IHasSpecialFeatures, IHasTrailers, IHasLookupInfo<MovieInfo>, ISupportsBoxSetGrouping
{
/// <inheritdoc />
+ [JsonIgnore]
public IReadOnlyList<Guid> SpecialFeatureIds => GetExtras()
.Where(extra => extra.ExtraType != null && extra is Video)
.Select(extra => extra.Id)
.ToArray();
/// <inheritdoc />
+ [JsonIgnore]
public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
.Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
.ToArray();
diff --git a/MediaBrowser.Controller/Entities/TV/Episode.cs b/MediaBrowser.Controller/Entities/TV/Episode.cs
index dcc752f8c..c8a0e21eb 100644
--- a/MediaBrowser.Controller/Entities/TV/Episode.cs
+++ b/MediaBrowser.Controller/Entities/TV/Episode.cs
@@ -11,6 +11,7 @@ using Jellyfin.Data.Enums;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
+using MediaBrowser.Model.Providers;
using Microsoft.Extensions.Logging;
namespace MediaBrowser.Controller.Entities.TV
@@ -21,6 +22,7 @@ namespace MediaBrowser.Controller.Entities.TV
public class Episode : Video, IHasTrailers, IHasLookupInfo<EpisodeInfo>, IHasSeries
{
/// <inheritdoc />
+ [JsonIgnore]
public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
.Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
.ToArray();
@@ -336,5 +338,22 @@ namespace MediaBrowser.Controller.Entities.TV
return hasChanges;
}
+
+ public override List<ExternalUrl> GetRelatedUrls()
+ {
+ var list = base.GetRelatedUrls();
+
+ var imdbId = this.GetProviderId(MetadataProvider.Imdb);
+ if (!string.IsNullOrEmpty(imdbId))
+ {
+ list.Add(new ExternalUrl
+ {
+ Name = "Trakt",
+ Url = string.Format(CultureInfo.InvariantCulture, "https://trakt.tv/episodes/{0}", imdbId)
+ });
+ }
+
+ return list;
+ }
}
}
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index bdadc2775..a3c4a81fd 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -50,6 +50,7 @@ namespace MediaBrowser.Controller.Entities.TV
public override bool SupportsPeople => true;
/// <inheritdoc />
+ [JsonIgnore]
public IReadOnlyList<BaseItem> LocalTrailers => GetExtras()
.Where(extra => extra.ExtraType == Model.Entities.ExtraType.Trailer)
.ToArray();
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index d6f69a150..bde10dbbf 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -11,6 +11,7 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
using MediaBrowser.Model.Configuration;
using MediaBrowser.Model.Dlna;
using MediaBrowser.Model.Dto;
@@ -885,6 +886,13 @@ namespace MediaBrowser.Controller.MediaEncoding
if (state.AudioStream != null && state.AudioStream.IsExternal)
{
+ // Also seek the external audio stream.
+ var seekAudioParam = GetFastSeekCommandLineParameter(state, options);
+ if (!string.IsNullOrEmpty(seekAudioParam))
+ {
+ arg.Append(' ').Append(seekAudioParam);
+ }
+
arg.Append(" -i \"").Append(state.AudioStream.Path).Append('"');
}
@@ -1278,7 +1286,7 @@ namespace MediaBrowser.Controller.MediaEncoding
param += " -pix_fmt nv21";
}
- var isVc1 = string.Equals(state.VideoStream.Codec, "vc1", StringComparison.OrdinalIgnoreCase);
+ var isVc1 = string.Equals(state.VideoStream?.Codec, "vc1", StringComparison.OrdinalIgnoreCase);
var isLibX265 = string.Equals(videoEncoder, "libx265", StringComparison.OrdinalIgnoreCase);
if (string.Equals(videoEncoder, "libx264", StringComparison.OrdinalIgnoreCase) || isLibX265)
@@ -1318,7 +1326,7 @@ namespace MediaBrowser.Controller.MediaEncoding
{
string[] valid_h264_qsv = { "veryslow", "slower", "slow", "medium", "fast", "faster", "veryfast" };
- if (valid_h264_qsv.Contains(encodingOptions.EncoderPreset, StringComparer.OrdinalIgnoreCase))
+ if (valid_h264_qsv.Contains(encodingOptions.EncoderPreset, StringComparison.OrdinalIgnoreCase))
{
param += " -preset " + encodingOptions.EncoderPreset;
}
@@ -1669,7 +1677,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// Source and target codecs must match
if (string.IsNullOrEmpty(videoStream.Codec)
- || !state.SupportedVideoCodecs.Contains(videoStream.Codec, StringComparer.OrdinalIgnoreCase))
+ || !state.SupportedVideoCodecs.Contains(videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
return false;
}
@@ -1687,7 +1695,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var requestedProfile = requestedProfiles[0];
// strip spaces because they may be stripped out on the query string as well
if (!string.IsNullOrEmpty(videoStream.Profile)
- && !requestedProfiles.Contains(videoStream.Profile.Replace(" ", string.Empty, StringComparison.Ordinal), StringComparer.OrdinalIgnoreCase))
+ && !requestedProfiles.Contains(videoStream.Profile.Replace(" ", string.Empty, StringComparison.Ordinal), StringComparison.OrdinalIgnoreCase))
{
var currentScore = GetVideoProfileScore(videoStream.Codec, videoStream.Profile);
var requestedScore = GetVideoProfileScore(videoStream.Codec, requestedProfile);
@@ -1794,7 +1802,7 @@ namespace MediaBrowser.Controller.MediaEncoding
// Source and target codecs must match
if (string.IsNullOrEmpty(audioStream.Codec)
- || !supportedAudioCodecs.Contains(audioStream.Codec, StringComparer.OrdinalIgnoreCase))
+ || !supportedAudioCodecs.Contains(audioStream.Codec, StringComparison.OrdinalIgnoreCase))
{
return false;
}
@@ -4302,11 +4310,19 @@ namespace MediaBrowser.Controller.MediaEncoding
var decoderName = decoderPrefix + '_' + decoderSuffix;
- var isCodecAvailable = _mediaEncoder.SupportsDecoder(decoderName) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase);
+ var isCodecAvailable = _mediaEncoder.SupportsDecoder(decoderName) && options.HardwareDecodingCodecs.Contains(videoCodec, StringComparison.OrdinalIgnoreCase);
if (bitDepth == 10 && isCodecAvailable)
{
- if ((options.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase) && !options.EnableDecodingColorDepth10Hevc)
- || (options.HardwareDecodingCodecs.Contains("vp9", StringComparer.OrdinalIgnoreCase) && !options.EnableDecodingColorDepth10Vp9))
+ if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
+ && options.HardwareDecodingCodecs.Contains("hevc", StringComparison.OrdinalIgnoreCase)
+ && !options.EnableDecodingColorDepth10Hevc)
+ {
+ return null;
+ }
+
+ if (string.Equals(videoCodec, "vp9", StringComparison.OrdinalIgnoreCase)
+ && options.HardwareDecodingCodecs.Contains("vp9", StringComparison.OrdinalIgnoreCase)
+ && !options.EnableDecodingColorDepth10Vp9)
{
return null;
}
@@ -4344,15 +4360,23 @@ namespace MediaBrowser.Controller.MediaEncoding
var isCudaSupported = (isLinux || isWindows) && IsCudaFullSupported();
var isQsvSupported = (isLinux || isWindows) && _mediaEncoder.SupportsHwaccel("qsv");
var isVideotoolboxSupported = isMacOS && _mediaEncoder.SupportsHwaccel("videotoolbox");
- var isCodecAvailable = options.HardwareDecodingCodecs.Contains(videoCodec, StringComparer.OrdinalIgnoreCase);
+ var isCodecAvailable = options.HardwareDecodingCodecs.Contains(videoCodec, StringComparison.OrdinalIgnoreCase);
// Set the av1 codec explicitly to trigger hw accelerator, otherwise libdav1d will be used.
var isAv1 = string.Equals(videoCodec, "av1", StringComparison.OrdinalIgnoreCase);
if (bitDepth == 10 && isCodecAvailable)
{
- if ((options.HardwareDecodingCodecs.Contains("hevc", StringComparer.OrdinalIgnoreCase) && !options.EnableDecodingColorDepth10Hevc)
- || (options.HardwareDecodingCodecs.Contains("vp9", StringComparer.OrdinalIgnoreCase) && !options.EnableDecodingColorDepth10Vp9))
+ if (string.Equals(videoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
+ && options.HardwareDecodingCodecs.Contains("hevc", StringComparison.OrdinalIgnoreCase)
+ && !options.EnableDecodingColorDepth10Hevc)
+ {
+ return null;
+ }
+
+ if (string.Equals(videoCodec, "vp9", StringComparison.OrdinalIgnoreCase)
+ && options.HardwareDecodingCodecs.Contains("vp9", StringComparison.OrdinalIgnoreCase)
+ && !options.EnableDecodingColorDepth10Vp9)
{
return null;
}
@@ -5072,12 +5096,12 @@ namespace MediaBrowser.Controller.MediaEncoding
// Transcoding to 2ch ac3 almost always causes a playback failure
// Keep it in the supported codecs list, but shift it to the end of the list so that if transcoding happens, another codec is used
var shiftAudioCodecs = new[] { "ac3", "eac3" };
- if (audioCodecs.All(i => shiftAudioCodecs.Contains(i, StringComparer.OrdinalIgnoreCase)))
+ if (audioCodecs.All(i => shiftAudioCodecs.Contains(i, StringComparison.OrdinalIgnoreCase)))
{
return;
}
- while (shiftAudioCodecs.Contains(audioCodecs[0], StringComparer.OrdinalIgnoreCase))
+ while (shiftAudioCodecs.Contains(audioCodecs[0], StringComparison.OrdinalIgnoreCase))
{
var removed = shiftAudioCodecs[0];
audioCodecs.RemoveAt(0);
@@ -5100,12 +5124,12 @@ namespace MediaBrowser.Controller.MediaEncoding
}
var shiftVideoCodecs = new[] { "hevc", "h265" };
- if (videoCodecs.All(i => shiftVideoCodecs.Contains(i, StringComparer.OrdinalIgnoreCase)))
+ if (videoCodecs.All(i => shiftVideoCodecs.Contains(i, StringComparison.OrdinalIgnoreCase)))
{
return;
}
- while (shiftVideoCodecs.Contains(videoCodecs[0], StringComparer.OrdinalIgnoreCase))
+ while (shiftVideoCodecs.Contains(videoCodecs[0], StringComparison.OrdinalIgnoreCase))
{
var removed = shiftVideoCodecs[0];
videoCodecs.RemoveAt(0);
diff --git a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs
index 08d129a82..a9d16a49e 100644
--- a/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs
+++ b/MediaBrowser.Controller/Providers/ImageRefreshOptions.cs
@@ -27,6 +27,11 @@ namespace MediaBrowser.Controller.Providers
public bool IsAutomated { get; set; }
+ /// <summary>
+ /// Gets or sets a value indicating whether old metadata should be removed if it isn't replaced.
+ /// </summary>
+ public bool RemoveOldMetadata { get; set; }
+
public bool IsReplacingImage(ImageType type)
{
return ImageRefreshMode == MetadataRefreshMode.FullRefresh &&
diff --git a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
index 90fd6e269..a38bbaf69 100644
--- a/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
+++ b/MediaBrowser.Controller/Providers/MetadataRefreshOptions.cs
@@ -30,6 +30,7 @@ namespace MediaBrowser.Controller.Providers
ReplaceAllImages = copy.ReplaceAllImages;
ReplaceImages = copy.ReplaceImages;
SearchResult = copy.SearchResult;
+ RemoveOldMetadata = copy.RemoveOldMetadata;
if (copy.RefreshPaths != null && copy.RefreshPaths.Length > 0)
{
diff --git a/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs b/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs
new file mode 100644
index 000000000..68a5c2534
--- /dev/null
+++ b/MediaBrowser.Model/Drawing/ImageFormatExtensions.cs
@@ -0,0 +1,27 @@
+using System.ComponentModel;
+using System.Net.Mime;
+
+namespace MediaBrowser.Model.Drawing;
+
+/// <summary>
+/// Extension class for the <see cref="ImageFormat" /> enum.
+/// </summary>
+public static class ImageFormatExtensions
+{
+ /// <summary>
+ /// Returns the correct mime type for this <see cref="ImageFormat" />.
+ /// </summary>
+ /// <param name="format">This <see cref="ImageFormat" />.</param>
+ /// <exception cref="InvalidEnumArgumentException">The <paramref name="format"/> is an invalid enumeration value.</exception>
+ /// <returns>The correct mime type for this <see cref="ImageFormat" />.</returns>
+ public static string GetMimeType(this ImageFormat format)
+ => format switch
+ {
+ ImageFormat.Bmp => "image/bmp",
+ ImageFormat.Gif => MediaTypeNames.Image.Gif,
+ ImageFormat.Jpg => MediaTypeNames.Image.Jpeg,
+ ImageFormat.Png => "image/png",
+ ImageFormat.Webp => "image/webp",
+ _ => throw new InvalidEnumArgumentException(nameof(format), (int)format, typeof(ImageFormat))
+ };
+}
diff --git a/MediaBrowser.Model/Net/MimeTypes.cs b/MediaBrowser.Model/Net/MimeTypes.cs
index ee8451853..3b03466e9 100644
--- a/MediaBrowser.Model/Net/MimeTypes.cs
+++ b/MediaBrowser.Model/Net/MimeTypes.cs
@@ -116,7 +116,6 @@ namespace MediaBrowser.Model.Net
{ "audio/x-wavpack", ".wv" },
// Type image
- { "image/jpg", ".jpg" },
{ "image/jpeg", ".jpg" },
{ "image/x-png", ".png" },
diff --git a/MediaBrowser.Providers/Books/AudioBookMetadataService.cs b/MediaBrowser.Providers/Books/AudioBookMetadataService.cs
index eabc66c6b..96e1165b6 100644
--- a/MediaBrowser.Providers/Books/AudioBookMetadataService.cs
+++ b/MediaBrowser.Providers/Books/AudioBookMetadataService.cs
@@ -31,7 +31,7 @@ namespace MediaBrowser.Providers.Books
bool replaceData,
bool mergeMetadataSettings)
{
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+ base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
var sourceItem = source.Item;
var targetItem = target.Item;
diff --git a/MediaBrowser.Providers/Books/BookMetadataService.cs b/MediaBrowser.Providers/Books/BookMetadataService.cs
index 3f3782dfb..50b9922c6 100644
--- a/MediaBrowser.Providers/Books/BookMetadataService.cs
+++ b/MediaBrowser.Providers/Books/BookMetadataService.cs
@@ -26,7 +26,7 @@ namespace MediaBrowser.Providers.Books
/// <inheritdoc />
protected override void MergeData(MetadataResult<Book> source, MetadataResult<Book> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
{
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+ base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
if (replaceData || string.IsNullOrEmpty(target.Item.SeriesName))
{
diff --git a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs
index 88ce8d087..cbbb343e5 100644
--- a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs
+++ b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs
@@ -47,7 +47,7 @@ namespace MediaBrowser.Providers.BoxSets
/// <inheritdoc />
protected override void MergeData(MetadataResult<BoxSet> source, MetadataResult<BoxSet> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
{
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+ base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
var sourceItem = source.Item;
var targetItem = target.Item;
diff --git a/MediaBrowser.Providers/Channels/ChannelMetadataService.cs b/MediaBrowser.Providers/Channels/ChannelMetadataService.cs
index db2213bad..0267fa13f 100644
--- a/MediaBrowser.Providers/Channels/ChannelMetadataService.cs
+++ b/MediaBrowser.Providers/Channels/ChannelMetadataService.cs
@@ -4,7 +4,6 @@ using MediaBrowser.Controller.Channels;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
@@ -22,11 +21,5 @@ namespace MediaBrowser.Providers.Channels
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
{
}
-
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<Channel> source, MetadataResult<Channel> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
- {
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- }
}
}
diff --git a/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs b/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs
index e0f3131fd..0629824d3 100644
--- a/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs
+++ b/MediaBrowser.Providers/Folders/CollectionFolderMetadataService.cs
@@ -4,7 +4,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
@@ -22,11 +21,5 @@ namespace MediaBrowser.Providers.Folders
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
{
}
-
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<CollectionFolder> source, MetadataResult<CollectionFolder> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
- {
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- }
}
}
diff --git a/MediaBrowser.Providers/Folders/FolderMetadataService.cs b/MediaBrowser.Providers/Folders/FolderMetadataService.cs
index 998bf4c6a..79d52991a 100644
--- a/MediaBrowser.Providers/Folders/FolderMetadataService.cs
+++ b/MediaBrowser.Providers/Folders/FolderMetadataService.cs
@@ -4,7 +4,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
@@ -26,11 +25,5 @@ namespace MediaBrowser.Providers.Folders
/// <inheritdoc />
// Make sure the type-specific services get picked first
public override int Order => 10;
-
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<Folder> source, MetadataResult<Folder> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
- {
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- }
}
}
diff --git a/MediaBrowser.Providers/Folders/UserViewMetadataService.cs b/MediaBrowser.Providers/Folders/UserViewMetadataService.cs
index 2d536f12e..79c5597e5 100644
--- a/MediaBrowser.Providers/Folders/UserViewMetadataService.cs
+++ b/MediaBrowser.Providers/Folders/UserViewMetadataService.cs
@@ -4,7 +4,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
@@ -22,11 +21,5 @@ namespace MediaBrowser.Providers.Folders
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
{
}
-
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<UserView> source, MetadataResult<UserView> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
- {
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- }
}
}
diff --git a/MediaBrowser.Providers/Genres/GenreMetadataService.cs b/MediaBrowser.Providers/Genres/GenreMetadataService.cs
index f7ea767e7..4d10d8987 100644
--- a/MediaBrowser.Providers/Genres/GenreMetadataService.cs
+++ b/MediaBrowser.Providers/Genres/GenreMetadataService.cs
@@ -4,7 +4,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
@@ -22,11 +21,5 @@ namespace MediaBrowser.Providers.Genres
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
{
}
-
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<Genre> source, MetadataResult<Genre> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
- {
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- }
}
}
diff --git a/MediaBrowser.Providers/LiveTv/LiveTvMetadataService.cs b/MediaBrowser.Providers/LiveTv/LiveTvMetadataService.cs
index 2e6cf4530..c94d36530 100644
--- a/MediaBrowser.Providers/LiveTv/LiveTvMetadataService.cs
+++ b/MediaBrowser.Providers/LiveTv/LiveTvMetadataService.cs
@@ -4,7 +4,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
@@ -22,11 +21,5 @@ namespace MediaBrowser.Providers.LiveTv
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
{
}
-
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<LiveTvChannel> source, MetadataResult<LiveTvChannel> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
- {
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- }
}
}
diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
index b1d73c4c4..0f21ec7b2 100644
--- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs
+++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs
@@ -14,6 +14,7 @@ using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Drawing;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Model.MediaInfo;
@@ -62,6 +63,29 @@ namespace MediaBrowser.Providers.Manager
}
/// <summary>
+ /// Removes all existing images from the provided item.
+ /// </summary>
+ /// <param name="item">The <see cref="BaseItem"/> to remove images from.</param>
+ /// <returns><c>true</c> if changes were made to the item; otherwise <c>false</c>.</returns>
+ public bool RemoveImages(BaseItem item)
+ {
+ var singular = new List<ItemImageInfo>();
+ for (var i = 0; i < _singularImages.Length; i++)
+ {
+ var currentImage = item.GetImageInfo(_singularImages[i], 0);
+ if (currentImage != null)
+ {
+ singular.Add(currentImage);
+ }
+ }
+
+ singular.AddRange(item.GetImages(ImageType.Backdrop));
+ PruneImages(item, singular);
+
+ return singular.Count > 0;
+ }
+
+ /// <summary>
/// Verifies existing images have valid paths and adds any new local images provided.
/// </summary>
/// <param name="item">The <see cref="BaseItem"/> to validate images for.</param>
@@ -99,7 +123,7 @@ namespace MediaBrowser.Providers.Manager
public async Task<RefreshResult> RefreshImages(
BaseItem item,
LibraryOptions libraryOptions,
- List<IImageProvider> providers,
+ IEnumerable<IImageProvider> providers,
ImageRefreshOptions refreshOptions,
CancellationToken cancellationToken)
{
@@ -159,24 +183,30 @@ namespace MediaBrowser.Providers.Manager
foreach (var imageType in images)
{
- if (!IsEnabled(savedOptions, imageType))
+ if (!savedOptions.IsEnabled(imageType))
{
continue;
}
- if (!HasImage(item, imageType) || (refreshOptions.IsReplacingImage(imageType) && !downloadedImages.Contains(imageType)))
+ if (!item.HasImage(imageType) || (refreshOptions.IsReplacingImage(imageType) && !downloadedImages.Contains(imageType)))
{
- _logger.LogDebug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name);
+ _logger.LogDebug("Running {Provider} for {Item}", provider.GetType().Name, item.Path ?? item.Name);
var response = await provider.GetImage(item, imageType, cancellationToken).ConfigureAwait(false);
if (response.HasImage)
{
- if (!string.IsNullOrEmpty(response.Path))
+ if (string.IsNullOrEmpty(response.Path))
+ {
+ var mimeType = response.Format.GetMimeType();
+
+ await _providerManager.SaveImage(item, response.Stream, mimeType, imageType, null, cancellationToken).ConfigureAwait(false);
+ }
+ else
{
if (response.Protocol == MediaProtocol.Http)
{
- _logger.LogDebug("Setting image url into item {0}", item.Id);
+ _logger.LogDebug("Setting image url into item {Item}", item.Id);
var index = item.AllowsMultipleImages(imageType) ? item.GetImages(imageType).Count() : 0;
item.SetImage(
new ItemImageInfo
@@ -195,12 +225,6 @@ namespace MediaBrowser.Providers.Manager
await _providerManager.SaveImage(item, stream, mimeType, imageType, null, cancellationToken).ConfigureAwait(false);
}
}
- else
- {
- var mimeType = "image/" + response.Format.ToString().ToLowerInvariant();
-
- await _providerManager.SaveImage(item, response.Stream, mimeType, imageType, null, cancellationToken).ConfigureAwait(false);
- }
downloadedImages.Add(imageType);
result.UpdateType |= ItemUpdateType.ImageUpdate;
@@ -219,39 +243,6 @@ namespace MediaBrowser.Providers.Manager
}
}
- private bool HasImage(BaseItem item, ImageType type)
- {
- return item.HasImage(type);
- }
-
- /// <summary>
- /// Determines if an item already contains the given images.
- /// </summary>
- /// <param name="item">The item.</param>
- /// <param name="images">The images.</param>
- /// <param name="savedOptions">The saved options.</param>
- /// <param name="backdropLimit">The backdrop limit.</param>
- /// <returns><c>true</c> if the specified item contains images; otherwise, <c>false</c>.</returns>
- private bool ContainsImages(BaseItem item, List<ImageType> images, TypeOptions savedOptions, int backdropLimit)
- {
- // Using .Any causes the creation of a DisplayClass aka. variable capture
- for (var i = 0; i < _singularImages.Length; i++)
- {
- var type = _singularImages[i];
- if (images.Contains(type) && !HasImage(item, type) && savedOptions.GetLimit(type) > 0)
- {
- return false;
- }
- }
-
- if (images.Contains(ImageType.Backdrop) && item.GetImages(ImageType.Backdrop).Count() < backdropLimit)
- {
- return false;
- }
-
- return true;
- }
-
/// <summary>
/// Refreshes from a remote provider.
/// </summary>
@@ -288,7 +279,7 @@ namespace MediaBrowser.Providers.Manager
return;
}
- _logger.LogDebug("Running {0} for {1}", provider.GetType().Name, item.Path ?? item.Name);
+ _logger.LogDebug("Running {Provider} for {Item}", provider.GetType().Name, item.Path ?? item.Name);
var images = await _providerManager.GetAvailableRemoteImages(
item,
@@ -304,12 +295,12 @@ namespace MediaBrowser.Providers.Manager
foreach (var imageType in _singularImages)
{
- if (!IsEnabled(savedOptions, imageType))
+ if (!savedOptions.IsEnabled(imageType))
{
continue;
}
- if (!HasImage(item, imageType) || (refreshOptions.IsReplacingImage(imageType) && !downloadedImages.Contains(imageType)))
+ if (!item.HasImage(imageType) || (refreshOptions.IsReplacingImage(imageType) && !downloadedImages.Contains(imageType)))
{
minWidth = savedOptions.GetMinWidth(imageType);
var downloaded = await DownloadImage(item, provider, result, list, minWidth, imageType, cancellationToken).ConfigureAwait(false);
@@ -335,14 +326,37 @@ namespace MediaBrowser.Providers.Manager
}
}
- private bool IsEnabled(TypeOptions options, ImageType type)
+ /// <summary>
+ /// Determines if an item already contains the given images.
+ /// </summary>
+ /// <param name="item">The item.</param>
+ /// <param name="images">The images.</param>
+ /// <param name="savedOptions">The saved options.</param>
+ /// <param name="backdropLimit">The backdrop limit.</param>
+ /// <returns><c>true</c> if the specified item contains images; otherwise, <c>false</c>.</returns>
+ private bool ContainsImages(BaseItem item, List<ImageType> images, TypeOptions savedOptions, int backdropLimit)
{
- return options.IsEnabled(type);
+ // Using .Any causes the creation of a DisplayClass aka. variable capture
+ for (var i = 0; i < _singularImages.Length; i++)
+ {
+ var type = _singularImages[i];
+ if (images.Contains(type) && !item.HasImage(type) && savedOptions.GetLimit(type) > 0)
+ {
+ return false;
+ }
+ }
+
+ if (images.Contains(ImageType.Backdrop) && item.GetImages(ImageType.Backdrop).Count() < backdropLimit)
+ {
+ return false;
+ }
+
+ return true;
}
- private void PruneImages(BaseItem item, ItemImageInfo[] images)
+ private void PruneImages(BaseItem item, IReadOnlyList<ItemImageInfo> images)
{
- for (var i = 0; i < images.Length; i++)
+ for (var i = 0; i < images.Count; i++)
{
var image = images[i];
@@ -354,6 +368,11 @@ namespace MediaBrowser.Providers.Manager
}
catch (FileNotFoundException)
{
+ // nothing to do, already gone
+ }
+ catch (UnauthorizedAccessException ex)
+ {
+ _logger.LogWarning(ex, "Unable to delete {Image}", image.Path);
}
}
}
@@ -380,12 +399,7 @@ namespace MediaBrowser.Providers.Manager
{
var currentImage = item.GetImageInfo(type, 0);
- if (currentImage == null)
- {
- item.SetImagePath(type, image.FileInfo);
- changed = true;
- }
- else if (!string.Equals(currentImage.Path, image.FileInfo.FullName, StringComparison.OrdinalIgnoreCase))
+ if (currentImage == null || !string.Equals(currentImage.Path, image.FileInfo.FullName, StringComparison.OrdinalIgnoreCase))
{
item.SetImagePath(type, image.FileInfo);
changed = true;
@@ -493,7 +507,7 @@ namespace MediaBrowser.Providers.Manager
await _providerManager.SaveImage(
item,
stream,
- response.Content.Headers.ContentType.MediaType,
+ response.Content.Headers.ContentType?.MediaType,
type,
null,
cancellationToken).ConfigureAwait(false);
@@ -616,11 +630,11 @@ namespace MediaBrowser.Providers.Manager
await _providerManager.SaveImage(
item,
stream,
- response.Content.Headers.ContentType.MediaType,
+ response.Content.Headers.ContentType?.MediaType,
imageType,
null,
cancellationToken).ConfigureAwait(false);
- result.UpdateType = result.UpdateType | ItemUpdateType.ImageUpdate;
+ result.UpdateType |= ItemUpdateType.ImageUpdate;
}
catch (HttpRequestException)
{
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index 94045b38b..0c52d2673 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -8,8 +8,10 @@ using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
+using Diacritics.Extensions;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
@@ -74,14 +76,10 @@ namespace MediaBrowser.Providers.Manager
var itemOfType = (TItemType)item;
var updateType = ItemUpdateType.None;
- var requiresRefresh = false;
var libraryOptions = LibraryManager.GetLibraryOptions(item);
- if (!requiresRefresh && libraryOptions.AutomaticRefreshIntervalDays > 0 && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= libraryOptions.AutomaticRefreshIntervalDays)
- {
- requiresRefresh = true;
- }
+ var requiresRefresh = libraryOptions.AutomaticRefreshIntervalDays > 0 && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= libraryOptions.AutomaticRefreshIntervalDays;
if (!requiresRefresh && refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None)
{
@@ -90,7 +88,7 @@ namespace MediaBrowser.Providers.Manager
if (requiresRefresh)
{
- Logger.LogDebug("Refreshing {0} {1} because item.RequiresRefresh() returned true", typeof(TItemType).Name, item.Path ?? item.Name);
+ Logger.LogDebug("Refreshing {Type} {Item} because item.RequiresRefresh() returned true", typeof(TItemType).Name, item.Path ?? item.Name);
}
}
@@ -98,6 +96,14 @@ namespace MediaBrowser.Providers.Manager
var allImageProviders = ((ProviderManager)ProviderManager).GetImageProviders(item, refreshOptions).ToList();
+ if (refreshOptions.RemoveOldMetadata && refreshOptions.ReplaceAllImages)
+ {
+ if (ImageProvider.RemoveImages(item))
+ {
+ updateType |= ItemUpdateType.ImageUpdate;
+ }
+ }
+
// Start by validating images
try
{
@@ -110,7 +116,7 @@ namespace MediaBrowser.Providers.Manager
catch (Exception ex)
{
localImagesFailed = true;
- Logger.LogError(ex, "Error validating images for {0}", item.Path ?? item.Name ?? "Unknown name");
+ Logger.LogError(ex, "Error validating images for {Item}", item.Path ?? item.Name ?? "Unknown name");
}
var metadataResult = new MetadataResult<TItemType>
@@ -380,8 +386,7 @@ namespace MediaBrowser.Providers.Manager
{
var updateType = ItemUpdateType.None;
- var folder = item as Folder;
- if (folder != null && folder.SupportsDateLastMediaAdded)
+ if (item is Folder folder && folder.SupportsDateLastMediaAdded)
{
var dateLastMediaAdded = DateTime.MinValue;
var any = false;
@@ -668,7 +673,7 @@ namespace MediaBrowser.Providers.Manager
foreach (var provider in providers.OfType<ILocalMetadataProvider<TItemType>>().ToList())
{
var providerName = provider.GetType().Name;
- Logger.LogDebug("Running {0} for {1}", providerName, logName);
+ Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
var itemInfo = new ItemInfo(item);
@@ -713,7 +718,7 @@ namespace MediaBrowser.Providers.Manager
break;
}
- Logger.LogDebug("{0} returned no metadata for {1}", providerName, logName);
+ Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName);
}
catch (OperationCanceledException)
{
@@ -749,8 +754,11 @@ namespace MediaBrowser.Providers.Manager
}
else
{
- // TODO: If the new metadata from above has some blank data, this can cause old data to get filled into those empty fields
- MergeData(metadata, temp, Array.Empty<MetadataField>(), false, false);
+ if (!options.RemoveOldMetadata)
+ {
+ MergeData(metadata, temp, Array.Empty<MetadataField>(), false, false);
+ }
+
MergeData(temp, metadata, item.LockedFields, true, false);
}
}
@@ -780,7 +788,7 @@ namespace MediaBrowser.Providers.Manager
private async Task RunCustomProvider(ICustomMetadataProvider<TItemType> provider, TItemType item, string logName, MetadataRefreshOptions options, RefreshResult refreshResult, CancellationToken cancellationToken)
{
- Logger.LogDebug("Running {0} for {1}", provider.GetType().Name, logName);
+ Logger.LogDebug("Running {Provider} for {Item}", provider.GetType().Name, logName);
try
{
@@ -811,7 +819,7 @@ namespace MediaBrowser.Providers.Manager
foreach (var provider in providers)
{
var providerName = provider.GetType().Name;
- Logger.LogDebug("Running {0} for {1}", providerName, logName);
+ Logger.LogDebug("Running {Provider} for {Item}", providerName, logName);
if (id != null && !tmpDataMerged)
{
@@ -834,7 +842,7 @@ namespace MediaBrowser.Providers.Manager
}
else
{
- Logger.LogDebug("{0} returned no metadata for {1}", providerName, logName);
+ Logger.LogDebug("{Provider} returned no metadata for {Item}", providerName, logName);
}
}
catch (OperationCanceledException)
@@ -867,13 +875,6 @@ namespace MediaBrowser.Providers.Manager
}
}
- protected abstract void MergeData(
- MetadataResult<TItemType> source,
- MetadataResult<TItemType> target,
- MetadataField[] lockedFields,
- bool replaceData,
- bool mergeMetadataSettings);
-
private bool HasChanged(BaseItem item, IHasItemChangeMonitor changeMonitor, IDirectoryService directoryService)
{
try
@@ -882,16 +883,312 @@ namespace MediaBrowser.Providers.Manager
if (hasChanged)
{
- Logger.LogDebug("{0} reports change to {1}", changeMonitor.GetType().Name, item.Path ?? item.Name);
+ Logger.LogDebug("{Monitor} reports change to {Item}", changeMonitor.GetType().Name, item.Path ?? item.Name);
}
return hasChanged;
}
catch (Exception ex)
{
- Logger.LogError(ex, "Error in {0}.HasChanged", changeMonitor.GetType().Name);
+ Logger.LogError(ex, "Error in {Monitor}.HasChanged", changeMonitor.GetType().Name);
return false;
}
}
+
+ /// <summary>
+ /// Merges metadata from source into target.
+ /// </summary>
+ /// <param name="source">The source for new metadata.</param>
+ /// <param name="target">The target to insert new metadata into.</param>
+ /// <param name="lockedFields">The fields that are locked and should not be updated.</param>
+ /// <param name="replaceData"><c>true</c> if existing data should be replaced.</param>
+ /// <param name="mergeMetadataSettings"><c>true</c> if the metadata settings in target should be updated to match source.</param>
+ /// <exception cref="ArgumentException">Thrown if source or target are null.</exception>
+ protected virtual void MergeData(
+ MetadataResult<TItemType> source,
+ MetadataResult<TItemType> target,
+ MetadataField[] lockedFields,
+ bool replaceData,
+ bool mergeMetadataSettings)
+ {
+ MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+ }
+
+ internal static void MergeBaseItemData(
+ MetadataResult<TItemType> sourceResult,
+ MetadataResult<TItemType> targetResult,
+ MetadataField[] lockedFields,
+ bool replaceData,
+ bool mergeMetadataSettings)
+ {
+ var source = sourceResult.Item;
+ var target = targetResult.Item;
+
+ if (source == null)
+ {
+ throw new ArgumentException("Item cannot be null.", nameof(sourceResult));
+ }
+
+ if (target == null)
+ {
+ throw new ArgumentException("Item cannot be null.", nameof(targetResult));
+ }
+
+ if (!lockedFields.Contains(MetadataField.Name))
+ {
+ if (replaceData || string.IsNullOrEmpty(target.Name))
+ {
+ // Safeguard against incoming data having an empty name
+ if (!string.IsNullOrWhiteSpace(source.Name))
+ {
+ target.Name = source.Name;
+ }
+ }
+ }
+
+ if (replaceData || string.IsNullOrEmpty(target.OriginalTitle))
+ {
+ // Safeguard against incoming data having an empty name
+ if (!string.IsNullOrWhiteSpace(source.OriginalTitle))
+ {
+ target.OriginalTitle = source.OriginalTitle;
+ }
+ }
+
+ if (replaceData || !target.CommunityRating.HasValue)
+ {
+ target.CommunityRating = source.CommunityRating;
+ }
+
+ if (replaceData || !target.EndDate.HasValue)
+ {
+ target.EndDate = source.EndDate;
+ }
+
+ if (!lockedFields.Contains(MetadataField.Genres))
+ {
+ if (replaceData || target.Genres.Length == 0)
+ {
+ target.Genres = source.Genres;
+ }
+ }
+
+ if (replaceData || !target.IndexNumber.HasValue)
+ {
+ target.IndexNumber = source.IndexNumber;
+ }
+
+ if (!lockedFields.Contains(MetadataField.OfficialRating))
+ {
+ if (replaceData || string.IsNullOrEmpty(target.OfficialRating))
+ {
+ target.OfficialRating = source.OfficialRating;
+ }
+ }
+
+ if (replaceData || string.IsNullOrEmpty(target.CustomRating))
+ {
+ target.CustomRating = source.CustomRating;
+ }
+
+ if (replaceData || string.IsNullOrEmpty(target.Tagline))
+ {
+ target.Tagline = source.Tagline;
+ }
+
+ if (!lockedFields.Contains(MetadataField.Overview))
+ {
+ if (replaceData || string.IsNullOrEmpty(target.Overview))
+ {
+ target.Overview = source.Overview;
+ }
+ }
+
+ if (replaceData || !target.ParentIndexNumber.HasValue)
+ {
+ target.ParentIndexNumber = source.ParentIndexNumber;
+ }
+
+ if (!lockedFields.Contains(MetadataField.Cast))
+ {
+ if (replaceData || targetResult.People == null || targetResult.People.Count == 0)
+ {
+ targetResult.People = sourceResult.People;
+ }
+ else if (targetResult.People != null && sourceResult.People != null)
+ {
+ MergePeople(sourceResult.People, targetResult.People);
+ }
+ }
+
+ if (replaceData || !target.PremiereDate.HasValue)
+ {
+ target.PremiereDate = source.PremiereDate;
+ }
+
+ if (replaceData || !target.ProductionYear.HasValue)
+ {
+ target.ProductionYear = source.ProductionYear;
+ }
+
+ if (!lockedFields.Contains(MetadataField.Runtime))
+ {
+ if (replaceData || !target.RunTimeTicks.HasValue)
+ {
+ if (target is not Audio && target is not Video)
+ {
+ target.RunTimeTicks = source.RunTimeTicks;
+ }
+ }
+ }
+
+ if (!lockedFields.Contains(MetadataField.Studios))
+ {
+ if (replaceData || target.Studios.Length == 0)
+ {
+ target.Studios = source.Studios;
+ }
+ }
+
+ if (!lockedFields.Contains(MetadataField.Tags))
+ {
+ if (replaceData || target.Tags.Length == 0)
+ {
+ target.Tags = source.Tags;
+ }
+ }
+
+ if (!lockedFields.Contains(MetadataField.ProductionLocations))
+ {
+ if (replaceData || target.ProductionLocations.Length == 0)
+ {
+ target.ProductionLocations = source.ProductionLocations;
+ }
+ }
+
+ foreach (var id in source.ProviderIds)
+ {
+ var key = id.Key;
+
+ // Don't replace existing Id's.
+ if (replaceData)
+ {
+ target.ProviderIds[key] = id.Value;
+ }
+ else
+ {
+ target.ProviderIds.TryAdd(key, id.Value);
+ }
+ }
+
+ MergeAlbumArtist(source, target, replaceData);
+ MergeCriticRating(source, target, replaceData);
+ MergeTrailers(source, target, replaceData);
+ MergeVideoInfo(source, target, replaceData);
+ MergeDisplayOrder(source, target, replaceData);
+
+ if (replaceData || string.IsNullOrEmpty(target.ForcedSortName))
+ {
+ var forcedSortName = source.ForcedSortName;
+
+ if (!string.IsNullOrWhiteSpace(forcedSortName))
+ {
+ target.ForcedSortName = forcedSortName;
+ }
+ }
+
+ if (mergeMetadataSettings)
+ {
+ target.LockedFields = source.LockedFields;
+ target.IsLocked = source.IsLocked;
+
+ // Grab the value if it's there, but if not then don't overwrite with the default
+ if (source.DateCreated != default)
+ {
+ target.DateCreated = source.DateCreated;
+ }
+
+ target.PreferredMetadataCountryCode = source.PreferredMetadataCountryCode;
+ target.PreferredMetadataLanguage = source.PreferredMetadataLanguage;
+ }
+ }
+
+ private static void MergePeople(List<PersonInfo> source, List<PersonInfo> target)
+ {
+ foreach (var person in target)
+ {
+ var normalizedName = person.Name.RemoveDiacritics();
+ var personInSource = source.FirstOrDefault(i => string.Equals(i.Name.RemoveDiacritics(), normalizedName, StringComparison.OrdinalIgnoreCase));
+
+ if (personInSource != null)
+ {
+ foreach (var providerId in personInSource.ProviderIds)
+ {
+ person.ProviderIds.TryAdd(providerId.Key, providerId.Value);
+ }
+
+ if (string.IsNullOrWhiteSpace(person.ImageUrl))
+ {
+ person.ImageUrl = personInSource.ImageUrl;
+ }
+ }
+ }
+ }
+
+ private static void MergeDisplayOrder(BaseItem source, BaseItem target, bool replaceData)
+ {
+ if (source is IHasDisplayOrder sourceHasDisplayOrder
+ && target is IHasDisplayOrder targetHasDisplayOrder)
+ {
+ if (replaceData || string.IsNullOrEmpty(targetHasDisplayOrder.DisplayOrder))
+ {
+ var displayOrder = sourceHasDisplayOrder.DisplayOrder;
+
+ if (!string.IsNullOrWhiteSpace(displayOrder))
+ {
+ targetHasDisplayOrder.DisplayOrder = displayOrder;
+ }
+ }
+ }
+ }
+
+ private static void MergeAlbumArtist(BaseItem source, BaseItem target, bool replaceData)
+ {
+ if (source is IHasAlbumArtist sourceHasAlbumArtist
+ && target is IHasAlbumArtist targetHasAlbumArtist)
+ {
+ if (replaceData || targetHasAlbumArtist.AlbumArtists.Count == 0)
+ {
+ targetHasAlbumArtist.AlbumArtists = sourceHasAlbumArtist.AlbumArtists;
+ }
+ }
+ }
+
+ private static void MergeCriticRating(BaseItem source, BaseItem target, bool replaceData)
+ {
+ if (replaceData || !target.CriticRating.HasValue)
+ {
+ target.CriticRating = source.CriticRating;
+ }
+ }
+
+ private static void MergeTrailers(BaseItem source, BaseItem target, bool replaceData)
+ {
+ if (replaceData || target.RemoteTrailers.Count == 0)
+ {
+ target.RemoteTrailers = source.RemoteTrailers;
+ }
+ }
+
+ private static void MergeVideoInfo(BaseItem source, BaseItem target, bool replaceData)
+ {
+ if (source is Video sourceCast && target is Video targetCast)
+ {
+ if (replaceData || targetCast.Video3DFormat == null)
+ {
+ targetCast.Video3DFormat = sourceCast.Video3DFormat;
+ }
+ }
+ }
}
}
diff --git a/MediaBrowser.Providers/Manager/ProviderUtils.cs b/MediaBrowser.Providers/Manager/ProviderUtils.cs
deleted file mode 100644
index b90136d50..000000000
--- a/MediaBrowser.Providers/Manager/ProviderUtils.cs
+++ /dev/null
@@ -1,295 +0,0 @@
-#nullable disable
-
-#pragma warning disable CS1591
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Diacritics.Extensions;
-using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Entities.Audio;
-using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
-
-namespace MediaBrowser.Providers.Manager
-{
- public static class ProviderUtils
- {
- public static void MergeBaseItemData<T>(
- MetadataResult<T> sourceResult,
- MetadataResult<T> targetResult,
- MetadataField[] lockedFields,
- bool replaceData,
- bool mergeMetadataSettings)
- where T : BaseItem
- {
- var source = sourceResult.Item;
- var target = targetResult.Item;
-
- if (source == null)
- {
- throw new ArgumentException("Item cannot be null.", nameof(sourceResult));
- }
-
- if (target == null)
- {
- throw new ArgumentException("Item cannot be null.", nameof(targetResult));
- }
-
- if (!lockedFields.Contains(MetadataField.Name))
- {
- if (replaceData || string.IsNullOrEmpty(target.Name))
- {
- // Safeguard against incoming data having an empty name
- if (!string.IsNullOrWhiteSpace(source.Name))
- {
- target.Name = source.Name;
- }
- }
- }
-
- if (replaceData || string.IsNullOrEmpty(target.OriginalTitle))
- {
- // Safeguard against incoming data having an empty name
- if (!string.IsNullOrWhiteSpace(source.OriginalTitle))
- {
- target.OriginalTitle = source.OriginalTitle;
- }
- }
-
- if (replaceData || !target.CommunityRating.HasValue)
- {
- target.CommunityRating = source.CommunityRating;
- }
-
- if (replaceData || !target.EndDate.HasValue)
- {
- target.EndDate = source.EndDate;
- }
-
- if (!lockedFields.Contains(MetadataField.Genres))
- {
- if (replaceData || target.Genres.Length == 0)
- {
- target.Genres = source.Genres;
- }
- }
-
- if (replaceData || !target.IndexNumber.HasValue)
- {
- target.IndexNumber = source.IndexNumber;
- }
-
- if (!lockedFields.Contains(MetadataField.OfficialRating))
- {
- if (replaceData || string.IsNullOrEmpty(target.OfficialRating))
- {
- target.OfficialRating = source.OfficialRating;
- }
- }
-
- if (replaceData || string.IsNullOrEmpty(target.CustomRating))
- {
- target.CustomRating = source.CustomRating;
- }
-
- if (replaceData || string.IsNullOrEmpty(target.Tagline))
- {
- target.Tagline = source.Tagline;
- }
-
- if (!lockedFields.Contains(MetadataField.Overview))
- {
- if (replaceData || string.IsNullOrEmpty(target.Overview))
- {
- target.Overview = source.Overview;
- }
- }
-
- if (replaceData || !target.ParentIndexNumber.HasValue)
- {
- target.ParentIndexNumber = source.ParentIndexNumber;
- }
-
- if (!lockedFields.Contains(MetadataField.Cast))
- {
- if (replaceData || targetResult.People == null || targetResult.People.Count == 0)
- {
- targetResult.People = sourceResult.People;
- }
- else if (targetResult.People != null && sourceResult.People != null)
- {
- MergePeople(sourceResult.People, targetResult.People);
- }
- }
-
- if (replaceData || !target.PremiereDate.HasValue)
- {
- target.PremiereDate = source.PremiereDate;
- }
-
- if (replaceData || !target.ProductionYear.HasValue)
- {
- target.ProductionYear = source.ProductionYear;
- }
-
- if (!lockedFields.Contains(MetadataField.Runtime))
- {
- if (replaceData || !target.RunTimeTicks.HasValue)
- {
- if (target is not Audio && target is not Video)
- {
- target.RunTimeTicks = source.RunTimeTicks;
- }
- }
- }
-
- if (!lockedFields.Contains(MetadataField.Studios))
- {
- if (replaceData || target.Studios.Length == 0)
- {
- target.Studios = source.Studios;
- }
- }
-
- if (!lockedFields.Contains(MetadataField.Tags))
- {
- if (replaceData || target.Tags.Length == 0)
- {
- target.Tags = source.Tags;
- }
- }
-
- if (!lockedFields.Contains(MetadataField.ProductionLocations))
- {
- if (replaceData || target.ProductionLocations.Length == 0)
- {
- target.ProductionLocations = source.ProductionLocations;
- }
- }
-
- foreach (var id in source.ProviderIds)
- {
- var key = id.Key;
-
- // Don't replace existing Id's.
- if (replaceData || !target.ProviderIds.ContainsKey(key))
- {
- target.ProviderIds[key] = id.Value;
- }
- }
-
- MergeAlbumArtist(source, target, replaceData);
- MergeCriticRating(source, target, replaceData);
- MergeTrailers(source, target, replaceData);
- MergeVideoInfo(source, target, replaceData);
- MergeDisplayOrder(source, target, replaceData);
-
- if (replaceData || string.IsNullOrEmpty(target.ForcedSortName))
- {
- var forcedSortName = source.ForcedSortName;
-
- if (!string.IsNullOrWhiteSpace(forcedSortName))
- {
- target.ForcedSortName = forcedSortName;
- }
- }
-
- if (mergeMetadataSettings)
- {
- target.LockedFields = source.LockedFields;
- target.IsLocked = source.IsLocked;
-
- // Grab the value if it's there, but if not then don't overwrite the default
- if (source.DateCreated != default)
- {
- target.DateCreated = source.DateCreated;
- }
-
- target.PreferredMetadataCountryCode = source.PreferredMetadataCountryCode;
- target.PreferredMetadataLanguage = source.PreferredMetadataLanguage;
- }
- }
-
- private static void MergePeople(List<PersonInfo> source, List<PersonInfo> target)
- {
- foreach (var person in target)
- {
- var normalizedName = person.Name.RemoveDiacritics();
- var personInSource = source.FirstOrDefault(i => string.Equals(i.Name.RemoveDiacritics(), normalizedName, StringComparison.OrdinalIgnoreCase));
-
- if (personInSource != null)
- {
- foreach (var providerId in personInSource.ProviderIds)
- {
- if (!person.ProviderIds.ContainsKey(providerId.Key))
- {
- person.ProviderIds[providerId.Key] = providerId.Value;
- }
- }
-
- if (string.IsNullOrWhiteSpace(person.ImageUrl))
- {
- person.ImageUrl = personInSource.ImageUrl;
- }
- }
- }
- }
-
- private static void MergeDisplayOrder(BaseItem source, BaseItem target, bool replaceData)
- {
- if (source is IHasDisplayOrder sourceHasDisplayOrder
- && target is IHasDisplayOrder targetHasDisplayOrder)
- {
- if (replaceData || string.IsNullOrEmpty(targetHasDisplayOrder.DisplayOrder))
- {
- var displayOrder = sourceHasDisplayOrder.DisplayOrder;
-
- if (!string.IsNullOrWhiteSpace(displayOrder))
- {
- targetHasDisplayOrder.DisplayOrder = displayOrder;
- }
- }
- }
- }
-
- private static void MergeAlbumArtist(BaseItem source, BaseItem target, bool replaceData)
- {
- if (source is IHasAlbumArtist sourceHasAlbumArtist
- && target is IHasAlbumArtist targetHasAlbumArtist)
- {
- if (replaceData || targetHasAlbumArtist.AlbumArtists.Count == 0)
- {
- targetHasAlbumArtist.AlbumArtists = sourceHasAlbumArtist.AlbumArtists;
- }
- }
- }
-
- private static void MergeCriticRating(BaseItem source, BaseItem target, bool replaceData)
- {
- if (replaceData || !target.CriticRating.HasValue)
- {
- target.CriticRating = source.CriticRating;
- }
- }
-
- private static void MergeTrailers(BaseItem source, BaseItem target, bool replaceData)
- {
- if (replaceData || target.RemoteTrailers.Count == 0)
- {
- target.RemoteTrailers = source.RemoteTrailers;
- }
- }
-
- private static void MergeVideoInfo(BaseItem source, BaseItem target, bool replaceData)
- {
- if (source is Video sourceCast && target is Video targetCast)
- {
- if (replaceData || targetCast.Video3DFormat == null)
- {
- targetCast.Video3DFormat = sourceCast.Video3DFormat;
- }
- }
- }
- }
-}
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index 43cf621cd..049c0bf22 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -22,7 +22,7 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="OptimizedPriorityQueue" Version="5.0.0" />
<PackageReference Include="PlaylistsNET" Version="1.1.3" />
- <PackageReference Include="TMDbLib" Version="1.8.1" />
+ <PackageReference Include="TMDbLib" Version="1.9.1" />
</ItemGroup>
<PropertyGroup>
diff --git a/MediaBrowser.Providers/Movies/MovieMetadataService.cs b/MediaBrowser.Providers/Movies/MovieMetadataService.cs
index c477fb70f..984a3c122 100644
--- a/MediaBrowser.Providers/Movies/MovieMetadataService.cs
+++ b/MediaBrowser.Providers/Movies/MovieMetadataService.cs
@@ -42,7 +42,7 @@ namespace MediaBrowser.Providers.Movies
/// <inheritdoc />
protected override void MergeData(MetadataResult<Movie> source, MetadataResult<Movie> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
{
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+ base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
var sourceItem = source.Item;
var targetItem = target.Item;
diff --git a/MediaBrowser.Providers/Movies/TrailerMetadataService.cs b/MediaBrowser.Providers/Movies/TrailerMetadataService.cs
index f32d9ec0a..ad0c5aaa7 100644
--- a/MediaBrowser.Providers/Movies/TrailerMetadataService.cs
+++ b/MediaBrowser.Providers/Movies/TrailerMetadataService.cs
@@ -42,7 +42,7 @@ namespace MediaBrowser.Providers.Movies
/// <inheritdoc />
protected override void MergeData(MetadataResult<Trailer> source, MetadataResult<Trailer> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
{
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+ base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
if (replaceData || target.Item.TrailerTypes.Length == 0)
{
diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
index 7c5b80e1e..7743d3b27 100644
--- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs
+++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs
@@ -114,7 +114,7 @@ namespace MediaBrowser.Providers.Music
bool replaceData,
bool mergeMetadataSettings)
{
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+ base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
var sourceItem = source.Item;
var targetItem = target.Item;
diff --git a/MediaBrowser.Providers/Music/ArtistMetadataService.cs b/MediaBrowser.Providers/Music/ArtistMetadataService.cs
index e29475dd7..1f342c0db 100644
--- a/MediaBrowser.Providers/Music/ArtistMetadataService.cs
+++ b/MediaBrowser.Providers/Music/ArtistMetadataService.cs
@@ -6,7 +6,6 @@ using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
@@ -39,11 +38,5 @@ namespace MediaBrowser.Providers.Music
})
: item.GetRecursiveChildren(i => i is IHasArtist && !i.IsFolder);
}
-
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<MusicArtist> source, MetadataResult<MusicArtist> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
- {
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- }
}
}
diff --git a/MediaBrowser.Providers/Music/AudioMetadataService.cs b/MediaBrowser.Providers/Music/AudioMetadataService.cs
index 8b9fc8a08..4577f7745 100644
--- a/MediaBrowser.Providers/Music/AudioMetadataService.cs
+++ b/MediaBrowser.Providers/Music/AudioMetadataService.cs
@@ -26,7 +26,7 @@ namespace MediaBrowser.Providers.Music
/// <inheritdoc />
protected override void MergeData(MetadataResult<Audio> source, MetadataResult<Audio> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
{
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+ base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
var sourceItem = source.Item;
var targetItem = target.Item;
diff --git a/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs b/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs
index 1d611a746..b97b76630 100644
--- a/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs
+++ b/MediaBrowser.Providers/Music/MusicVideoMetadataService.cs
@@ -31,7 +31,7 @@ namespace MediaBrowser.Providers.Music
bool replaceData,
bool mergeMetadataSettings)
{
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+ base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
var sourceItem = source.Item;
var targetItem = target.Item;
diff --git a/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs b/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs
index 7dda7e9bf..46eb546c2 100644
--- a/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs
+++ b/MediaBrowser.Providers/MusicGenres/MusicGenreMetadataService.cs
@@ -4,7 +4,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities.Audio;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
@@ -22,11 +21,5 @@ namespace MediaBrowser.Providers.MusicGenres
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
{
}
-
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<MusicGenre> source, MetadataResult<MusicGenre> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
- {
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- }
}
}
diff --git a/MediaBrowser.Providers/People/PersonMetadataService.cs b/MediaBrowser.Providers/People/PersonMetadataService.cs
index fe6d1d4d3..59bf7e4e6 100644
--- a/MediaBrowser.Providers/People/PersonMetadataService.cs
+++ b/MediaBrowser.Providers/People/PersonMetadataService.cs
@@ -4,7 +4,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
@@ -22,11 +21,5 @@ namespace MediaBrowser.Providers.People
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
{
}
-
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<Person> source, MetadataResult<Person> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
- {
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- }
}
}
diff --git a/MediaBrowser.Providers/Photos/PhotoAlbumMetadataService.cs b/MediaBrowser.Providers/Photos/PhotoAlbumMetadataService.cs
index 60ed96452..f2cccb90f 100644
--- a/MediaBrowser.Providers/Photos/PhotoAlbumMetadataService.cs
+++ b/MediaBrowser.Providers/Photos/PhotoAlbumMetadataService.cs
@@ -4,7 +4,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
@@ -22,11 +21,5 @@ namespace MediaBrowser.Providers.Photos
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
{
}
-
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<PhotoAlbum> source, MetadataResult<PhotoAlbum> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
- {
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- }
}
}
diff --git a/MediaBrowser.Providers/Photos/PhotoMetadataService.cs b/MediaBrowser.Providers/Photos/PhotoMetadataService.cs
index cbbb433c0..6941401e0 100644
--- a/MediaBrowser.Providers/Photos/PhotoMetadataService.cs
+++ b/MediaBrowser.Providers/Photos/PhotoMetadataService.cs
@@ -4,7 +4,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
@@ -22,11 +21,5 @@ namespace MediaBrowser.Providers.Photos
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
{
}
-
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<Photo> source, MetadataResult<Photo> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
- {
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- }
}
}
diff --git a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs
index 5262919d5..1bd000a48 100644
--- a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs
+++ b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs
@@ -41,7 +41,7 @@ namespace MediaBrowser.Providers.Playlists
/// <inheritdoc />
protected override void MergeData(MetadataResult<Playlist> source, MetadataResult<Playlist> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
{
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+ base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
var sourceItem = source.Item;
var targetItem = target.Item;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs
index dec796148..03aaf380b 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/PluginConfiguration.cs
@@ -23,6 +23,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
public bool ExcludeTagsMovies { get; set; }
/// <summary>
+ /// Gets or sets a value indicating whether season name should be imported from TMDb.
+ /// </summary>
+ public bool ImportSeasonName { get; set; }
+
+ /// <summary>
/// Gets or sets a value indicating the maximum number of cast members to fetch for an item.
/// </summary>
public int MaxCastMembers { get; set; } = 15;
@@ -38,6 +43,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
public string? BackdropSize { get; set; }
/// <summary>
+ /// Gets or sets a value indicating the logo image size to fetch.
+ /// </summary>
+ public string? LogoSize { get; set; }
+
+ /// <summary>
/// Gets or sets a value indicating the profile image size to fetch.
/// </summary>
public string? ProfileSize { get; set; }
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
index 52693795b..48ec0535c 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Configuration/config.html
@@ -20,6 +20,10 @@
<input is="emby-checkbox" type="checkbox" id="excludeTagsMovies" />
<span>Exclude tags/keywords from metadata fetched for movies.</span>
</label>
+ <label class="checkboxContainer">
+ <input is="emby-checkbox" type="checkbox" id="importSeasonName" />
+ <span>Import season name from metadata fetched for series.</span>
+ </label>
<div class="inputContainer">
<input is="emby-input" type="number" id="maxCastMembers" pattern="[0-9]*" required min="0" max="1000" label="Max Cast Members" />
<div class="fieldDescription">The maximum number of cast members to fetch for an item.</div>
@@ -33,6 +37,9 @@
<select is="emby-select" id="selectBackdropSize" label="Backdrop"></select>
</div>
<div class="selectContainer">
+ <select is="emby-select" id="selectLogoSize" label="Logo"></select>
+ </div>
+ <div class="selectContainer">
<select is="emby-select" id="selectProfileSize" label="Profile"></select>
</div>
<div class="selectContainer">
@@ -72,6 +79,10 @@
selBackdropSize.innerHTML = clientConfig.BackdropSizes.map(sizeOptionsGenerator);
selBackdropSize.value = pluginConfig.BackdropSize;
+ var selLogoSize = document.querySelector('#selectLogoSize');
+ selLogoSize.innerHTML = clientConfig.LogoSizes.map(sizeOptionsGenerator);
+ selLogoSize.value = pluginConfig.LogoSize;
+
var selProfileSize = document.querySelector('#selectProfileSize');
selProfileSize.innerHTML = clientConfig.ProfileSizes.map(sizeOptionsGenerator);
selProfileSize.value = pluginConfig.ProfileSize;
@@ -98,6 +109,7 @@
document.querySelector('#includeAdult').checked = config.IncludeAdult;
document.querySelector('#excludeTagsSeries').checked = config.ExcludeTagsSeries;
document.querySelector('#excludeTagsMovies').checked = config.ExcludeTagsMovies;
+ document.querySelector('#importSeasonName').checked = config.ImportSeasonName;
var maxCastMembers = document.querySelector('#maxCastMembers');
maxCastMembers.value = config.MaxCastMembers;
@@ -120,9 +132,11 @@
config.IncludeAdult = document.querySelector('#includeAdult').checked;
config.ExcludeTagsSeries = document.querySelector('#excludeTagsSeries').checked;
config.ExcludeTagsMovies = document.querySelector('#excludeTagsMovies').checked;
+ config.ImportSeasonName = document.querySelector('#importSeasonName').checked;
config.MaxCastMembers = document.querySelector('#maxCastMembers').value;
config.PosterSize = document.querySelector('#selectPosterSize').value;
config.BackdropSize = document.querySelector('#selectBackdropSize').value;
+ config.LogoSize = document.querySelector('#selectLogoSize').value;
config.ProfileSize = document.querySelector('#selectProfileSize').value;
config.StillSize = document.querySelector('#selectStillSize').value;
ApiClient.updatePluginConfiguration(PluginConfig.pluginId, config).then(Dashboard.processPluginConfigurationUpdateResult);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
index f71f7bd10..16f0089f8 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieImageProvider.cs
@@ -44,7 +44,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
return new List<ImageType>
{
ImageType.Primary,
- ImageType.Backdrop
+ ImageType.Backdrop,
+ ImageType.Logo
};
}
@@ -85,10 +86,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
var posters = movie.Images.Posters;
var backdrops = movie.Images.Backdrops;
- var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count);
+ var logos = movie.Images.Logos;
+ var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count + logos.Count);
_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language, remoteImages);
_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language, remoteImages);
+ _tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language, remoteImages);
return remoteImages;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
index e4a56fde9..f14f31858 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
@@ -312,7 +312,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
var trailers = new List<MediaUrl>();
for (var i = 0; i < movieResult.Videos.Results.Count; i++)
{
- var video = movieResult.Videos.Results[0];
+ var video = movieResult.Videos.Results[i];
if (!TmdbUtils.IsTrailerType(video))
{
continue;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
index 27c52a5a2..64ed3f408 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
@@ -59,6 +59,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
Overview = seasonResult.Overview
};
+ if (Plugin.Instance.Configuration.ImportSeasonName)
+ {
+ result.Item.Name = seasonResult.Name;
+ }
+
if (!string.IsNullOrEmpty(seasonResult.ExternalIds?.TvdbId))
{
result.Item.SetProviderId(MetadataProvider.Tvdb, seasonResult.ExternalIds.TvdbId);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
index 5ef3736c4..130d6ce44 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesImageProvider.cs
@@ -42,7 +42,8 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
return new List<ImageType>
{
ImageType.Primary,
- ImageType.Backdrop
+ ImageType.Backdrop,
+ ImageType.Logo
};
}
@@ -69,10 +70,12 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var posters = series.Images.Posters;
var backdrops = series.Images.Backdrops;
- var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count);
+ var logos = series.Images.Logos;
+ var remoteImages = new List<RemoteImageInfo>(posters.Count + backdrops.Count + logos.Count);
_tmdbClientManager.ConvertPostersToRemoteImageInfo(posters, language, remoteImages);
_tmdbClientManager.ConvertBackdropsToRemoteImageInfo(backdrops, language, remoteImages);
+ _tmdbClientManager.ConvertLogosToRemoteImageInfo(logos, language, remoteImages);
return remoteImages;
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
index 28d6f4d0c..d78652834 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbClientManager.cs
@@ -544,6 +544,17 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
}
/// <summary>
+ /// Converts logo <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
+ /// </summary>
+ /// <param name="images">The input images.</param>
+ /// <param name="requestLanguage">The requested language.</param>
+ /// <param name="results">The collection to add the remote images into.</param>
+ public void ConvertLogosToRemoteImageInfo(List<ImageData> images, string requestLanguage, List<RemoteImageInfo> results)
+ {
+ ConvertToRemoteImageInfo(images, Plugin.Instance.Configuration.LogoSize, ImageType.Logo, requestLanguage, results);
+ }
+
+ /// <summary>
/// Converts profile <see cref="ImageData"/>s into <see cref="RemoteImageInfo"/>s.
/// </summary>
/// <param name="images">The input images.</param>
@@ -622,6 +633,11 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
pluginConfig.BackdropSize = imageConfig.BackdropSizes[^1];
}
+ if (!imageConfig.LogoSizes.Contains(pluginConfig.LogoSize))
+ {
+ pluginConfig.LogoSize = imageConfig.LogoSizes[^1];
+ }
+
if (!imageConfig.ProfileSizes.Contains(pluginConfig.ProfileSize))
{
pluginConfig.ProfileSize = imageConfig.ProfileSizes[^1];
diff --git a/MediaBrowser.Providers/Studios/StudioMetadataService.cs b/MediaBrowser.Providers/Studios/StudioMetadataService.cs
index 091b33ce0..df938325f 100644
--- a/MediaBrowser.Providers/Studios/StudioMetadataService.cs
+++ b/MediaBrowser.Providers/Studios/StudioMetadataService.cs
@@ -4,7 +4,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
@@ -22,11 +21,5 @@ namespace MediaBrowser.Providers.Studios
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
{
}
-
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<Studio> source, MetadataResult<Studio> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
- {
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- }
}
}
diff --git a/MediaBrowser.Providers/TV/EpisodeMetadataService.cs b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs
index 08cb6ced9..d8855ec93 100644
--- a/MediaBrowser.Providers/TV/EpisodeMetadataService.cs
+++ b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs
@@ -70,7 +70,7 @@ namespace MediaBrowser.Providers.TV
/// <inheritdoc />
protected override void MergeData(MetadataResult<Episode> source, MetadataResult<Episode> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
{
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+ base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
var sourceItem = source.Item;
var targetItem = target.Item;
diff --git a/MediaBrowser.Providers/TV/SeasonMetadataService.cs b/MediaBrowser.Providers/TV/SeasonMetadataService.cs
index 1f06cbdb2..54dcee41e 100644
--- a/MediaBrowser.Providers/TV/SeasonMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeasonMetadataService.cs
@@ -87,12 +87,6 @@ namespace MediaBrowser.Providers.TV
return updateType;
}
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<Season> source, MetadataResult<Season> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
- {
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- }
-
private ItemUpdateType SaveIsVirtualItem(Season item, IList<BaseItem> episodes)
{
var isVirtualItem = item.LocationType == LocationType.Virtual && (episodes.Count == 0 || episodes.All(i => i.LocationType == LocationType.Virtual));
diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
index 770dc3e00..f49492f33 100644
--- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs
+++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs
@@ -63,7 +63,7 @@ namespace MediaBrowser.Providers.TV
/// <inheritdoc />
protected override void MergeData(MetadataResult<Series> source, MetadataResult<Series> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
{
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
+ base.MergeData(source, target, lockedFields, replaceData, mergeMetadataSettings);
var sourceItem = source.Item;
var targetItem = target.Item;
diff --git a/MediaBrowser.Providers/Videos/VideoMetadataService.cs b/MediaBrowser.Providers/Videos/VideoMetadataService.cs
index 31c7eaac4..caa6d6e1f 100644
--- a/MediaBrowser.Providers/Videos/VideoMetadataService.cs
+++ b/MediaBrowser.Providers/Videos/VideoMetadataService.cs
@@ -4,7 +4,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
@@ -26,11 +25,5 @@ namespace MediaBrowser.Providers.Videos
/// <inheritdoc />
// Make sure the type-specific services get picked first
public override int Order => 10;
-
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<Video> source, MetadataResult<Video> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
- {
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- }
}
}
diff --git a/MediaBrowser.Providers/Years/YearMetadataService.cs b/MediaBrowser.Providers/Years/YearMetadataService.cs
index 6151d12e9..689e8661b 100644
--- a/MediaBrowser.Providers/Years/YearMetadataService.cs
+++ b/MediaBrowser.Providers/Years/YearMetadataService.cs
@@ -4,7 +4,6 @@ using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
-using MediaBrowser.Model.Entities;
using MediaBrowser.Model.IO;
using MediaBrowser.Providers.Manager;
using Microsoft.Extensions.Logging;
@@ -22,11 +21,5 @@ namespace MediaBrowser.Providers.Years
: base(serverConfigurationManager, logger, providerManager, fileSystem, libraryManager)
{
}
-
- /// <inheritdoc />
- protected override void MergeData(MetadataResult<Year> source, MetadataResult<Year> target, MetadataField[] lockedFields, bool replaceData, bool mergeMetadataSettings)
- {
- ProviderUtils.MergeBaseItemData(source, target, lockedFields, replaceData, mergeMetadataSettings);
- }
}
}
diff --git a/README.md b/README.md
index 6653bff11..7f6daca68 100644
--- a/README.md
+++ b/README.md
@@ -85,7 +85,7 @@ These instructions will help you get set up with a local development environment
Before the project can be built, you must first install the [.NET 6.0 SDK](https://dotnet.microsoft.com/download/dotnet) on your system.
-Instructions to run this project from the command line are included here, but you will also need to install an IDE if you want to debug the server while it is running. Any IDE that supports .NET Core development will work, but two options are recent versions of [Visual Studio](https://visualstudio.microsoft.com/downloads/) (at least 2017) and [Visual Studio Code](https://code.visualstudio.com/Download).
+Instructions to run this project from the command line are included here, but you will also need to install an IDE if you want to debug the server while it is running. Any IDE that supports .NET 6 development will work, but two options are recent versions of [Visual Studio](https://visualstudio.microsoft.com/downloads/) (at least 2022) and [Visual Studio Code](https://code.visualstudio.com/Download).
[ffmpeg](https://github.com/jellyfin/jellyfin-ffmpeg) will also need to be installed.
@@ -138,10 +138,10 @@ A second option is to build the project and then run the resulting executable fi
1. Build the project
- ```bash
- dotnet build # Build the project
- cd bin/Debug/net5.0 # Change into the build output directory
- ```
+```bash
+dotnet build # Build the project
+cd Jellyfin.Server/bin/Debug/net6.0 # Change into the build output directory
+```
2. Execute the build output. On Linux, Mac, etc. use `./jellyfin` and on Windows use `jellyfin.exe`.
diff --git a/jellyfin.ruleset b/jellyfin.ruleset
index 52bedabee..dea1a748b 100644
--- a/jellyfin.ruleset
+++ b/jellyfin.ruleset
@@ -13,6 +13,8 @@
<Rule Id="SA1210" Action="Error" />
<!-- error on SA1316: Tuple element names should use correct casing -->
<Rule Id="SA1316" Action="Error" />
+ <!-- error on SA1414: Tuple types in signatures should have element names -->
+ <Rule Id="SA1414" Action="Error" />
<!-- error on SA1518: File is required to end with a single newline character -->
<Rule Id="SA1518" Action="Error" />
<!-- error on SA1629: Documentation text should end with a period -->
@@ -73,6 +75,8 @@
<Rule Id="CA1843" Action="Error" />
<!-- error on CA1845: Use span-based 'string.Concat' -->
<Rule Id="CA1845" Action="Error" />
+ <!-- error on CA1849: Call async methods when in an async method -->
+ <Rule Id="CA1849" Action="Error" />
<!-- error on CA2016: Forward the CancellationToken parameter to methods that take one
or pass in 'CancellationToken.None' explicitly to indicate intentionally not propagating the token -->
<Rule Id="CA2016" Action="Error" />
diff --git a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
index 8476c935e..aaa6b5d90 100644
--- a/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
+++ b/tests/Jellyfin.Common.Tests/Jellyfin.Common.Tests.csproj
@@ -16,7 +16,7 @@
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="coverlet.collector" Version="3.1.0" />
- <PackageReference Include="FsCheck.Xunit" Version="2.16.3" />
+ <PackageReference Include="FsCheck.Xunit" Version="2.16.4" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
index 3dcc00ff0..2a3918469 100644
--- a/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
+++ b/tests/Jellyfin.Extensions.Tests/Jellyfin.Extensions.Tests.csproj
@@ -17,7 +17,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
- <PackageReference Include="FsCheck.Xunit" Version="2.16.3" />
+ <PackageReference Include="FsCheck.Xunit" Version="2.16.4" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs b/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs
new file mode 100644
index 000000000..7c3a7ff6c
--- /dev/null
+++ b/tests/Jellyfin.Model.Tests/Drawing/ImageFormatExtensionsTests.cs
@@ -0,0 +1,33 @@
+using System;
+using System.ComponentModel;
+using MediaBrowser.Model.Drawing;
+using Xunit;
+
+namespace Jellyfin.Model.Drawing;
+
+public static class ImageFormatExtensionsTests
+{
+ private static TheoryData<ImageFormat> GetAllImageFormats()
+ {
+ var theoryTypes = new TheoryData<ImageFormat>();
+ foreach (var x in Enum.GetValues<ImageFormat>())
+ {
+ theoryTypes.Add(x);
+ }
+
+ return theoryTypes;
+ }
+
+ [Theory]
+ [MemberData(nameof(GetAllImageFormats))]
+ public static void GetMimeType_Valid_Valid(ImageFormat format)
+ => Assert.Null(Record.Exception(() => format.GetMimeType()));
+
+ [Theory]
+ [InlineData((ImageFormat)int.MinValue)]
+ [InlineData((ImageFormat)int.MaxValue)]
+ [InlineData((ImageFormat)(-1))]
+ [InlineData((ImageFormat)5)]
+ public static void GetMimeType_Valid_ThrowsInvalidEnumArgumentException(ImageFormat format)
+ => Assert.Throws<InvalidEnumArgumentException>(() => format.GetMimeType());
+}
diff --git a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
index d4a1a30c3..3b6259abd 100644
--- a/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
+++ b/tests/Jellyfin.Model.Tests/Jellyfin.Model.Tests.csproj
@@ -11,7 +11,7 @@
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="3.1.0" />
- <PackageReference Include="FsCheck.Xunit" Version="2.16.3" />
+ <PackageReference Include="FsCheck.Xunit" Version="2.16.4" />
</ItemGroup>
<!-- Code Analyzers -->
diff --git a/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs b/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs
index 7b50c54b0..cbab455f0 100644
--- a/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs
+++ b/tests/Jellyfin.Model.Tests/Net/MimeTypesTests.cs
@@ -124,7 +124,6 @@ namespace Jellyfin.Model.Tests.Net
[InlineData("font/woff2", ".woff2")]
[InlineData("image/bmp", ".bmp")]
[InlineData("image/gif", ".gif")]
- [InlineData("image/jpg", ".jpg")]
[InlineData("image/jpeg", ".jpg")]
[InlineData("image/png", ".png")]
[InlineData("image/svg+xml", ".svg")]
diff --git a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
index 87acc7f68..7f9b60b9e 100644
--- a/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
+++ b/tests/Jellyfin.Networking.Tests/Jellyfin.Networking.Tests.csproj
@@ -16,7 +16,7 @@
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.1" />
<PackageReference Include="coverlet.collector" Version="3.1.0" />
- <PackageReference Include="FsCheck.Xunit" Version="2.16.3" />
+ <PackageReference Include="FsCheck.Xunit" Version="2.16.4" />
<PackageReference Include="Moq" Version="4.16.1" />
</ItemGroup>
diff --git a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
index 2ba5c47d7..c0931dbcf 100644
--- a/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
+++ b/tests/Jellyfin.Providers.Tests/Manager/ItemImageProviderTests.cs
@@ -41,10 +41,7 @@ namespace Jellyfin.Providers.Tests.Manager
[Fact]
public void ValidateImages_EmptyItemEmptyProviders_NoChange()
{
- var itemImageProvider = GetItemImageProvider(null, null);
- var changed = itemImageProvider.ValidateImages(new Video(), Enumerable.Empty<ILocalImageProvider>(), null);
-
- Assert.False(changed);
+ ValidateImages_Test(ImageType.Primary, 0, true, 0, false, 0);
}
private static TheoryData<ImageType, int> GetImageTypesWithCount()
@@ -53,7 +50,6 @@ namespace Jellyfin.Providers.Tests.Manager
{
// minimal test cases that hit different handling
{ ImageType.Primary, 1 },
- { ImageType.Backdrop, 1 },
{ ImageType.Backdrop, 2 }
};
@@ -64,43 +60,34 @@ namespace Jellyfin.Providers.Tests.Manager
[MemberData(nameof(GetImageTypesWithCount))]
public void ValidateImages_EmptyItemAndPopulatedProviders_AddsImages(ImageType imageType, int imageCount)
{
- // Has to exist for querying DateModified time on file, results stored but not checked so not populating
- BaseItem.FileSystem = Mock.Of<IFileSystem>();
-
- var item = new Video();
- var imageProvider = GetImageProvider(imageType, imageCount, true);
-
- var itemImageProvider = GetItemImageProvider(null, null);
- var changed = itemImageProvider.ValidateImages(item, new[] { imageProvider }, null);
-
- Assert.True(changed);
- Assert.Equal(imageCount, item.GetImages(imageType).Count());
+ ValidateImages_Test(imageType, 0, true, imageCount, true, imageCount);
}
[Theory]
[MemberData(nameof(GetImageTypesWithCount))]
public void ValidateImages_PopulatedItemWithGoodPathsAndEmptyProviders_NoChange(ImageType imageType, int imageCount)
{
- var item = GetItemWithImages(imageType, imageCount, true);
-
- var itemImageProvider = GetItemImageProvider(null, null);
- var changed = itemImageProvider.ValidateImages(item, Enumerable.Empty<ILocalImageProvider>(), null);
-
- Assert.False(changed);
- Assert.Equal(imageCount, item.GetImages(imageType).Count());
+ ValidateImages_Test(imageType, imageCount, true, 0, false, imageCount);
}
[Theory]
[MemberData(nameof(GetImageTypesWithCount))]
public void ValidateImages_PopulatedItemWithBadPathsAndEmptyProviders_RemovesImage(ImageType imageType, int imageCount)
{
- var item = GetItemWithImages(imageType, imageCount, false);
+ ValidateImages_Test(imageType, imageCount, false, 0, true, 0);
+ }
+
+ private void ValidateImages_Test(ImageType imageType, int initialImageCount, bool initialPathsValid, int providerImageCount, bool expectedChange, int expectedImageCount)
+ {
+ var item = GetItemWithImages(imageType, initialImageCount, initialPathsValid);
+
+ var imageProvider = GetImageProvider(imageType, providerImageCount, true);
var itemImageProvider = GetItemImageProvider(null, null);
- var changed = itemImageProvider.ValidateImages(item, Enumerable.Empty<ILocalImageProvider>(), null);
+ var actualChange = itemImageProvider.ValidateImages(item, new[] { imageProvider }, null);
- Assert.True(changed);
- Assert.Empty(item.GetImages(imageType));
+ Assert.Equal(expectedChange, actualChange);
+ Assert.Equal(expectedImageCount, item.GetImages(imageType).Count());
}
[Fact]
@@ -137,20 +124,23 @@ namespace Jellyfin.Providers.Tests.Manager
}
[Theory]
- [MemberData(nameof(GetImageTypesWithCount))]
- public void MergeImages_PopulatedItemWithGoodPathsAndSameNewImages_NoChange(ImageType imageType, int imageCount)
+ [InlineData(ImageType.Primary, 1, false)]
+ [InlineData(ImageType.Backdrop, 2, false)]
+ [InlineData(ImageType.Primary, 1, true)]
+ [InlineData(ImageType.Backdrop, 2, true)]
+ public void MergeImages_PopulatedItemWithGoodPathsAndSameNewImages_ResetIfTimeChanges(ImageType imageType, int imageCount, bool updateTime)
{
var oldTime = new DateTime(1970, 1, 1);
+ var updatedTime = updateTime ? new DateTime(2021, 1, 1) : oldTime;
- // match update time with time added to item images (unix epoch)
var fileSystem = new Mock<IFileSystem>();
fileSystem.Setup(fs => fs.GetLastWriteTimeUtc(It.IsAny<FileSystemMetadata>()))
- .Returns(oldTime);
+ .Returns(updatedTime);
BaseItem.FileSystem = fileSystem.Object;
// all valid paths - matching for strictly updating
var item = GetItemWithImages(imageType, imageCount, true);
- // set size to non-zero to allow for updates to occur
+ // set size to non-zero to allow for image size reset to occur
foreach (var image in item.GetImages(imageType))
{
image.DateModified = oldTime;
@@ -163,45 +153,52 @@ namespace Jellyfin.Providers.Tests.Manager
var itemImageProvider = GetItemImageProvider(null, fileSystem);
var changed = itemImageProvider.MergeImages(item, images);
- Assert.False(changed);
+ if (updateTime)
+ {
+ Assert.True(changed);
+ // before and after paths are the same, verify updated by size reset to 0
+ var typedImages = item.GetImages(imageType).ToArray();
+ Assert.Equal(imageCount, typedImages.Length);
+ foreach (var image in typedImages)
+ {
+ Assert.Equal(updatedTime, image.DateModified);
+ Assert.Equal(0, image.Height);
+ Assert.Equal(0, image.Width);
+ }
+ }
+ else
+ {
+ Assert.False(changed);
+ }
}
[Theory]
- [MemberData(nameof(GetImageTypesWithCount))]
- public void MergeImages_PopulatedItemWithGoodPathsAndSameNewImagesWithNewTimestamps_ResetsImageSizes(ImageType imageType, int imageCount)
+ [InlineData(ImageType.Primary, 0)]
+ [InlineData(ImageType.Primary, 1)]
+ [InlineData(ImageType.Backdrop, 2)]
+ public void RemoveImages_DeletesImages_WhenFound(ImageType imageType, int imageCount)
{
- var oldTime = new DateTime(1970, 1, 1);
- var updatedTime = new DateTime(2021, 1, 1);
-
- var fileSystem = new Mock<IFileSystem>();
- fileSystem.Setup(fs => fs.GetLastWriteTimeUtc(It.IsAny<FileSystemMetadata>()))
- .Returns(updatedTime);
- BaseItem.FileSystem = fileSystem.Object;
+ var item = GetItemWithImages(imageType, imageCount, false);
- // all valid paths - matching for strictly updating
- var item = GetItemWithImages(imageType, imageCount, true);
- // set size to non-zero to allow for image size reset to occur
- foreach (var image in item.GetImages(imageType))
+ var mockFileSystem = new Mock<IFileSystem>(MockBehavior.Strict);
+ if (imageCount > 0)
{
- image.DateModified = oldTime;
- image.Height = 1;
- image.Width = 1;
+ mockFileSystem.Setup(fs => fs.DeleteFile("invalid path 0"))
+ .Verifiable();
}
- var images = GetImages(imageType, imageCount, true);
-
- var itemImageProvider = GetItemImageProvider(null, fileSystem);
- var changed = itemImageProvider.MergeImages(item, images);
-
- Assert.True(changed);
- // before and after paths are the same, verify updated by size reset to 0
- Assert.Equal(imageCount, item.GetImages(imageType).Count());
- foreach (var image in item.GetImages(imageType))
+ if (imageCount > 1)
{
- Assert.Equal(updatedTime, image.DateModified);
- Assert.Equal(0, image.Height);
- Assert.Equal(0, image.Width);
+ mockFileSystem.Setup(fs => fs.DeleteFile("invalid path 1"))
+ .Verifiable();
}
+
+ var itemImageProvider = GetItemImageProvider(Mock.Of<IProviderManager>(), mockFileSystem);
+ var result = itemImageProvider.RemoveImages(item);
+
+ Assert.Equal(imageCount != 0, result);
+ Assert.Empty(item.GetImages(imageType));
+ mockFileSystem.Verify();
}
[Theory]
@@ -336,8 +333,7 @@ namespace Jellyfin.Providers.Tests.Manager
remoteInfo[i] = new RemoteImageInfo
{
Type = imageType,
- Url = "image url " + i,
- Width = 1 // min width is set to 0, this will always pass
+ Url = "image url " + i
};
}
@@ -403,11 +399,10 @@ namespace Jellyfin.Providers.Tests.Manager
var remoteInfo = new RemoteImageInfo[targetImageCount];
for (int i = 0; i < targetImageCount; i++)
{
- remoteInfo[i] = new RemoteImageInfo()
+ remoteInfo[i] = new RemoteImageInfo
{
Type = imageType,
- Url = "image url " + i,
- Width = 1 // min width is set to 0, this will always pass
+ Url = "image url " + i
};
}
@@ -449,11 +444,10 @@ namespace Jellyfin.Providers.Tests.Manager
var remoteInfo = new RemoteImageInfo[remoteInfoCount];
for (int i = 0; i < remoteInfoCount; i++)
{
- remoteInfo[i] = new RemoteImageInfo()
+ remoteInfo[i] = new RemoteImageInfo
{
Type = imageType,
- Url = "image url " + i,
- Width = 1 // min width is set to 0, this will always pass
+ Url = "image url " + i
};
}
@@ -500,6 +494,62 @@ namespace Jellyfin.Providers.Tests.Manager
Assert.Equal(imageCount, item.GetImages(imageType).Count());
}
+ [Theory]
+ [InlineData(9, false)]
+ [InlineData(10, true)]
+ [InlineData(null, true)]
+ public async void RefreshImages_ProviderRemote_FiltersByWidth(int? remoteImageWidth, bool expectedToUpdate)
+ {
+ var imageType = ImageType.Primary;
+
+ var item = new Video();
+
+ var libraryOptions = new LibraryOptions
+ {
+ TypeOptions = new[]
+ {
+ new TypeOptions
+ {
+ Type = item.GetType().Name,
+ ImageOptions = new[]
+ {
+ new ImageOption
+ {
+ Type = imageType,
+ MinWidth = 10
+ }
+ }
+ }
+ }
+ };
+
+ var remoteProvider = new Mock<IRemoteImageProvider>(MockBehavior.Strict);
+ remoteProvider.Setup(rp => rp.Name).Returns("MockRemoteProvider");
+ remoteProvider.Setup(rp => rp.GetSupportedImages(item))
+ .Returns(new[] { imageType });
+
+ var refreshOptions = new ImageRefreshOptions(Mock.Of<IDirectoryService>());
+
+ // set width on image from remote
+ var remoteInfo = new[]
+ {
+ new RemoteImageInfo()
+ {
+ Type = imageType,
+ Url = "image url",
+ Width = remoteImageWidth
+ }
+ };
+
+ var providerManager = new Mock<IProviderManager>(MockBehavior.Strict);
+ providerManager.Setup(pm => pm.GetAvailableRemoteImages(It.IsAny<BaseItem>(), It.IsAny<RemoteImageQuery>(), It.IsAny<CancellationToken>()))
+ .ReturnsAsync(remoteInfo);
+ var itemImageProvider = GetItemImageProvider(providerManager.Object, null);
+ var result = await itemImageProvider.RefreshImages(item, libraryOptions, new List<IImageProvider> { remoteProvider.Object }, refreshOptions, CancellationToken.None);
+
+ Assert.Equal(expectedToUpdate, result.UpdateType.HasFlag(ItemUpdateType.ImageUpdate));
+ }
+
private static ItemImageProvider GetItemImageProvider(IProviderManager? providerManager, Mock<IFileSystem>? mockFileSystem)
{
// strict to ensure this isn't accidentally used where a prepared mock is intended
@@ -586,7 +636,6 @@ namespace Jellyfin.Providers.Tests.Manager
{
Type = type,
Limit = count,
- MinWidth = 0
}
}
}
diff --git a/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs b/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs
new file mode 100644
index 000000000..b74b331b7
--- /dev/null
+++ b/tests/Jellyfin.Providers.Tests/Manager/MetadataServiceTests.cs
@@ -0,0 +1,378 @@
+using System;
+using System.Collections.Generic;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Entities.Movies;
+using MediaBrowser.Controller.Entities.TV;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.Entities;
+using MediaBrowser.Providers.Manager;
+using Xunit;
+
+namespace Jellyfin.Providers.Tests.Manager
+{
+ public class MetadataServiceTests
+ {
+ [Theory]
+ [InlineData(false, false)]
+ [InlineData(true, false)]
+ [InlineData(true, true)]
+ public void MergeBaseItemData_MergeMetadataSettings_MergesWhenSet(bool mergeMetadataSettings, bool defaultDate)
+ {
+ var newLocked = new[] { MetadataField.Cast };
+ var newString = "new";
+ var newDate = DateTime.Now;
+
+ var oldLocked = new[] { MetadataField.Genres };
+ var oldString = "old";
+ var oldDate = DateTime.UnixEpoch;
+
+ var source = new MetadataResult<Movie>
+ {
+ Item = new Movie
+ {
+ LockedFields = newLocked,
+ IsLocked = true,
+ PreferredMetadataCountryCode = newString,
+ PreferredMetadataLanguage = newString,
+ DateCreated = newDate
+ }
+ };
+ if (defaultDate)
+ {
+ source.Item.DateCreated = default;
+ }
+
+ var target = new MetadataResult<Movie>
+ {
+ Item = new Movie
+ {
+ LockedFields = oldLocked,
+ IsLocked = false,
+ PreferredMetadataCountryCode = oldString,
+ PreferredMetadataLanguage = oldString,
+ DateCreated = oldDate
+ }
+ };
+
+ MetadataService<Movie, MovieInfo>.MergeBaseItemData(source, target, Array.Empty<MetadataField>(), true, mergeMetadataSettings);
+
+ if (mergeMetadataSettings)
+ {
+ Assert.Equal(newLocked, target.Item.LockedFields);
+ Assert.True(target.Item.IsLocked);
+ Assert.Equal(newString, target.Item.PreferredMetadataCountryCode);
+ Assert.Equal(newString, target.Item.PreferredMetadataLanguage);
+ Assert.Equal(defaultDate ? oldDate : newDate, target.Item.DateCreated);
+ }
+ else
+ {
+ Assert.Equal(oldLocked, target.Item.LockedFields);
+ Assert.False(target.Item.IsLocked);
+ Assert.Equal(oldString, target.Item.PreferredMetadataCountryCode);
+ Assert.Equal(oldString, target.Item.PreferredMetadataLanguage);
+ Assert.Equal(oldDate, target.Item.DateCreated);
+ }
+ }
+
+ [Theory]
+ [InlineData("Name", MetadataField.Name, false)]
+ [InlineData("OriginalTitle", null, false)]
+ [InlineData("OfficialRating", MetadataField.OfficialRating)]
+ [InlineData("CustomRating")]
+ [InlineData("Tagline")]
+ [InlineData("Overview", MetadataField.Overview)]
+ [InlineData("DisplayOrder", null, false)]
+ [InlineData("ForcedSortName", null, false)]
+ public void MergeBaseItemData_StringField_ReplacesAppropriately(string propName, MetadataField? lockField = null, bool replacesWithEmpty = true)
+ {
+ var oldValue = "Old";
+ var newValue = "New";
+
+ // Use type Series to hit DisplayOrder
+ Assert.False(TestMergeBaseItemData<Series, SeriesInfo>(propName, oldValue, newValue, null, false, out _));
+ if (lockField != null)
+ {
+ Assert.False(TestMergeBaseItemData<Series, SeriesInfo>(propName, oldValue, newValue, lockField, true, out _));
+ Assert.False(TestMergeBaseItemData<Series, SeriesInfo>(propName, null, newValue, lockField, false, out _));
+ Assert.False(TestMergeBaseItemData<Series, SeriesInfo>(propName, string.Empty, newValue, lockField, false, out _));
+ }
+
+ Assert.True(TestMergeBaseItemData<Series, SeriesInfo>(propName, oldValue, newValue, null, true, out _));
+ Assert.True(TestMergeBaseItemData<Series, SeriesInfo>(propName, null, newValue, null, false, out _));
+ Assert.True(TestMergeBaseItemData<Series, SeriesInfo>(propName, string.Empty, newValue, null, false, out _));
+
+ var replacedWithEmpty = TestMergeBaseItemData<Series, SeriesInfo>(propName, oldValue, string.Empty, null, true, out _);
+ Assert.Equal(replacesWithEmpty, replacedWithEmpty);
+ }
+
+ [Theory]
+ [InlineData("Genres", MetadataField.Genres)]
+ [InlineData("Studios", MetadataField.Studios)]
+ [InlineData("Tags", MetadataField.Tags)]
+ [InlineData("ProductionLocations", MetadataField.ProductionLocations)]
+ [InlineData("AlbumArtists")]
+ public void MergeBaseItemData_StringArrayField_ReplacesAppropriately(string propName, MetadataField? lockField = null)
+ {
+ // Note that arrays are replaced, not merged
+ var oldValue = new[] { "Old" };
+ var newValue = new[] { "New" };
+
+ // Use type Audio to hit AlbumArtists
+ Assert.False(TestMergeBaseItemData<Audio, SongInfo>(propName, oldValue, newValue, null, false, out _));
+ if (lockField != null)
+ {
+ Assert.False(TestMergeBaseItemData<Audio, SongInfo>(propName, oldValue, newValue, lockField, true, out _));
+ Assert.False(TestMergeBaseItemData<Audio, SongInfo>(propName, Array.Empty<string>(), newValue, lockField, false, out _));
+ }
+
+ Assert.True(TestMergeBaseItemData<Audio, SongInfo>(propName, oldValue, newValue, null, true, out _));
+ Assert.True(TestMergeBaseItemData<Audio, SongInfo>(propName, Array.Empty<string>(), newValue, null, false, out _));
+
+ Assert.True(TestMergeBaseItemData<Audio, SongInfo>(propName, oldValue, Array.Empty<string>(), null, true, out _));
+ }
+
+ private static TheoryData<string, object, object> MergeBaseItemData_SimpleField_ReplacesAppropriately_TestData()
+ => new()
+ {
+ { "IndexNumber", 1, 2 },
+ { "ParentIndexNumber", 1, 2 },
+ { "ProductionYear", 1, 2 },
+ { "CommunityRating", 1.0f, 2.0f },
+ { "CriticRating", 1.0f, 2.0f },
+ { "EndDate", DateTime.UnixEpoch, DateTime.Now },
+ { "PremiereDate", DateTime.UnixEpoch, DateTime.Now },
+ { "Video3DFormat", Video3DFormat.HalfSideBySide, Video3DFormat.FullSideBySide }
+ };
+
+ [Theory]
+ [MemberData(nameof(MergeBaseItemData_SimpleField_ReplacesAppropriately_TestData))]
+ public void MergeBaseItemData_SimpleField_ReplacesAppropriately(string propName, object oldValue, object newValue)
+ {
+ // Use type Movie to allow testing of Video3DFormat
+ Assert.False(TestMergeBaseItemData<Movie, MovieInfo>(propName, oldValue, newValue, null, false, out _));
+
+ Assert.True(TestMergeBaseItemData<Movie, MovieInfo>(propName, oldValue, newValue, null, true, out _));
+ Assert.True(TestMergeBaseItemData<Movie, MovieInfo>(propName, null, newValue, null, false, out _));
+
+ Assert.True(TestMergeBaseItemData<Movie, MovieInfo>(propName, oldValue, null, null, true, out _));
+ }
+
+ [Fact]
+ public void MergeBaseItemData_MergeTrailers_ReplacesAppropriately()
+ {
+ string propName = "RemoteTrailers";
+ var oldValue = new[]
+ {
+ new MediaUrl
+ {
+ Name = "Name 1",
+ Url = "URL 1"
+ }
+ };
+ var newValue = new[]
+ {
+ new MediaUrl
+ {
+ Name = "Name 2",
+ Url = "URL 2"
+ }
+ };
+
+ Assert.False(TestMergeBaseItemData<Movie, MovieInfo>(propName, oldValue, newValue, null, false, out _));
+
+ Assert.True(TestMergeBaseItemData<Movie, MovieInfo>(propName, oldValue, newValue, null, true, out _));
+ Assert.True(TestMergeBaseItemData<Movie, MovieInfo>(propName, Array.Empty<MediaUrl>(), newValue, null, false, out _));
+
+ Assert.True(TestMergeBaseItemData<Movie, MovieInfo>(propName, oldValue, Array.Empty<MediaUrl>(), null, true, out _));
+ }
+
+ [Fact]
+ public void MergeBaseItemData_ProviderIds_MergesAppropriately()
+ {
+ var propName = "ProviderIds";
+ var oldValue = new Dictionary<string, string>
+ {
+ { "provider 1", "id 1" }
+ };
+
+ // overwrite provider id
+ var overwriteNewValue = new Dictionary<string, string>
+ {
+ { "provider 1", "id 2" }
+ };
+ Assert.False(TestMergeBaseItemData<Movie, MovieInfo>(propName, new Dictionary<string, string>(oldValue), overwriteNewValue, null, false, out _));
+ TestMergeBaseItemData<Movie, MovieInfo>(propName, new Dictionary<string, string>(oldValue), overwriteNewValue, null, true, out var overwritten);
+ Assert.Equal(overwriteNewValue, overwritten);
+
+ // merge without overwriting
+ var mergeNewValue = new Dictionary<string, string>
+ {
+ { "provider 1", "id 2" },
+ { "provider 2", "id 3" }
+ };
+ TestMergeBaseItemData<Movie, MovieInfo>(propName, new Dictionary<string, string>(oldValue), mergeNewValue, null, false, out var merged);
+ var actual = (Dictionary<string, string>)merged!;
+ Assert.Equal("id 1", actual["provider 1"]);
+ Assert.Equal("id 3", actual["provider 2"]);
+
+ // empty source results in no change
+ TestMergeBaseItemData<Movie, MovieInfo>(propName, new Dictionary<string, string>(oldValue), new Dictionary<string, string>(), null, true, out var notOverwritten);
+ Assert.Equal(oldValue, notOverwritten);
+ }
+
+ [Fact]
+ public void MergeBaseItemData_MergePeople_MergesAppropriately()
+ {
+ // PersonInfo in list is changed by merge, create new for every call
+ List<PersonInfo> GetOldValue()
+ => new()
+ {
+ new PersonInfo
+ {
+ Name = "Name 1",
+ ProviderIds = new Dictionary<string, string>
+ {
+ { "Provider 1", "1234" }
+ }
+ }
+ };
+
+ object? result;
+ List<PersonInfo> actual;
+
+ // overwrite provider id
+ var overwriteNewValue = new List<PersonInfo>
+ {
+ new()
+ {
+ Name = "Name 2"
+ }
+ };
+ Assert.False(TestMergeBaseItemDataPerson(GetOldValue(), overwriteNewValue, null, false, out result));
+ // People not already in target are not merged into it from source
+ actual = (List<PersonInfo>)result!;
+ Assert.Single(actual);
+ Assert.Equal("Name 1", actual[0].Name);
+
+ Assert.True(TestMergeBaseItemDataPerson(GetOldValue(), overwriteNewValue, null, true, out _));
+ Assert.True(TestMergeBaseItemDataPerson(new List<PersonInfo>(), overwriteNewValue, null, false, out _));
+ Assert.True(TestMergeBaseItemDataPerson(null, overwriteNewValue, null, false, out _));
+
+ Assert.False(TestMergeBaseItemDataPerson(GetOldValue(), overwriteNewValue, MetadataField.Cast, true, out _));
+
+ // providers merge but don't overwrite existing keys
+ var mergeNewValue = new List<PersonInfo>
+ {
+ new()
+ {
+ Name = "Name 1",
+ ProviderIds = new Dictionary<string, string>
+ {
+ { "Provider 1", "5678" },
+ { "Provider 2", "5678" }
+ }
+ }
+ };
+ TestMergeBaseItemDataPerson(GetOldValue(), mergeNewValue, null, false, out result);
+ actual = (List<PersonInfo>)result!;
+ Assert.Single(actual);
+ Assert.Equal("Name 1", actual[0].Name);
+ Assert.Equal(2, actual[0].ProviderIds.Count);
+ Assert.Equal("1234", actual[0].ProviderIds["Provider 1"]);
+ Assert.Equal("5678", actual[0].ProviderIds["Provider 2"]);
+
+ // picture adds if missing but won't overwrite (forcing overwrites entire list, not entries in merged PersonInfo)
+ var mergePicture1 = new List<PersonInfo>
+ {
+ new()
+ {
+ Name = "Name 1",
+ ImageUrl = "URL 1"
+ }
+ };
+ TestMergeBaseItemDataPerson(GetOldValue(), mergePicture1, null, false, out result);
+ actual = (List<PersonInfo>)result!;
+ Assert.Single(actual);
+ Assert.Equal("Name 1", actual[0].Name);
+ Assert.Equal("URL 1", actual[0].ImageUrl);
+ var mergePicture2 = new List<PersonInfo>
+ {
+ new()
+ {
+ Name = "Name 1",
+ ImageUrl = "URL 2"
+ }
+ };
+ TestMergeBaseItemDataPerson(mergePicture1, mergePicture2, null, false, out result);
+ actual = (List<PersonInfo>)result!;
+ Assert.Single(actual);
+ Assert.Equal("Name 1", actual[0].Name);
+ Assert.Equal("URL 1", actual[0].ImageUrl);
+
+ // empty source can be forced to overwrite a target with data
+ Assert.True(TestMergeBaseItemDataPerson(GetOldValue(), new List<PersonInfo>(), null, true, out _));
+ }
+
+ private static bool TestMergeBaseItemDataPerson(List<PersonInfo>? oldValue, List<PersonInfo>? newValue, MetadataField? lockField, bool replaceData, out object? actualValue)
+ {
+ var source = new MetadataResult<Movie>
+ {
+ Item = new Movie(),
+ People = newValue
+ };
+
+ var target = new MetadataResult<Movie>
+ {
+ Item = new Movie(),
+ People = oldValue
+ };
+
+ var lockedFields = lockField == null ? Array.Empty<MetadataField>() : new[] { (MetadataField)lockField };
+ MetadataService<Movie, MovieInfo>.MergeBaseItemData(source, target, lockedFields, replaceData, false);
+
+ actualValue = target.People;
+ return newValue?.Equals(actualValue) ?? actualValue == null;
+ }
+
+ /// <summary>
+ /// Makes a call to <see cref="MetadataService{TItemType,TIdType}.MergeBaseItemData"/> with the provided parameters and returns whether the target changed or not.
+ ///
+ /// Reflection is used to allow testing of all fields using the same logic, rather than relying on copy/pasting test code for each field.
+ /// </summary>
+ /// <param name="propName">The property to test.</param>
+ /// <param name="oldValue">The initial value in the target object.</param>
+ /// <param name="newValue">The initial value in the source object.</param>
+ /// <param name="lockField">The metadata field that locks this property if the field should be locked, or <c>null</c> to leave unlocked.</param>
+ /// <param name="replaceData">Passed through to <see cref="MetadataService{TItemType,TIdType}.MergeBaseItemData"/>.</param>
+ /// <param name="actualValue">The resulting value set to the target.</param>
+ /// <typeparam name="TItemType">The <see cref="BaseItem"/> type to test on.</typeparam>
+ /// <typeparam name="TIdType">The <see cref="BaseItem"/> info type.</typeparam>
+ /// <returns><c>true</c> if the property on the target updates to match the source value when<see cref="MetadataService{TItemType,TIdType}.MergeBaseItemData"/> is called.</returns>
+ private static bool TestMergeBaseItemData<TItemType, TIdType>(string propName, object? oldValue, object? newValue, MetadataField? lockField, bool replaceData, out object? actualValue)
+ where TItemType : BaseItem, IHasLookupInfo<TIdType>, new()
+ where TIdType : ItemLookupInfo, new()
+ {
+ var property = typeof(TItemType).GetProperty(propName)!;
+
+ var source = new MetadataResult<TItemType>
+ {
+ Item = new TItemType()
+ };
+ property.SetValue(source.Item, newValue);
+
+ var target = new MetadataResult<TItemType>
+ {
+ Item = new TItemType()
+ };
+ property.SetValue(target.Item, oldValue);
+
+ var lockedFields = lockField == null ? Array.Empty<MetadataField>() : new[] { (MetadataField)lockField };
+ // generic type doesn't actually matter to call the static method, just has to be filled in
+ MetadataService<TItemType, TIdType>.MergeBaseItemData(source, target, lockedFields, replaceData, false);
+
+ actualValue = property.GetValue(target.Item);
+ return newValue?.Equals(actualValue) ?? actualValue == null;
+ }
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs
index de4421320..f5c8cc970 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs
@@ -60,7 +60,8 @@ public class FindExtrasTests
"/movies/Up/Up.mkv",
"/movies/Up/Up - trailer.mkv",
"/movies/Up/Up - sample.mkv",
- "/movies/Up/Up something else.mkv"
+ "/movies/Up/Up something else.mkv",
+ "/movies/Up/Up-extra.mkv"
};
var files = paths.Select(p => new FileSystemMetadata
@@ -71,10 +72,11 @@ public class FindExtrasTests
var extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).OrderBy(e => e.ExtraType).ToList();
- Assert.Equal(2, extras.Count);
- Assert.Equal(ExtraType.Trailer, extras[0].ExtraType);
- Assert.Equal(typeof(Trailer), extras[0].GetType());
- Assert.Equal(ExtraType.Sample, extras[1].ExtraType);
+ Assert.Equal(3, extras.Count);
+ Assert.Equal(ExtraType.Unknown, extras[0].ExtraType);
+ Assert.Equal(ExtraType.Trailer, extras[1].ExtraType);
+ Assert.Equal(typeof(Trailer), extras[1].GetType());
+ Assert.Equal(ExtraType.Sample, extras[2].ExtraType);
}
[Fact]
@@ -92,7 +94,8 @@ public class FindExtrasTests
"/movies/Up/behind the scenes",
"/movies/Up/behind the scenes.mkv",
"/movies/Up/Up - sample.mkv",
- "/movies/Up/Up something else.mkv"
+ "/movies/Up/Up something else.mkv",
+ "/movies/Up/extras"
};
_fileSystemMock.Setup(f => f.GetFiles(
@@ -140,6 +143,21 @@ public class FindExtrasTests
}
}).Verifiable();
+ _fileSystemMock.Setup(f => f.GetFiles(
+ "/movies/Up/extras",
+ It.IsAny<string[]>(),
+ false,
+ false))
+ .Returns(new List<FileSystemMetadata>
+ {
+ new()
+ {
+ FullName = "/movies/Up/extras/Honest Trailer.mkv",
+ Name = "Honest Trailer.mkv",
+ IsDirectory = false
+ }
+ }).Verifiable();
+
var files = paths.Select(p => new FileSystemMetadata
{
FullName = p,
@@ -150,17 +168,19 @@ public class FindExtrasTests
var extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).OrderBy(e => e.ExtraType).ToList();
_fileSystemMock.Verify();
- Assert.Equal(6, extras.Count);
- Assert.Equal(ExtraType.Trailer, extras[0].ExtraType);
- Assert.Equal(typeof(Trailer), extras[0].GetType());
+ Assert.Equal(7, extras.Count);
+ Assert.Equal(ExtraType.Unknown, extras[0].ExtraType);
+ Assert.Equal(typeof(Video), extras[0].GetType());
Assert.Equal(ExtraType.Trailer, extras[1].ExtraType);
Assert.Equal(typeof(Trailer), extras[1].GetType());
- Assert.Equal(ExtraType.BehindTheScenes, extras[2].ExtraType);
- Assert.Equal(ExtraType.Sample, extras[3].ExtraType);
- Assert.Equal(ExtraType.ThemeSong, extras[4].ExtraType);
- Assert.Equal(typeof(Audio), extras[4].GetType());
+ Assert.Equal(ExtraType.Trailer, extras[2].ExtraType);
+ Assert.Equal(typeof(Trailer), extras[2].GetType());
+ Assert.Equal(ExtraType.BehindTheScenes, extras[3].ExtraType);
+ Assert.Equal(ExtraType.Sample, extras[4].ExtraType);
Assert.Equal(ExtraType.ThemeSong, extras[5].ExtraType);
Assert.Equal(typeof(Audio), extras[5].GetType());
+ Assert.Equal(ExtraType.ThemeSong, extras[6].ExtraType);
+ Assert.Equal(typeof(Audio), extras[6].GetType());
}
[Fact]
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs
new file mode 100644
index 000000000..f2efcddba
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/MovieResolverTests.cs
@@ -0,0 +1,34 @@
+using Emby.Naming.Common;
+using Emby.Server.Implementations.Library.Resolvers.Movies;
+using MediaBrowser.Controller;
+using MediaBrowser.Controller.Drawing;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Providers;
+using MediaBrowser.Model.IO;
+using Moq;
+using Xunit;
+
+namespace Jellyfin.Server.Implementations.Tests.Library;
+
+public class MovieResolverTests
+{
+ private static readonly NamingOptions _namingOptions = new();
+
+ [Fact]
+ public void Resolve_GivenLocalAlternateVersion_ResolvesToVideo()
+ {
+ var movieResolver = new MovieResolver(Mock.Of<IImageProcessor>(), _namingOptions);
+ var itemResolveArgs = new ItemResolveArgs(
+ Mock.Of<IServerApplicationPaths>(),
+ Mock.Of<IDirectoryService>())
+ {
+ Parent = null,
+ FileInfo = new FileSystemMetadata
+ {
+ FullName = "/movies/Black Panther (2018)/Black Panther (2018) - 1080p 3D.mk3d"
+ }
+ };
+
+ Assert.NotNull(movieResolver.Resolve(itemResolveArgs));
+ }
+}