aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Directory.Packages.props2
-rw-r--r--Emby.Naming/Common/NamingOptions.cs6
-rw-r--r--Emby.Server.Implementations/Library/LibraryManager.cs10
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs4
-rw-r--r--Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs13
-rw-r--r--Emby.Server.Implementations/Library/UserDataManager.cs1
-rw-r--r--Emby.Server.Implementations/Localization/Core/fo.json12
-rw-r--r--Emby.Server.Implementations/Localization/Core/hi.json3
-rw-r--r--Emby.Server.Implementations/Localization/Core/is.json8
-rw-r--r--Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs22
-rw-r--r--Emby.Server.Implementations/Sorting/PremiereDateComparer.cs2
-rw-r--r--Emby.Server.Implementations/Sorting/ProductionYearComparer.cs2
-rw-r--r--MediaBrowser.Controller/Entities/BaseItem.cs48
-rw-r--r--MediaBrowser.Controller/Entities/Movies/Movie.cs2
-rw-r--r--MediaBrowser.Controller/Entities/MusicVideo.cs2
-rw-r--r--MediaBrowser.Controller/Entities/TV/Series.cs2
-rw-r--r--MediaBrowser.Controller/Entities/Trailer.cs2
-rw-r--r--MediaBrowser.Controller/Entities/UserViewBuilder.cs2
-rw-r--r--MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs2
-rw-r--r--MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs2
-rw-r--r--MediaBrowser.Providers/Books/ComicBookInfo/ComicBookInfoProvider.cs2
-rw-r--r--MediaBrowser.Providers/Books/ComicInfo/ExternalComicInfoProvider.cs11
-rw-r--r--MediaBrowser.Providers/Books/ComicInfo/InternalComicInfoProvider.cs2
-rw-r--r--MediaBrowser.Providers/Manager/MetadataService.cs2
-rw-r--r--MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs14
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs2
-rw-r--r--MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs2
-rw-r--r--src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs6
-rw-r--r--src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs2
-rw-r--r--tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs77
-rw-r--r--tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs5
-rw-r--r--tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs60
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs41
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManagerSortTests.cs89
-rw-r--r--tests/Jellyfin.Server.Implementations.Tests/Library/UserDataManagerTests.cs7
35 files changed, 397 insertions, 72 deletions
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 15f36872e0..2e891de7cb 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -68,7 +68,7 @@
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="Serilog.Sinks.Graylog" Version="3.1.1" />
<PackageVersion Include="SerilogAnalyzer" Version="0.15.0" />
- <PackageVersion Include="SharpCompress" Version="0.50.0" />
+ <PackageVersion Include="SharpCompress" Version="0.50.1" />
<PackageVersion Include="SharpFuzz" Version="2.3.0" />
<PackageVersion Include="SkiaSharp" Version="3.119.4" />
<PackageVersion Include="SkiaSharp.HarfBuzz" Version="3.119.4" />
diff --git a/Emby.Naming/Common/NamingOptions.cs b/Emby.Naming/Common/NamingOptions.cs
index 21638ba9e7..0d93403758 100644
--- a/Emby.Naming/Common/NamingOptions.cs
+++ b/Emby.Naming/Common/NamingOptions.cs
@@ -57,7 +57,6 @@ namespace Emby.Naming.Common
".nrg",
".nsv",
".nuv",
- ".ogg",
".ogm",
".ogv",
".pva",
@@ -362,7 +361,10 @@ namespace Emby.Naming.Common
// Not a Kodi rule as well, but the expression below also causes false positives,
// so we make sure this one gets tested first.
// "Foo Bar 889"
- new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,4})(-(?<endingepnumber>[0-9]{2,4}))*[^\\\/x]*$")
+ // Names carrying an SxxEyy marker are excluded because the Kodi expression above already covers them.
+ // Without that guard this expression reads digits out of the title instead, turning
+ // "S01E01 1-23-45 [Bluray-1080p]" into episodes 1 through 45.
+ new EpisodeExpression(@".*[\\\/](?![Ee]pisode)(?![^\\\/]*[Ss][0-9]+[][ ._-]*[Ee][0-9]+)(?<seriesname>[\w\s]+?)\s(?<epnumber>[0-9]{1,4})(-(?<endingepnumber>[0-9]{2,4}))*[^\\\/x]*$")
{
IsNamed = true
},
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index f684f4dca3..983ecced02 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -2315,9 +2315,13 @@ namespace Emby.Server.Implementations.Library
{
var comparer = Comparers.FirstOrDefault(c => name == c.Type);
- // If it requires a user, create a new one, and assign the user
if (comparer is IUserBaseItemComparer)
{
+ if (user is null)
+ {
+ throw new ArgumentException($"Sort key '{name}' requires a user, but none was provided.");
+ }
+
var userComparer = (IUserBaseItemComparer)Activator.CreateInstance(comparer.GetType())!; // only null for Nullable<T> instances
userComparer.User = user;
@@ -3195,11 +3199,11 @@ namespace Emby.Server.Implementations.Library
}
}
- if (!episode.ProductionYear.HasValue)
+ if (episode.ProductionYear is null)
{
episode.ProductionYear = episodeInfo.Year;
- if (episode.ProductionYear.HasValue)
+ if (episode.ProductionYear is not null)
{
changed = true;
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
index b9f9f29723..6ba4a7bce6 100644
--- a/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/ExtraResolver.cs
@@ -32,8 +32,8 @@ namespace Emby.Server.Implementations.Library.Resolvers
: base(logger, namingOptions, directoryService)
{
_namingOptions = namingOptions;
- _trailerResolvers = new IItemResolver[] { new GenericVideoResolver<Trailer>(logger, namingOptions, directoryService) };
- _videoResolvers = new IItemResolver[] { this };
+ _trailerResolvers = [new GenericVideoResolver<Trailer>(logger, namingOptions, directoryService, parseName: true)];
+ _videoResolvers = [this];
}
protected override Video Resolve(ItemResolveArgs args)
diff --git a/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs
index ba320266a4..b3bdea704a 100644
--- a/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/GenericVideoResolver.cs
@@ -2,6 +2,7 @@
using Emby.Naming.Common;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Providers;
using Microsoft.Extensions.Logging;
@@ -14,15 +15,25 @@ namespace Emby.Server.Implementations.Library.Resolvers
public class GenericVideoResolver<T> : BaseVideoResolver<T>
where T : Video, new()
{
+ private readonly bool _parseName;
+
/// <summary>
/// Initializes a new instance of the <see cref="GenericVideoResolver{T}"/> class.
/// </summary>
/// <param name="logger">The logger.</param>
/// <param name="namingOptions">The naming options.</param>
/// <param name="directoryService">The directory service.</param>
- public GenericVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService)
+ /// <param name="parseName">Whether to parse the file name for metadata such as the year.</param>
+ public GenericVideoResolver(ILogger logger, NamingOptions namingOptions, IDirectoryService directoryService, bool parseName = false)
: base(logger, namingOptions, directoryService)
{
+ _parseName = parseName;
+ }
+
+ /// <inheritdoc />
+ protected override T Resolve(ItemResolveArgs args)
+ {
+ return ResolveVideo<T>(args, _parseName);
}
}
}
diff --git a/Emby.Server.Implementations/Library/UserDataManager.cs b/Emby.Server.Implementations/Library/UserDataManager.cs
index f5c41e5670..0680046c11 100644
--- a/Emby.Server.Implementations/Library/UserDataManager.cs
+++ b/Emby.Server.Implementations/Library/UserDataManager.cs
@@ -352,6 +352,7 @@ namespace Emby.Server.Implementations.Library
/// <inheritdoc />
public UserItemData? GetUserData(User user, BaseItem item)
{
+ ArgumentNullException.ThrowIfNull(user);
var row = ResolveUserDataRow(item, item.UserData?.Where(e => e.UserId.Equals(user.Id)));
return row is not null ? Map(row) : new UserItemData()
{
diff --git a/Emby.Server.Implementations/Localization/Core/fo.json b/Emby.Server.Implementations/Localization/Core/fo.json
index 8605a752db..e079ada064 100644
--- a/Emby.Server.Implementations/Localization/Core/fo.json
+++ b/Emby.Server.Implementations/Localization/Core/fo.json
@@ -1,5 +1,5 @@
{
- "Artists": "Listafólk",
+ "Artists": "Tónlistafólk",
"Collections": "Søvn",
"Default": "Forsett",
"External": "Ytri",
@@ -29,9 +29,9 @@
"NotificationOptionPluginUpdateInstalled": "Ískoytisforrit dagført",
"NotificationOptionUserLockedOut": "Brúkari útihýstur",
"Photos": "Ljósmyndir",
- "PluginInstalledWithName": "{0} var innlagt",
- "PluginUninstalledWithName": "{0} var strikað",
- "PluginUpdatedWithName": "{0} varð dagført",
+ "PluginInstalledWithName": "{0} innlagt",
+ "PluginUninstalledWithName": "{0} strikað",
+ "PluginUpdatedWithName": "{0} dagført",
"Shows": "Røðir",
"SubtitleDownloadFailureFromForItem": "Miseydnaðist at niðurtakað undirtekstir til {1} frá {0}",
"TvShows": "Sjónvarpsrøðir",
@@ -48,5 +48,7 @@
"TaskDownloadMissingLyricsDescription": "Niðurtekur sangtekstir",
"Movies": "Filmar",
"MixedContent": "Blandað innihald",
- "Music": "Tónleikur"
+ "Music": "Tónleikur",
+ "UserStartedPlayingItemWithValues": "{0} spælur {1} á {2}",
+ "HeaderContinueWatching": "Hald áfram at hyggja"
}
diff --git a/Emby.Server.Implementations/Localization/Core/hi.json b/Emby.Server.Implementations/Localization/Core/hi.json
index e98a5fbac1..5fbf61c627 100644
--- a/Emby.Server.Implementations/Localization/Core/hi.json
+++ b/Emby.Server.Implementations/Localization/Core/hi.json
@@ -105,5 +105,6 @@
"TaskExtractMediaSegmentsDescription": "मीडियासेगमेंट सक्षम प्लगइन्स से मीडिया सेगमेंट निकालता है या प्राप्त करता है।",
"TaskMoveTrickplayImages": "ट्रिकप्ले छवि स्थान माइग्रेट करें",
"TaskMoveTrickplayImagesDescription": "लाइब्रेरी सेटिंग्स के अनुसार मौजूदा ट्रिकप्ले फ़ाइलों को स्थानांतरित करता है।",
- "CleanupUserDataTask": "यूज़र डेटा सफाई कार्य"
+ "CleanupUserDataTask": "यूज़र डेटा सफाई कार्य",
+ "Original": "असली"
}
diff --git a/Emby.Server.Implementations/Localization/Core/is.json b/Emby.Server.Implementations/Localization/Core/is.json
index 825ad3084e..b889a073a2 100644
--- a/Emby.Server.Implementations/Localization/Core/is.json
+++ b/Emby.Server.Implementations/Localization/Core/is.json
@@ -62,7 +62,7 @@
"UserDownloadingItemWithValues": "{0} hleður niður {1}",
"SubtitleDownloadFailureFromForItem": "Tókst ekki að hala niður skjátextum frá {0} til {1}",
"Shows": "Þættir",
- "TaskRefreshChannelsDescription": "Endurhlaða upplýsingum netrása.",
+ "TaskRefreshChannelsDescription": "Endurhleður upplýsingum netrása.",
"TaskRefreshChannels": "Endurhlaða Rásir",
"TaskCleanTranscodeDescription": "Eyða umkóðuðum skrám sem eru meira en einum degi eldri.",
"TaskCleanTranscode": "Hreinsa Umkóðunarmöppu",
@@ -80,7 +80,7 @@
"TasksMaintenanceCategory": "Viðhald",
"Default": "Sjálfgefið",
"TaskCleanActivityLog": "Hreinsa athafnaskrá",
- "TaskRefreshPeople": "Endurnýja fólk",
+ "TaskRefreshPeople": "Endurnýja upplýsingar um fólk",
"TaskDownloadMissingSubtitles": "Sækja texta sem vantar",
"TaskOptimizeDatabase": "Fínstilla gagnagrunn",
"Undefined": "Óskilgreint",
@@ -95,8 +95,8 @@
"TaskCleanActivityLogDescription": "Eyðir virkniskráningarfærslum sem hafa náð settum hámarksaldri.",
"Forced": "Þvingað",
"External": "Útvær",
- "TaskRefreshTrickplayImagesDescription": "Býr til hraðspilunarmyndir fyrir myndbönd í virkum söfnum.",
- "TaskRefreshTrickplayImages": "Búa til hraðspilunarmyndir",
+ "TaskRefreshTrickplayImagesDescription": "Býr til hraðspilunarmyndir (Trickplay) fyrir myndbönd í virkum söfnum.",
+ "TaskRefreshTrickplayImages": "Búa til hraðspilunarmyndir (Trickplay)",
"TaskAudioNormalization": "Hljóðstöðlun",
"TaskAudioNormalizationDescription": "Leitar að hljóðstöðlunargögnum í skrám.",
"TaskDownloadMissingLyricsDescription": "Sækja söngtexta fyrir lög",
diff --git a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
index f10e7fcbb7..4159f8cf7d 100644
--- a/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
+++ b/Emby.Server.Implementations/Sorting/DateLastMediaAddedComparer.cs
@@ -3,35 +3,15 @@
using System;
using Jellyfin.Data.Enums;
-using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Entities;
-using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.Sorting;
using MediaBrowser.Model.Querying;
namespace Emby.Server.Implementations.Sorting
{
- public class DateLastMediaAddedComparer : IUserBaseItemComparer
+ public class DateLastMediaAddedComparer : IBaseItemComparer
{
/// <summary>
- /// Gets or sets the user.
- /// </summary>
- /// <value>The user.</value>
- public User User { get; set; }
-
- /// <summary>
- /// Gets or sets the user manager.
- /// </summary>
- /// <value>The user manager.</value>
- public IUserManager UserManager { get; set; }
-
- /// <summary>
- /// Gets or sets the user data manager.
- /// </summary>
- /// <value>The user data manager.</value>
- public IUserDataManager UserDataManager { get; set; }
-
- /// <summary>
/// Gets the name.
/// </summary>
/// <value>The name.</value>
diff --git a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
index 8c8b8824f3..30b268bb60 100644
--- a/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
+++ b/Emby.Server.Implementations/Sorting/PremiereDateComparer.cs
@@ -45,7 +45,7 @@ namespace Emby.Server.Implementations.Sorting
return x.PremiereDate.Value;
}
- if (x.ProductionYear.HasValue)
+ if (x.ProductionYear is not null)
{
try
{
diff --git a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
index 9aec87f183..8774bd8d4f 100644
--- a/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
+++ b/Emby.Server.Implementations/Sorting/ProductionYearComparer.cs
@@ -39,7 +39,7 @@ namespace Emby.Server.Implementations.Sorting
return 0;
}
- if (x.ProductionYear.HasValue)
+ if (x.ProductionYear is not null)
{
return x.ProductionYear.Value;
}
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index 21a726aaec..209feac702 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -1546,15 +1546,27 @@ namespace MediaBrowser.Controller.Entities
var extras = LibraryManager.FindExtras(item, fileSystemChildren, options.DirectoryService).ToArray();
var newExtraIds = Array.ConvertAll(extras, x => x.Id);
- var currentExtraIds = LibraryManager.GetItemList(new InternalItemsQuery()
+ var currentExtras = LibraryManager.GetItemList(new InternalItemsQuery()
{
OwnerIds = [item.Id]
- }).Select(e => e.Id).ToArray();
+ });
+
+ var currentExtraIds = currentExtras.Select(e => e.Id).ToArray();
var extrasChanged = !currentExtraIds.OrderBy(x => x).SequenceEqual(newExtraIds.OrderBy(x => x));
if (!extrasChanged && !options.ReplaceAllMetadata && options.MetadataRefreshMode != MetadataRefreshMode.FullRefresh)
{
+ // The owner's dates may only have become known after its extras were created, so keep
+ // them in sync even when there is nothing to refresh.
+ foreach (var extra in currentExtras)
+ {
+ if (extra.ExtraType is not null && InheritDatesFromOwner(item, extra))
+ {
+ await extra.UpdateToRepositoryAsync(ItemUpdateType.MetadataEdit, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
return false;
}
@@ -1570,6 +1582,7 @@ namespace MediaBrowser.Controller.Entities
i.OwnerId = ownerId;
i.ParentId = Guid.Empty;
+
return RefreshMetadataForOwnedItem(i, true, subOptions, cancellationToken);
});
@@ -2652,6 +2665,32 @@ namespace MediaBrowser.Controller.Entities
}
}
+ /// <summary>
+ /// Applies the owner's premiere date and production year to an owned item, returning whether anything changed.
+ /// </summary>
+ /// <param name="owner">The owner.</param>
+ /// <param name="ownedItem">The owned item.</param>
+ /// <returns><c>true</c> if the owned item was changed, else <c>false</c>.</returns>
+ internal static bool InheritDatesFromOwner(BaseItem owner, BaseItem ownedItem)
+ {
+ // Extras have no release date of their own, so the owner's is authoritative.
+ var changed = false;
+
+ if (owner.ProductionYear is not null && ownedItem.ProductionYear != owner.ProductionYear)
+ {
+ ownedItem.ProductionYear = owner.ProductionYear;
+ changed = true;
+ }
+
+ if (owner.PremiereDate is not null && ownedItem.PremiereDate != owner.PremiereDate)
+ {
+ ownedItem.PremiereDate = owner.PremiereDate;
+ changed = true;
+ }
+
+ return changed;
+ }
+
protected async Task RefreshMetadataForOwnedItem(BaseItem ownedItem, bool copyTitleMetadata, MetadataRefreshOptions options, CancellationToken cancellationToken)
{
var newOptions = new MetadataRefreshOptions(options)
@@ -2711,6 +2750,11 @@ namespace MediaBrowser.Controller.Entities
ownedItem.CustomRating = item.CustomRating;
newOptions.ForceSave = true;
}
+
+ if (InheritDatesFromOwner(item, ownedItem))
+ {
+ newOptions.ForceSave = true;
+ }
}
await ownedItem.RefreshMetadata(newOptions, cancellationToken).ConfigureAwait(false);
diff --git a/MediaBrowser.Controller/Entities/Movies/Movie.cs b/MediaBrowser.Controller/Entities/Movies/Movie.cs
index e8817a29cf..8c3ce2ff58 100644
--- a/MediaBrowser.Controller/Entities/Movies/Movie.cs
+++ b/MediaBrowser.Controller/Entities/Movies/Movie.cs
@@ -90,7 +90,7 @@ namespace MediaBrowser.Controller.Entities.Movies
{
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
- if (!ProductionYear.HasValue)
+ if (ProductionYear is null)
{
var info = LibraryManager.ParseName(Name);
diff --git a/MediaBrowser.Controller/Entities/MusicVideo.cs b/MediaBrowser.Controller/Entities/MusicVideo.cs
index 237ad5198c..effbf98820 100644
--- a/MediaBrowser.Controller/Entities/MusicVideo.cs
+++ b/MediaBrowser.Controller/Entities/MusicVideo.cs
@@ -40,7 +40,7 @@ namespace MediaBrowser.Controller.Entities
{
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
- if (!ProductionYear.HasValue)
+ if (ProductionYear is null)
{
var info = LibraryManager.ParseName(Name);
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index 952187c6e1..d9f300ad20 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -507,7 +507,7 @@ namespace MediaBrowser.Controller.Entities.TV
{
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
- if (!ProductionYear.HasValue)
+ if (ProductionYear is null)
{
var info = LibraryManager.ParseName(Name);
diff --git a/MediaBrowser.Controller/Entities/Trailer.cs b/MediaBrowser.Controller/Entities/Trailer.cs
index 939709215c..a2465eedf0 100644
--- a/MediaBrowser.Controller/Entities/Trailer.cs
+++ b/MediaBrowser.Controller/Entities/Trailer.cs
@@ -49,7 +49,7 @@ namespace MediaBrowser.Controller.Entities
{
var hasChanges = base.BeforeMetadataRefresh(replaceAllMetadata);
- if (!ProductionYear.HasValue)
+ if (ProductionYear is null)
{
var info = LibraryManager.ParseName(Name);
diff --git a/MediaBrowser.Controller/Entities/UserViewBuilder.cs b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
index c57ed2faf8..9ba103cc8b 100644
--- a/MediaBrowser.Controller/Entities/UserViewBuilder.cs
+++ b/MediaBrowser.Controller/Entities/UserViewBuilder.cs
@@ -730,7 +730,7 @@ namespace MediaBrowser.Controller.Entities
// Apply year filter
if (query.Years.Length > 0)
{
- if (!(item.ProductionYear.HasValue && query.Years.Contains(item.ProductionYear.Value)))
+ if (item.ProductionYear is null || !query.Years.Contains(item.ProductionYear.Value))
{
return false;
}
diff --git a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
index b0f51aec71..bc184d82fb 100644
--- a/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
+++ b/MediaBrowser.LocalMetadata/Savers/BaseXmlSaver.cs
@@ -277,7 +277,7 @@ namespace MediaBrowser.LocalMetadata.Savers
await writer.WriteElementStringAsync(null, "Rating", null, item.CommunityRating.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false);
}
- if (item.ProductionYear.HasValue && item is not Person)
+ if (item.ProductionYear is not null && item is not Person)
{
await writer.WriteElementStringAsync(null, "ProductionYear", null, item.ProductionYear.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false);
}
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index b6acfdbf3b..5533e55223 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -189,7 +189,7 @@ namespace MediaBrowser.MediaEncoding.Probing
}
// Guess ProductionYear from PremiereDate if missing
- if (!info.ProductionYear.HasValue && info.PremiereDate.HasValue)
+ if (info.ProductionYear is null && info.PremiereDate is not null)
{
info.ProductionYear = info.PremiereDate.Value.Year;
}
diff --git a/MediaBrowser.Providers/Books/ComicBookInfo/ComicBookInfoProvider.cs b/MediaBrowser.Providers/Books/ComicBookInfo/ComicBookInfoProvider.cs
index 787d2ad878..2bd2676ceb 100644
--- a/MediaBrowser.Providers/Books/ComicBookInfo/ComicBookInfoProvider.cs
+++ b/MediaBrowser.Providers/Books/ComicBookInfo/ComicBookInfoProvider.cs
@@ -42,7 +42,7 @@ public class ComicBookInfoProvider : IComicProvider
if (path is null)
{
- _logger.LogError("could not load comic: {Path}", info.Path);
+ _logger.LogDebug("could not load comic: {Path}", info.Path);
return new MetadataResult<Book> { HasMetadata = false };
}
diff --git a/MediaBrowser.Providers/Books/ComicInfo/ExternalComicInfoProvider.cs b/MediaBrowser.Providers/Books/ComicInfo/ExternalComicInfoProvider.cs
index 02cc02b7f3..cfd22a850e 100644
--- a/MediaBrowser.Providers/Books/ComicInfo/ExternalComicInfoProvider.cs
+++ b/MediaBrowser.Providers/Books/ComicInfo/ExternalComicInfoProvider.cs
@@ -38,7 +38,7 @@ public class ExternalComicInfoProvider : IComicProvider
if (comicInfoXml is null)
{
- _logger.LogInformation("Could not load ComicInfo metadata for {Path} from XML file.", info.Path);
+ _logger.LogDebug("No external ComicInfo metadata found for {Path}.", info.Path);
return new MetadataResult<Book> { HasMetadata = false };
}
@@ -67,23 +67,22 @@ public class ExternalComicInfoProvider : IComicProvider
private async Task<XDocument?> LoadXml(ItemInfo info, CancellationToken cancellationToken)
{
- var path = GetXmlFilePath(info.Path).FullName;
-
- if (path is null)
+ var file = GetXmlFilePath(info.Path);
+ if (!file.Exists)
{
return null;
}
try
{
- using var reader = XmlReader.Create(path, new XmlReaderSettings { Async = true });
+ using var reader = XmlReader.Create(file.FullName, new XmlReaderSettings { Async = true });
var comicInfoXml = XDocument.LoadAsync(reader, LoadOptions.None, cancellationToken);
return await comicInfoXml.ConfigureAwait(false);
}
catch (Exception e)
{
- _logger.LogInformation(e, "Could not load external XML from {Path}. This could mean there is no separate ComicInfo metadata file for this comic or the metadata is bundled within the comic.", path);
+ _logger.LogWarning(e, "Could not load external ComicInfo XML from {Path}.", file.FullName);
return null;
}
}
diff --git a/MediaBrowser.Providers/Books/ComicInfo/InternalComicInfoProvider.cs b/MediaBrowser.Providers/Books/ComicInfo/InternalComicInfoProvider.cs
index 98a6aba7d6..19062452b9 100644
--- a/MediaBrowser.Providers/Books/ComicInfo/InternalComicInfoProvider.cs
+++ b/MediaBrowser.Providers/Books/ComicInfo/InternalComicInfoProvider.cs
@@ -36,7 +36,7 @@ public class InternalComicInfoProvider : IComicProvider
if (comicInfoXml is null)
{
- _logger.LogInformation("Could not load ComicInfo metadata for {Path} from XML file. No internal XML in comic archive.", info.Path);
+ _logger.LogDebug("Could not load ComicInfo metadata for {Path} from XML file. No internal XML in comic archive.", info.Path);
return new MetadataResult<Book> { HasMetadata = false };
}
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index a438a94c40..f09c4c876c 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -1114,7 +1114,7 @@ namespace MediaBrowser.Providers.Manager
target.PremiereDate = source.PremiereDate;
}
- if (replaceData || !target.ProductionYear.HasValue)
+ if (replaceData || target.ProductionYear is null)
{
target.ProductionYear = source.ProductionYear;
}
diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
index f9d8883dff..1cb8e09414 100644
--- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
+++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs
@@ -386,7 +386,7 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
- private void FetchEmbeddedInfo(Video video, Model.MediaInfo.MediaInfo data, MetadataRefreshOptions refreshOptions, LibraryOptions libraryOptions)
+ internal void FetchEmbeddedInfo(Video video, Model.MediaInfo.MediaInfo data, MetadataRefreshOptions refreshOptions, LibraryOptions libraryOptions)
{
var replaceData = refreshOptions.ReplaceAllMetadata;
@@ -432,17 +432,19 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
- if (data.ProductionYear.HasValue)
+ // Extras have no release date of their own, they inherit it from the item they belong to.
+ var useContainerDates = video.ExtraType is null;
+ if (useContainerDates && data.ProductionYear is not null)
{
- if (!video.ProductionYear.HasValue || replaceData)
+ if (video.ProductionYear is null || replaceData)
{
video.ProductionYear = data.ProductionYear;
}
}
- if (data.PremiereDate.HasValue)
+ if (useContainerDates && data.PremiereDate is not null)
{
- if (!video.PremiereDate.HasValue || replaceData)
+ if (video.PremiereDate is null || replaceData)
{
video.PremiereDate = data.PremiereDate;
}
@@ -482,7 +484,7 @@ namespace MediaBrowser.Providers.MediaInfo
}
// If we don't have a ProductionYear try and get it from PremiereDate
- if (video.PremiereDate.HasValue && !video.ProductionYear.HasValue)
+ if (useContainerDates && video.PremiereDate is not null && video.ProductionYear is null)
{
video.ProductionYear = video.PremiereDate.Value.ToLocalTime().Year;
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs
index b5ba2d24f2..af2557df78 100644
--- a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs
@@ -82,7 +82,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
writer.WriteElementString("title", album.Name);
}
- if (album.ProductionYear.HasValue)
+ if (album.ProductionYear is not null)
{
writer.WriteElementString("year", album.ProductionYear.Value.ToString(CultureInfo.InvariantCulture));
}
diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
index 78907a5e68..5a9bbbef4f 100644
--- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
+++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs
@@ -544,7 +544,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
writer.WriteElementString("rating", item.CommunityRating.Value.ToString(CultureInfo.InvariantCulture));
}
- if (item.ProductionYear.HasValue)
+ if (item.ProductionYear is not null)
{
writer.WriteElementString("year", item.ProductionYear.Value.ToString(CultureInfo.InvariantCulture));
}
diff --git a/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs b/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs
index 846f9baf71..62a06370da 100644
--- a/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs
+++ b/src/Jellyfin.LiveTv/Recordings/RecordingsManager.cs
@@ -497,7 +497,7 @@ public sealed class RecordingsManager : IRecordingsManager, IDisposable
// trim trailing period from the folder name
var folderName = _fileSystem.GetValidFilename(timer.Name).Trim().TrimEnd('.').Trim();
- if (metadata is not null && metadata.ProductionYear.HasValue)
+ if (metadata is not null && metadata.ProductionYear is not null)
{
folderName += " (" + metadata.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
}
@@ -532,7 +532,7 @@ public sealed class RecordingsManager : IRecordingsManager, IDisposable
}
var folderName = _fileSystem.GetValidFilename(timer.Name).Trim();
- if (timer.ProductionYear.HasValue)
+ if (timer.ProductionYear is not null)
{
folderName += " (" + timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
}
@@ -550,7 +550,7 @@ public sealed class RecordingsManager : IRecordingsManager, IDisposable
}
var folderName = _fileSystem.GetValidFilename(timer.Name).Trim();
- if (timer.ProductionYear.HasValue)
+ if (timer.ProductionYear is not null)
{
folderName += " (" + timer.ProductionYear.Value.ToString(CultureInfo.InvariantCulture) + ")";
}
diff --git a/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs b/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs
index 7e68dbb547..e0e5a00fb9 100644
--- a/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs
+++ b/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs
@@ -290,7 +290,7 @@ public class RecordingsMetadataManager
null,
DateTime.UtcNow.ToString(DateAddedFormat, CultureInfo.InvariantCulture)).ConfigureAwait(false);
- if (item.ProductionYear.HasValue)
+ if (item.ProductionYear is not null)
{
await writer.WriteElementStringAsync(null, "year", null, item.ProductionYear.Value.ToString(CultureInfo.InvariantCulture)).ConfigureAwait(false);
}
diff --git a/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs b/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs
index de109c8d65..258cf326ca 100644
--- a/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs
+++ b/tests/Jellyfin.Controller.Tests/Entities/BaseItemTests.cs
@@ -6,6 +6,7 @@ using System.Threading;
using Jellyfin.Database.Implementations.Entities;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Movies;
using MediaBrowser.Controller.Library;
using MediaBrowser.Controller.LiveTv;
using MediaBrowser.Controller.MediaSegments;
@@ -366,4 +367,80 @@ public class BaseItemTests
Assert.Contains(alt2.Id, ids);
}
}
+
+ [Fact]
+ public void InheritDatesFromOwner_OwnerHasDates_OverwritesOwnedItemDates()
+ {
+ var owner = new Movie
+ {
+ ProductionYear = 1982,
+ PremiereDate = new DateTime(1982, 6, 25, 0, 0, 0, DateTimeKind.Utc)
+ };
+
+ // 2016 is what the container creation date of a re-encoded trailer would have yielded.
+ var trailer = new Trailer
+ {
+ ExtraType = ExtraType.Trailer,
+ ProductionYear = 2016,
+ PremiereDate = new DateTime(2016, 5, 4, 0, 0, 0, DateTimeKind.Utc)
+ };
+
+ Assert.True(BaseItem.InheritDatesFromOwner(owner, trailer));
+ Assert.Equal(owner.ProductionYear, trailer.ProductionYear);
+ Assert.Equal(owner.PremiereDate, trailer.PremiereDate);
+ }
+
+ [Fact]
+ public void InheritDatesFromOwner_OwnerHasNoDates_KeepsOwnedItemDates()
+ {
+ var owner = new Movie();
+ var trailer = new Trailer
+ {
+ ExtraType = ExtraType.Trailer,
+ ProductionYear = 1982,
+ PremiereDate = new DateTime(1982, 6, 25, 0, 0, 0, DateTimeKind.Utc)
+ };
+
+ Assert.False(BaseItem.InheritDatesFromOwner(owner, trailer));
+ Assert.Equal(1982, trailer.ProductionYear);
+ Assert.Equal(new DateTime(1982, 6, 25, 0, 0, 0, DateTimeKind.Utc), trailer.PremiereDate);
+ }
+
+ [Fact]
+ public void InheritDatesFromOwner_DatesAlreadyMatch_ReportsNoChange()
+ {
+ var owner = new Movie
+ {
+ ProductionYear = 1982,
+ PremiereDate = new DateTime(1982, 6, 25, 0, 0, 0, DateTimeKind.Utc)
+ };
+
+ var trailer = new Trailer
+ {
+ ExtraType = ExtraType.Trailer,
+ ProductionYear = owner.ProductionYear,
+ PremiereDate = owner.PremiereDate
+ };
+
+ Assert.False(BaseItem.InheritDatesFromOwner(owner, trailer));
+ }
+
+ [Fact]
+ public void InheritDatesFromOwner_OwnedItemHasNoDates_TakesOwnerDates()
+ {
+ var owner = new Movie
+ {
+ ProductionYear = 1982,
+ PremiereDate = new DateTime(1982, 6, 25, 0, 0, 0, DateTimeKind.Utc)
+ };
+
+ var trailer = new Trailer
+ {
+ ExtraType = ExtraType.Trailer
+ };
+
+ Assert.True(BaseItem.InheritDatesFromOwner(owner, trailer));
+ Assert.Equal(1982, trailer.ProductionYear);
+ Assert.Equal(new DateTime(1982, 6, 25, 0, 0, 0, DateTimeKind.Utc), trailer.PremiereDate);
+ }
}
diff --git a/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs b/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs
index b441e49b19..7e708c681d 100644
--- a/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs
+++ b/tests/Jellyfin.Naming.Tests/TV/MultiEpisodeTests.cs
@@ -69,6 +69,11 @@ namespace Jellyfin.Naming.Tests.TV
[InlineData("Season 1/series-s09e14-720i.mkv", null)]
[InlineData("Season 1/MOONLIGHTING_s01e01-e04.mkv", 4)]
[InlineData("Season 1/MOONLIGHTING_s01e01-e04", 4)]
+ // Hyphenated numbers in the episode title must not be read as an episode range
+ [InlineData("Season 1/S01E01 The 6-10 to Lubbock [WEBRip-1080p][AV1 Opus].mkv", null)]
+ [InlineData("Season 5/S05E23 11-59 [HDTV-1080p][x265 AC3].mkv", null)]
+ [InlineData("Season 5/S05E23 11-59 [HDTV-1080p][HEVC AC3].mkv", null)]
+ [InlineData("Season 1/S01E01 1-23-45 [Bluray-1080p][AV1 Opus].mkv", null)]
public void TestGetEndingEpisodeNumberFromFile(string filename, int? endingEpisodeNumber)
{
var result = _episodePathParser.Parse(filename, false);
diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs
index 2438ef06d1..59d3f42edf 100644
--- a/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs
+++ b/tests/Jellyfin.Providers.Tests/MediaInfo/FFProbeVideoInfoTests.cs
@@ -3,7 +3,9 @@ using AutoFixture;
using AutoFixture.AutoMoq;
using MediaBrowser.Controller.Configuration;
using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Providers;
using MediaBrowser.Model.Configuration;
+using MediaBrowser.Model.Entities;
using MediaBrowser.Providers.MediaInfo;
using Moq;
using Xunit;
@@ -75,4 +77,62 @@ public class FFProbeVideoInfoTests
Assert.All(chapters, chapter => Assert.True(chapter.StartPositionTicks < runtime));
}
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void FetchEmbeddedInfo_NoExtra_AppliesContainerDates(bool replaceAllMetadata)
+ {
+ var video = new Video();
+
+ _fFProbeVideoInfo.FetchEmbeddedInfo(video, CreateMediaInfoWithDates(), CreateRefreshOptions(replaceAllMetadata), new LibraryOptions());
+
+ Assert.Equal(2016, video.ProductionYear);
+ Assert.Equal(new DateTime(2016, 5, 4, 0, 0, 0, DateTimeKind.Utc), video.PremiereDate);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void FetchEmbeddedInfo_Extra_IgnoresContainerDates(bool replaceAllMetadata)
+ {
+ var video = new Video
+ {
+ ExtraType = ExtraType.Trailer,
+ ProductionYear = 1982,
+ PremiereDate = new DateTime(1982, 6, 25, 0, 0, 0, DateTimeKind.Utc)
+ };
+
+ _fFProbeVideoInfo.FetchEmbeddedInfo(video, CreateMediaInfoWithDates(), CreateRefreshOptions(replaceAllMetadata), new LibraryOptions());
+
+ Assert.Equal(1982, video.ProductionYear);
+ Assert.Equal(new DateTime(1982, 6, 25, 0, 0, 0, DateTimeKind.Utc), video.PremiereDate);
+ }
+
+ [Fact]
+ public void FetchEmbeddedInfo_ExtraWithoutDates_StaysWithoutDates()
+ {
+ var video = new Video
+ {
+ ExtraType = ExtraType.Trailer
+ };
+
+ _fFProbeVideoInfo.FetchEmbeddedInfo(video, CreateMediaInfoWithDates(), CreateRefreshOptions(false), new LibraryOptions());
+
+ Assert.Null(video.ProductionYear);
+ Assert.Null(video.PremiereDate);
+ }
+
+ private static MediaBrowser.Model.MediaInfo.MediaInfo CreateMediaInfoWithDates()
+ => new()
+ {
+ ProductionYear = 2016,
+ PremiereDate = new DateTime(2016, 5, 4, 0, 0, 0, DateTimeKind.Utc)
+ };
+
+ private static MetadataRefreshOptions CreateRefreshOptions(bool replaceAllMetadata)
+ => new(Mock.Of<IDirectoryService>())
+ {
+ ReplaceAllMetadata = replaceAllMetadata
+ };
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs
index 562711337f..07c537aee1 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManager/FindExtrasTests.cs
@@ -306,6 +306,47 @@ public class FindExtrasTests
}
[Fact]
+ public void FindExtras_TrailerWithYearInFilename_SetsProductionYearFromFilename()
+ {
+ var owner = new Movie { Name = "Up", Path = "/movies/Up/Up.mkv" };
+ var paths = new List<string>
+ {
+ "/movies/Up/Up.mkv",
+ "/movies/Up/trailers"
+ };
+
+ _fileSystemMock.Setup(f => f.GetFiles(
+ "/movies/Up/trailers",
+ It.IsAny<string[]>(),
+ false,
+ false))
+ .Returns(new List<FileSystemMetadata>
+ {
+ new()
+ {
+ FullName = "/movies/Up/trailers/Trailer 1 (2013).mkv",
+ Name = "Trailer 1 (2013).mkv",
+ IsDirectory = false
+ }
+ }).Verifiable();
+
+ var files = paths.Select(p => new FileSystemMetadata
+ {
+ FullName = p,
+ Name = Path.GetFileName(p),
+ IsDirectory = !Path.HasExtension(p)
+ }).ToList();
+
+ var extras = _libraryManager.FindExtras(owner, files, new DirectoryService(_fileSystemMock.Object)).ToList();
+
+ _fileSystemMock.Verify();
+ var trailer = Assert.Single(extras);
+ Assert.Equal(ExtraType.Trailer, trailer.ExtraType);
+ Assert.Equal(typeof(Trailer), trailer.GetType());
+ Assert.Equal(2013, trailer.ProductionYear);
+ }
+
+ [Fact]
public void FindExtras_SeriesWithTrailers_FindsCorrectExtras()
{
var owner = new Series { Name = "Dexter", Path = "/series/Dexter" };
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManagerSortTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManagerSortTests.cs
new file mode 100644
index 0000000000..65ec41291d
--- /dev/null
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/LibraryManagerSortTests.cs
@@ -0,0 +1,89 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using AutoFixture;
+using AutoFixture.AutoMoq;
+using Emby.Naming.Common;
+using Emby.Server.Implementations.Library;
+using Emby.Server.Implementations.Sorting;
+using Jellyfin.Data.Enums;
+using Jellyfin.Database.Implementations.Enums;
+using MediaBrowser.Controller.Configuration;
+using MediaBrowser.Controller.Entities;
+using MediaBrowser.Controller.Entities.Audio;
+using MediaBrowser.Controller.Library;
+using MediaBrowser.Controller.Persistence;
+using MediaBrowser.Controller.Resolvers;
+using MediaBrowser.Controller.Sorting;
+using MediaBrowser.Model.IO;
+using Moq;
+using Xunit;
+using BaseItem = MediaBrowser.Controller.Entities.BaseItem;
+
+namespace Jellyfin.Server.Implementations.Tests.Library;
+
+public class LibraryManagerSortTests
+{
+ [Fact]
+ public void Sort_UserDependentKey_NullUser_ThrowsArgumentException()
+ {
+ var libraryManager = CreateLibraryManager(
+ new IBaseItemComparer[] { new PlayCountComparer(), new SortNameComparer() });
+
+ BaseItem[] items =
+ {
+ new Audio { Name = "Zulu", SortName = "Zulu", Id = Guid.NewGuid() },
+ new Audio { Name = "Alpha", SortName = "Alpha", Id = Guid.NewGuid() },
+ };
+
+ Assert.Throws<ArgumentException>(() => libraryManager.Sort(
+ items,
+ user: null,
+ new[] { (ItemSortBy.PlayCount, SortOrder.Descending) }).ToArray());
+ }
+
+ [Fact]
+ public void Sort_DateLastContentAdded_NullUser_OrdersByDateNotSortName()
+ {
+ var libraryManager = CreateLibraryManager(
+ new IBaseItemComparer[] { new DateLastMediaAddedComparer(), new SortNameComparer() });
+
+ BaseItem[] items =
+ {
+ MakeFolder("Alpha", new DateTime(2026, 1, 1)),
+ MakeFolder("Mike", new DateTime(2025, 1, 1)),
+ MakeFolder("Zulu", new DateTime(2024, 1, 1))
+ };
+
+ var sorted = libraryManager.Sort(
+ items,
+ user: null,
+ new[] { (ItemSortBy.DateLastContentAdded, SortOrder.Descending) }).ToArray();
+
+ Assert.Equal(new[] { "Alpha", "Mike", "Zulu" }, sorted.Select(i => i.Name));
+ }
+
+ private static Folder MakeFolder(string name, DateTime dateLastMediaAdded)
+ => new() { Name = name, Id = Guid.NewGuid(), DateLastMediaAdded = dateLastMediaAdded };
+
+ private static Emby.Server.Implementations.Library.LibraryManager CreateLibraryManager(IReadOnlyCollection<IBaseItemComparer> comparers)
+ {
+ var fixture = new Fixture().Customize(new AutoMoqCustomization());
+ fixture.Register(() => new NamingOptions());
+ var configMock = fixture.Freeze<Mock<IServerConfigurationManager>>();
+ configMock.Setup(c => c.ApplicationPaths.ProgramDataPath).Returns("/data");
+ BaseItem.ConfigurationManager ??= configMock.Object;
+ var itemRepository = fixture.Freeze<Mock<IItemRepository>>();
+ itemRepository.Setup(i => i.RetrieveItem(It.IsAny<Guid>())).Returns<BaseItem>(null);
+ var fileSystemMock = fixture.Freeze<Mock<IFileSystem>>();
+ fileSystemMock.Setup(f => f.GetFileInfo(It.IsAny<string>())).Returns<string>(path => new FileSystemMetadata { FullName = path });
+
+ return fixture.Build<Emby.Server.Implementations.Library.LibraryManager>().Do(s => s.AddParts(
+ fixture.Create<IEnumerable<IResolverIgnoreRule>>(),
+ fixture.Create<IEnumerable<IItemResolver>>(),
+ fixture.Create<IEnumerable<IIntroProvider>>(),
+ comparers,
+ fixture.Create<IEnumerable<ILibraryPostScanTask>>()))
+ .Create();
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/UserDataManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/UserDataManagerTests.cs
index bd14ca008d..ba3127bc08 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/UserDataManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/UserDataManagerTests.cs
@@ -206,4 +206,11 @@ public sealed class UserDataManagerTests : IDisposable
Assert.Equal(222, result[fossilItem.Id].PlaybackPositionTicks);
Assert.Equal(333, result[retiredItem.Id].PlaybackPositionTicks);
}
+
+ [Fact]
+ public void GetUserData_NullUser_ThrowsArgumentNullException()
+ {
+ var item = CreateAudioBook();
+ Assert.Throws<ArgumentNullException>(() => _userDataManager.GetUserData(null!, item));
+ }
}