diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index a1ba8f17a0..3b7d6d0a16 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -207,6 +207,7 @@
- [TokerX](https://github.com/TokerX)
- [GeneMarks](https://github.com/GeneMarks)
- [martenumberto](https://github.com/martenumberto)
+ - [MarcoCoreDuo](https://github.com/MarcoCoreDuo)
# Emby Contributors
diff --git a/Emby.Naming/Emby.Naming.csproj b/Emby.Naming/Emby.Naming.csproj
index 3d4f3d9f4d..5e236bc230 100644
--- a/Emby.Naming/Emby.Naming.csproj
+++ b/Emby.Naming/Emby.Naming.csproj
@@ -36,7 +36,7 @@
Jellyfin Contributors
Jellyfin.Naming
- 10.11.5
+ 10.11.6
https://github.com/jellyfin/jellyfin
GPL-3.0-only
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index c5dc3b054c..b392340f71 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -1051,16 +1051,16 @@ namespace Emby.Server.Implementations.Dto
// Include artists that are not in the database yet, e.g., just added via metadata editor
// var foundArtists = artistItems.Items.Select(i => i.Item1.Name).ToList();
- dto.ArtistItems = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))])
- .Where(e => e.Value.Length > 0)
- .Select(i =>
- {
- return new NameGuidPair
- {
- Name = i.Key,
- Id = i.Value.First().Id
- };
- }).Where(i => i is not null).ToArray();
+ var artistsLookup = _libraryManager.GetArtists([.. hasArtist.Artists.Where(e => !string.IsNullOrWhiteSpace(e))]);
+
+ dto.ArtistItems = hasArtist.Artists
+ .Where(name => !string.IsNullOrWhiteSpace(name))
+ .Distinct()
+ .Select(name => artistsLookup.TryGetValue(name, out var artists) && artists.Length > 0
+ ? new NameGuidPair { Name = name, Id = artists[0].Id }
+ : null)
+ .Where(item => item is not null)
+ .ToArray();
}
if (item is IHasAlbumArtist hasAlbumArtist)
@@ -1085,31 +1085,16 @@ namespace Emby.Server.Implementations.Dto
// })
// .ToList();
+ var albumArtistsLookup = _libraryManager.GetArtists([.. hasAlbumArtist.AlbumArtists.Where(e => !string.IsNullOrWhiteSpace(e))]);
+
dto.AlbumArtists = hasAlbumArtist.AlbumArtists
- // .Except(foundArtists, new DistinctNameComparer())
- .Select(i =>
- {
- // This should not be necessary but we're seeing some cases of it
- if (string.IsNullOrEmpty(i))
- {
- return null;
- }
-
- var artist = _libraryManager.GetArtist(i, new DtoOptions(false)
- {
- EnableImages = false
- });
- if (artist is not null)
- {
- return new NameGuidPair
- {
- Name = artist.Name,
- Id = artist.Id
- };
- }
-
- return null;
- }).Where(i => i is not null).ToArray();
+ .Where(name => !string.IsNullOrWhiteSpace(name))
+ .Distinct()
+ .Select(name => albumArtistsLookup.TryGetValue(name, out var albumArtists) && albumArtists.Length > 0
+ ? new NameGuidPair { Name = name, Id = albumArtists[0].Id }
+ : null)
+ .Where(item => item is not null)
+ .ToArray();
}
// Add video info
diff --git a/Emby.Server.Implementations/IO/LibraryMonitor.cs b/Emby.Server.Implementations/IO/LibraryMonitor.cs
index d87ad729ee..7cff2a25b6 100644
--- a/Emby.Server.Implementations/IO/LibraryMonitor.cs
+++ b/Emby.Server.Implementations/IO/LibraryMonitor.cs
@@ -352,6 +352,12 @@ namespace Emby.Server.Implementations.IO
return;
}
+ var fileInfo = _fileSystem.GetFileSystemInfo(path);
+ if (DotIgnoreIgnoreRule.IsIgnored(fileInfo, null))
+ {
+ return;
+ }
+
// Ignore certain files, If the parent of an ignored path has a change event, ignore that too
foreach (var i in _tempIgnoredPaths.Keys)
{
diff --git a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
index 273d356a39..a25373326f 100644
--- a/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
+++ b/Emby.Server.Implementations/Images/CollectionFolderImageProvider.cs
@@ -98,5 +98,11 @@ namespace Emby.Server.Implementations.Images
return base.CreateImage(item, itemsWithImages, outputPath, imageType, imageIndex);
}
+
+ protected override bool HasChangedByDate(BaseItem item, ItemImageInfo image)
+ {
+ var age = DateTime.UtcNow - image.DateModified;
+ return age.TotalDays > 7;
+ }
}
}
diff --git a/Emby.Server.Implementations/Library/IgnorePatterns.cs b/Emby.Server.Implementations/Library/IgnorePatterns.cs
index fe3a1ce611..5fac2f6b0a 100644
--- a/Emby.Server.Implementations/Library/IgnorePatterns.cs
+++ b/Emby.Server.Implementations/Library/IgnorePatterns.cs
@@ -83,6 +83,7 @@ namespace Emby.Server.Implementations.Library
// Unix hidden files
"**/.*",
+ "**/.*/**",
// Mac - if you ever remove the above.
// "**/._*",
diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs
index 83c4eb2e91..aa5b37a94d 100644
--- a/Emby.Server.Implementations/Library/LibraryManager.cs
+++ b/Emby.Server.Implementations/Library/LibraryManager.cs
@@ -2202,6 +2202,12 @@ namespace Emby.Server.Implementations.Library
public Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken)
=> UpdateItemsAsync([item], parent, updateReason, cancellationToken);
+ ///
+ public async Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken)
+ {
+ await _itemRepository.ReattachUserDataAsync(item, cancellationToken).ConfigureAwait(false);
+ }
+
public async Task RunMetadataSavers(BaseItem item, ItemUpdateType updateReason)
{
if (item.IsFileProtocol)
@@ -3195,19 +3201,7 @@ namespace Emby.Server.Implementations.Library
var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName);
- var shortcutFilename = Path.GetFileNameWithoutExtension(path);
-
- var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
-
- while (File.Exists(lnk))
- {
- shortcutFilename += "1";
- lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
- }
-
- _fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
-
- RemoveContentTypeOverrides(path);
+ CreateShortcut(virtualFolderPath, pathInfo);
if (saveLibraryOptions)
{
@@ -3372,5 +3366,24 @@ namespace Emby.Server.Implementations.Library
return item is UserRootFolder || item.IsVisibleStandalone(user);
}
+
+ public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo)
+ {
+ var path = pathInfo.Path;
+ var rootFolderPath = _configurationManager.ApplicationPaths.DefaultUserViewsPath;
+
+ var shortcutFilename = Path.GetFileNameWithoutExtension(path);
+
+ var lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
+
+ while (File.Exists(lnk))
+ {
+ shortcutFilename += "1";
+ lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension);
+ }
+
+ _fileSystem.CreateShortcut(lnk, _appHost.ReverseVirtualPath(path));
+ RemoveContentTypeOverrides(path);
+ }
}
}
diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs
index b4c65ad85f..bc80c2b405 100644
--- a/Emby.Server.Implementations/Localization/LocalizationManager.cs
+++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs
@@ -38,6 +38,7 @@ namespace Emby.Server.Implementations.Localization
private readonly JsonSerializerOptions _jsonOptions = JsonDefaults.Options;
+ private readonly ConcurrentDictionary _cultureCache = new(StringComparer.OrdinalIgnoreCase);
private List _cultures = [];
private FrozenDictionary _iso6392BtoT = null!;
@@ -161,6 +162,7 @@ namespace Emby.Server.Implementations.Localization
list.Add(new CultureDto(name, displayname, twoCharName, threeLetterNames));
}
+ _cultureCache.Clear();
_cultures = list;
_iso6392BtoT = iso6392BtoTdict.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
}
@@ -169,20 +171,31 @@ namespace Emby.Server.Implementations.Localization
///
public CultureDto? FindLanguageInfo(string language)
{
- // TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs
- for (var i = 0; i < _cultures.Count; i++)
+ if (string.IsNullOrEmpty(language))
{
- var culture = _cultures[i];
- if (language.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase)
- || language.Equals(culture.Name, StringComparison.OrdinalIgnoreCase)
- || culture.ThreeLetterISOLanguageNames.Contains(language, StringComparison.OrdinalIgnoreCase)
- || language.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase))
- {
- return culture;
- }
+ return null;
}
- return default;
+ return _cultureCache.GetOrAdd(
+ language,
+ static (lang, cultures) =>
+ {
+ // TODO language should ideally be a ReadOnlySpan but moq cannot mock ref structs
+ for (var i = 0; i < cultures.Count; i++)
+ {
+ var culture = cultures[i];
+ if (lang.Equals(culture.DisplayName, StringComparison.OrdinalIgnoreCase)
+ || lang.Equals(culture.Name, StringComparison.OrdinalIgnoreCase)
+ || culture.ThreeLetterISOLanguageNames.Contains(lang, StringComparison.OrdinalIgnoreCase)
+ || lang.Equals(culture.TwoLetterISOLanguageName, StringComparison.OrdinalIgnoreCase))
+ {
+ return culture;
+ }
+ }
+
+ return null;
+ },
+ _cultures);
}
///
@@ -311,15 +324,19 @@ namespace Emby.Server.Implementations.Localization
else
{
// Fall back to server default language for ratings check
- // If it has no ratings, use the US ratings
- var ratingsDictionary = GetParentalRatingsDictionary() ?? GetParentalRatingsDictionary("us");
+ var ratingsDictionary = GetParentalRatingsDictionary();
if (ratingsDictionary is not null && ratingsDictionary.TryGetValue(rating, out ParentalRatingScore? value))
{
return value;
}
}
- // If we don't find anything, check all ratings systems
+ // If we don't find anything, check all ratings systems, starting with US
+ if (_allParentalRatings.TryGetValue("us", out var usRatings) && usRatings.TryGetValue(rating, out var usValue))
+ {
+ return usValue;
+ }
+
foreach (var dictionary in _allParentalRatings.Values)
{
if (dictionary.TryGetValue(rating, out var value))
diff --git a/Emby.Server.Implementations/Updates/InstallationManager.cs b/Emby.Server.Implementations/Updates/InstallationManager.cs
index 5ff4001601..5f9e29b563 100644
--- a/Emby.Server.Implementations/Updates/InstallationManager.cs
+++ b/Emby.Server.Implementations/Updates/InstallationManager.cs
@@ -156,6 +156,11 @@ namespace Emby.Server.Implementations.Updates
_logger.LogError(ex, "The URL configured for the plugin repository manifest URL is not valid: {Manifest}", manifest);
return Array.Empty();
}
+ catch (NotSupportedException ex)
+ {
+ _logger.LogError(ex, "The URL scheme configured for the plugin repository is not supported: {Manifest}", manifest);
+ return Array.Empty();
+ }
catch (HttpRequestException ex)
{
_logger.LogError(ex, "An error occurred while accessing the plugin manifest: {Manifest}", manifest);
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index fe6f855b5e..1e3e2740f0 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -1839,8 +1839,9 @@ public class DynamicHlsController : BaseJellyfinApiController
{
if (isActualOutputVideoCodecHevc)
{
- // Prefer dvh1 to dvhe
- args += " -tag:v:0 dvh1 -strict -2";
+ // Use hvc1 for 8.4. This is what Dolby uses for its official sample streams. Tagging with dvh1 would break some players with strict tag checking like Apple Safari.
+ var codecTag = state.VideoStream.VideoRangeType == VideoRangeType.DOVIWithHLG ? "hvc1" : "dvh1";
+ args += $" -tag:v:0 {codecTag} -strict -2";
}
else if (isActualOutputVideoCodecAv1)
{
diff --git a/Jellyfin.Api/Controllers/ItemUpdateController.cs b/Jellyfin.Api/Controllers/ItemUpdateController.cs
index e1d9b6bba0..e8a50666be 100644
--- a/Jellyfin.Api/Controllers/ItemUpdateController.cs
+++ b/Jellyfin.Api/Controllers/ItemUpdateController.cs
@@ -418,7 +418,7 @@ public class ItemUpdateController : BaseJellyfinApiController
{
if (item is IHasAlbumArtist hasAlbumArtists)
{
- hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name);
+ hasAlbumArtists.AlbumArtists = Array.ConvertAll(request.AlbumArtists, i => i.Name.Trim());
}
}
@@ -426,7 +426,7 @@ public class ItemUpdateController : BaseJellyfinApiController
{
if (item is IHasArtist hasArtists)
{
- hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name);
+ hasArtists.Artists = Array.ConvertAll(request.ArtistItems, i => i.Name.Trim());
}
}
diff --git a/Jellyfin.Api/Controllers/LibraryStructureController.cs b/Jellyfin.Api/Controllers/LibraryStructureController.cs
index 2a885662b5..117811429a 100644
--- a/Jellyfin.Api/Controllers/LibraryStructureController.cs
+++ b/Jellyfin.Api/Controllers/LibraryStructureController.cs
@@ -342,6 +342,17 @@ public class LibraryStructureController : BaseJellyfinApiController
return NotFound();
}
+ LibraryOptions options = item.GetLibraryOptions();
+ foreach (var mediaPath in request.LibraryOptions!.PathInfos)
+ {
+ if (options.PathInfos.Any(i => i.Path == mediaPath.Path))
+ {
+ continue;
+ }
+
+ _libraryManager.CreateShortcut(item.Path, mediaPath);
+ }
+
item.UpdateLibraryOptions(request.LibraryOptions);
return NoContent();
}
diff --git a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
index a38ad379cc..16e51151d9 100644
--- a/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
+++ b/Jellyfin.Api/Helpers/DynamicHlsHelper.cs
@@ -154,7 +154,7 @@ public class DynamicHlsHelper
// from universal audio service, need to override the AudioCodec when the actual request differs from original query
if (!string.Equals(state.OutputAudioCodec, _httpContextAccessor.HttpContext.Request.Query["AudioCodec"].ToString(), StringComparison.OrdinalIgnoreCase))
{
- var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(_httpContextAccessor.HttpContext.Request.QueryString.ToString());
+ var newQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(queryString);
newQuery["AudioCodec"] = state.OutputAudioCodec;
queryString = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(string.Empty, newQuery);
}
@@ -173,10 +173,21 @@ public class DynamicHlsHelper
queryString += "&TranscodeReasons=" + state.Request.TranscodeReasons;
}
- // Main stream
- var playlistUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
+ // Video rotation metadata is only supported in fMP4 remuxing
+ if (state.VideoStream is not null
+ && state.VideoRequest is not null
+ && (state.VideoStream?.Rotation ?? 0) != 0
+ && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+ && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
+ && !string.Equals(state.Request.SegmentContainer, "mp4", StringComparison.OrdinalIgnoreCase))
+ {
+ queryString += "&AllowVideoStreamCopy=false";
+ }
- playlistUrl += queryString;
+ // Main stream
+ var baseUrl = isLiveStream ? "live.m3u8" : "main.m3u8";
+ var playlistUrl = baseUrl + queryString;
+ var playlistQuery = Microsoft.AspNetCore.WebUtilities.QueryHelpers.ParseQuery(queryString);
var subtitleStreams = state.MediaSource
.MediaStreams
@@ -198,37 +209,36 @@ public class DynamicHlsHelper
AddSubtitles(state, subtitleStreams, builder, _httpContextAccessor.HttpContext.User);
}
- // Video rotation metadata is only supported in fMP4 remuxing
- if (state.VideoStream is not null
- && state.VideoRequest is not null
- && (state.VideoStream?.Rotation ?? 0) != 0
- && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
- && !string.IsNullOrWhiteSpace(state.Request.SegmentContainer)
- && !string.Equals(state.Request.SegmentContainer, "mp4", StringComparison.OrdinalIgnoreCase))
- {
- playlistUrl += "&AllowVideoStreamCopy=false";
- }
-
var basicPlaylist = AppendPlaylist(builder, state, playlistUrl, totalBitrate, subtitleGroup);
if (state.VideoStream is not null && state.VideoRequest is not null)
{
var encodingOptions = _serverConfigurationManager.GetEncodingOptions();
- // Provide SDR HEVC entrance for backward compatibility.
- if (encodingOptions.AllowHevcEncoding
- && !encodingOptions.AllowAv1Encoding
- && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
- && state.VideoStream.VideoRange == VideoRange.HDR
- && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase))
+ // Provide AV1 and HEVC SDR entrances for backward compatibility.
+ foreach (var sdrVideoCodec in new[] { "av1", "hevc" })
{
- var requestedVideoProfiles = state.GetRequestedProfiles("hevc");
- if (requestedVideoProfiles is not null && requestedVideoProfiles.Length > 0)
+ var isAv1EncodingAllowed = encodingOptions.AllowAv1Encoding
+ && string.Equals(sdrVideoCodec, "av1", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(state.ActualOutputVideoCodec, "av1", StringComparison.OrdinalIgnoreCase);
+ var isHevcEncodingAllowed = encodingOptions.AllowHevcEncoding
+ && string.Equals(sdrVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase)
+ && string.Equals(state.ActualOutputVideoCodec, "hevc", StringComparison.OrdinalIgnoreCase);
+ var isEncodingAllowed = isAv1EncodingAllowed || isHevcEncodingAllowed;
+
+ if (isEncodingAllowed
+ && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+ && state.VideoStream.VideoRange == VideoRange.HDR)
{
- // Force HEVC Main Profile and disable video stream copy.
- state.OutputVideoCodec = "hevc";
- var sdrVideoUrl = ReplaceProfile(playlistUrl, "hevc", string.Join(',', requestedVideoProfiles), "main");
- sdrVideoUrl += "&AllowVideoStreamCopy=false";
+ // Force AV1 and HEVC Main Profile and disable video stream copy.
+ state.OutputVideoCodec = sdrVideoCodec;
+
+ var sdrPlaylistQuery = playlistQuery;
+ sdrPlaylistQuery["VideoCodec"] = sdrVideoCodec;
+ sdrPlaylistQuery[sdrVideoCodec + "-profile"] = "main";
+ sdrPlaylistQuery["AllowVideoStreamCopy"] = "false";
+
+ var sdrVideoUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, sdrPlaylistQuery);
// HACK: Use the same bitrate so that the client can choose by other attributes, such as color range.
AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup);
@@ -238,12 +248,30 @@ public class DynamicHlsHelper
}
}
+ // Provide H.264 SDR entrance for backward compatibility.
+ if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+ && state.VideoStream.VideoRange == VideoRange.HDR)
+ {
+ // Force H.264 and disable video stream copy.
+ state.OutputVideoCodec = "h264";
+
+ var sdrPlaylistQuery = playlistQuery;
+ sdrPlaylistQuery["VideoCodec"] = "h264";
+ sdrPlaylistQuery["AllowVideoStreamCopy"] = "false";
+
+ var sdrVideoUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, sdrPlaylistQuery);
+
+ // HACK: Use the same bitrate so that the client can choose by other attributes, such as color range.
+ AppendPlaylist(builder, state, sdrVideoUrl, totalBitrate, subtitleGroup);
+
+ // Restore the video codec
+ state.OutputVideoCodec = "copy";
+ }
+
// Provide Level 5.0 entrance for backward compatibility.
// e.g. Apple A10 chips refuse the master playlist containing SDR HEVC Main Level 5.1 video,
// but in fact it is capable of playing videos up to Level 6.1.
- if (encodingOptions.AllowHevcEncoding
- && !encodingOptions.AllowAv1Encoding
- && EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
+ if (EncodingHelper.IsCopyCodec(state.OutputVideoCodec)
&& state.VideoStream.Level.HasValue
&& state.VideoStream.Level > 150
&& state.VideoStream.VideoRange == VideoRange.SDR
@@ -273,12 +301,15 @@ public class DynamicHlsHelper
var variation = GetBitrateVariation(totalBitrate);
var newBitrate = totalBitrate - variation;
- var variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
+ var variantQuery = playlistQuery;
+ variantQuery["VideoBitrate"] = (requestedVideoBitrate - variation).ToString(CultureInfo.InvariantCulture);
+ var variantUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, variantQuery);
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
variation *= 2;
newBitrate = totalBitrate - variation;
- variantUrl = ReplaceVideoBitrate(playlistUrl, requestedVideoBitrate, requestedVideoBitrate - variation);
+ variantQuery["VideoBitrate"] = (requestedVideoBitrate - variation).ToString(CultureInfo.InvariantCulture);
+ variantUrl = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(baseUrl, variantQuery);
AppendPlaylist(builder, state, variantUrl, newBitrate, subtitleGroup);
}
@@ -863,23 +894,6 @@ public class DynamicHlsHelper
return variation;
}
- private string ReplaceVideoBitrate(string url, int oldValue, int newValue)
- {
- return url.Replace(
- "videobitrate=" + oldValue.ToString(CultureInfo.InvariantCulture),
- "videobitrate=" + newValue.ToString(CultureInfo.InvariantCulture),
- StringComparison.OrdinalIgnoreCase);
- }
-
- private string ReplaceProfile(string url, string codec, string oldValue, string newValue)
- {
- string profileStr = codec + "-profile=";
- return url.Replace(
- profileStr + oldValue,
- profileStr + newValue,
- StringComparison.OrdinalIgnoreCase);
- }
-
private string ReplacePlaylistCodecsField(StringBuilder playlist, StringBuilder oldValue, StringBuilder newValue)
{
var oldPlaylist = playlist.ToString();
diff --git a/Jellyfin.Data/Jellyfin.Data.csproj b/Jellyfin.Data/Jellyfin.Data.csproj
index 41429d9619..8425c07631 100644
--- a/Jellyfin.Data/Jellyfin.Data.csproj
+++ b/Jellyfin.Data/Jellyfin.Data.csproj
@@ -18,7 +18,7 @@
Jellyfin Contributors
Jellyfin.Data
- 10.11.5
+ 10.11.6
https://github.com/jellyfin/jellyfin
GPL-3.0-only
diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
index 289ead11d7..787ef67a0c 100644
--- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
+++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs
@@ -617,7 +617,6 @@ public sealed class BaseItemRepository
var ids = tuples.Select(f => f.Item.Id).ToArray();
var existingItems = context.BaseItems.Where(e => ids.Contains(e.Id)).Select(f => f.Id).ToArray();
- var newItems = tuples.Where(e => !existingItems.Contains(e.Item.Id)).ToArray();
foreach (var item in tuples)
{
@@ -651,19 +650,6 @@ public sealed class BaseItemRepository
context.SaveChanges();
- foreach (var item in newItems)
- {
- // reattach old userData entries
- var userKeys = item.UserDataKey.ToArray();
- var retentionDate = (DateTime?)null;
- context.UserData
- .Where(e => e.ItemId == PlaceholderId)
- .Where(e => userKeys.Contains(e.CustomDataKey))
- .ExecuteUpdate(e => e
- .SetProperty(f => f.ItemId, item.Item.Id)
- .SetProperty(f => f.RetentionDate, retentionDate));
- }
-
var itemValueMaps = tuples
.Select(e => (e.Item, Values: GetItemValuesToSave(e.Item, e.InheritedTags)))
.ToArray();
@@ -759,6 +745,29 @@ public sealed class BaseItemRepository
transaction.Commit();
}
+ ///
+ public async Task ReattachUserDataAsync(BaseItemDto item, CancellationToken cancellationToken)
+ {
+ ArgumentNullException.ThrowIfNull(item);
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var dbContext = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
+
+ await using (dbContext.ConfigureAwait(false))
+ {
+ var userKeys = item.GetUserDataKeys().ToArray();
+ var retentionDate = (DateTime?)null;
+ await dbContext.UserData
+ .Where(e => e.ItemId == PlaceholderId)
+ .Where(e => userKeys.Contains(e.CustomDataKey))
+ .ExecuteUpdateAsync(
+ e => e
+ .SetProperty(f => f.ItemId, item.Id)
+ .SetProperty(f => f.RetentionDate, retentionDate),
+ cancellationToken).ConfigureAwait(false);
+ }
+ }
+
///
public BaseItemDto? RetrieveItem(Guid id)
{
@@ -866,7 +875,7 @@ public sealed class BaseItemRepository
}
dto.ExtraIds = string.IsNullOrWhiteSpace(entity.ExtraIds) ? [] : entity.ExtraIds.Split('|').Select(e => Guid.Parse(e)).ToArray();
- dto.ProductionLocations = entity.ProductionLocations?.Split('|') ?? [];
+ dto.ProductionLocations = entity.ProductionLocations?.Split('|', StringSplitOptions.RemoveEmptyEntries) ?? [];
dto.Studios = entity.Studios?.Split('|') ?? [];
dto.Tags = string.IsNullOrWhiteSpace(entity.Tags) ? [] : entity.Tags.Split('|');
@@ -1028,7 +1037,7 @@ public sealed class BaseItemRepository
}
entity.ExtraIds = dto.ExtraIds is not null ? string.Join('|', dto.ExtraIds) : null;
- entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations) : null;
+ entity.ProductionLocations = dto.ProductionLocations is not null ? string.Join('|', dto.ProductionLocations.Where(p => !string.IsNullOrWhiteSpace(p))) : null;
entity.Studios = dto.Studios is not null ? string.Join('|', dto.Studios) : null;
entity.Tags = dto.Tags is not null ? string.Join('|', dto.Tags) : null;
entity.LockedFields = dto.LockedFields is not null ? dto.LockedFields
@@ -1558,29 +1567,36 @@ public sealed class BaseItemRepository
IOrderedQueryable? orderedQuery = null;
+ // When searching, prioritize by match quality: exact match > prefix match > contains
+ if (hasSearch)
+ {
+ orderedQuery = query.OrderBy(OrderMapper.MapSearchRelevanceOrder(filter.SearchTerm!));
+ }
+
var firstOrdering = orderBy.FirstOrDefault();
if (firstOrdering != default)
{
var expression = OrderMapper.MapOrderByField(firstOrdering.OrderBy, filter, context);
- if (firstOrdering.SortOrder == SortOrder.Ascending)
+ if (orderedQuery is null)
{
- orderedQuery = query.OrderBy(expression);
+ // No search relevance ordering, start fresh
+ orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
+ ? query.OrderBy(expression)
+ : query.OrderByDescending(expression);
}
else
{
- orderedQuery = query.OrderByDescending(expression);
+ // Search relevance ordering already applied, chain with ThenBy
+ orderedQuery = firstOrdering.SortOrder == SortOrder.Ascending
+ ? orderedQuery.ThenBy(expression)
+ : orderedQuery.ThenByDescending(expression);
}
if (firstOrdering.OrderBy is ItemSortBy.Default or ItemSortBy.SortName)
{
- if (firstOrdering.SortOrder is SortOrder.Ascending)
- {
- orderedQuery = orderedQuery.ThenBy(e => e.Name);
- }
- else
- {
- orderedQuery = orderedQuery.ThenByDescending(e => e.Name);
- }
+ orderedQuery = firstOrdering.SortOrder is SortOrder.Ascending
+ ? orderedQuery.ThenBy(e => e.Name)
+ : orderedQuery.ThenByDescending(e => e.Name);
}
}
@@ -2447,35 +2463,24 @@ public sealed class BaseItemRepository
if (filter.ExcludeInheritedTags.Length > 0)
{
+ var excludedTags = filter.ExcludeInheritedTags;
baseQuery = baseQuery.Where(e =>
- !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue))
- && (e.Type != _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode] || !e.SeriesId.HasValue ||
- !context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && filter.ExcludeInheritedTags.Contains(f.ItemValue.CleanValue))));
+ !e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))
+ && (!e.SeriesId.HasValue || !context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && excludedTags.Contains(f.ItemValue.CleanValue))));
}
if (filter.IncludeInheritedTags.Length > 0)
{
- // For seasons and episodes, we also need to check the parent series' tags.
- if (includeTypes.Any(t => t == BaseItemKind.Episode || t == BaseItemKind.Season))
- {
- baseQuery = baseQuery.Where(e =>
- e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
- || (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))));
- }
+ var includeTags = filter.IncludeInheritedTags;
+ var isPlaylistOnlyQuery = includeTypes.Length == 1 && includeTypes.FirstOrDefault() == BaseItemKind.Playlist;
+ baseQuery = baseQuery.Where(e =>
+ e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue))
- // A playlist should be accessible to its owner regardless of allowed tags.
- else if (includeTypes.Length == 1 && includeTypes.FirstOrDefault() is BaseItemKind.Playlist)
- {
- baseQuery = baseQuery.Where(e =>
- e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue))
- || e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\""));
- // d ^^ this is stupid it hate this.
- }
- else
- {
- baseQuery = baseQuery.Where(e =>
- e.ItemValues!.Any(f => f.ItemValue.Type == ItemValueType.Tags && filter.IncludeInheritedTags.Contains(f.ItemValue.CleanValue)));
- }
+ // For seasons and episodes, we also need to check the parent series' tags.
+ || (e.SeriesId.HasValue && context.ItemValuesMap.Any(f => f.ItemId == e.SeriesId.Value && f.ItemValue.Type == ItemValueType.Tags && includeTags.Contains(f.ItemValue.CleanValue)))
+
+ // A playlist should be accessible to its owner regardless of allowed tags
+ || (isPlaylistOnlyQuery && e.Data!.Contains($"OwnerUserId\":\"{filter.User!.Id:N}\"")));
}
if (filter.SeriesStatuses.Length > 0)
@@ -2629,6 +2634,21 @@ public sealed class BaseItemRepository
.Where(e => artistNames.Contains(e.Name))
.ToArray();
- return artists.GroupBy(e => e.Name).ToDictionary(e => e.Key!, e => e.Select(f => DeserializeBaseItem(f)).Cast().ToArray());
+ var lookup = artists
+ .GroupBy(e => e.Name!)
+ .ToDictionary(
+ g => g.Key,
+ g => g.Select(f => DeserializeBaseItem(f)).Where(dto => dto is not null).Cast().ToArray());
+
+ var result = new Dictionary(artistNames.Count);
+ foreach (var name in artistNames)
+ {
+ if (lookup.TryGetValue(name, out var artistArray))
+ {
+ result[name] = artistArray;
+ }
+ }
+
+ return result;
}
}
diff --git a/Jellyfin.Server.Implementations/Item/OrderMapper.cs b/Jellyfin.Server.Implementations/Item/OrderMapper.cs
index 192ee74996..1ae7cc6c4a 100644
--- a/Jellyfin.Server.Implementations/Item/OrderMapper.cs
+++ b/Jellyfin.Server.Implementations/Item/OrderMapper.cs
@@ -6,6 +6,7 @@ using System.Linq.Expressions;
using Jellyfin.Data.Enums;
using Jellyfin.Database.Implementations;
using Jellyfin.Database.Implementations.Entities;
+using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
using Microsoft.EntityFrameworkCore;
@@ -68,4 +69,30 @@ public static class OrderMapper
_ => e => e.SortName
};
}
+
+ ///
+ /// Creates an expression to order search results by match quality.
+ /// Prioritizes: exact match (0) > prefix match with word boundary (1) > prefix match (2) > contains (3).
+ ///
+ /// The search term to match against.
+ /// An expression that returns an integer representing match quality (lower is better).
+ public static Expression> MapSearchRelevanceOrder(string searchTerm)
+ {
+ var cleanSearchTerm = GetCleanValue(searchTerm);
+ var searchPrefix = cleanSearchTerm + " ";
+ return e =>
+ e.CleanName == cleanSearchTerm ? 0 :
+ e.CleanName!.StartsWith(searchPrefix) ? 1 :
+ e.CleanName!.StartsWith(cleanSearchTerm) ? 2 : 3;
+ }
+
+ private static string GetCleanValue(string value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return value;
+ }
+
+ return value.RemoveDiacritics().ToLowerInvariant();
+ }
}
diff --git a/MediaBrowser.Common/MediaBrowser.Common.csproj b/MediaBrowser.Common/MediaBrowser.Common.csproj
index b046b53b1d..0e9ce7f2d0 100644
--- a/MediaBrowser.Common/MediaBrowser.Common.csproj
+++ b/MediaBrowser.Common/MediaBrowser.Common.csproj
@@ -8,7 +8,7 @@
Jellyfin Contributors
Jellyfin.Common
- 10.11.5
+ 10.11.6
https://github.com/jellyfin/jellyfin
GPL-3.0-only
diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs
index d9d2d0e3a8..7586b99e77 100644
--- a/MediaBrowser.Controller/Entities/BaseItem.cs
+++ b/MediaBrowser.Controller/Entities/BaseItem.cs
@@ -2053,6 +2053,9 @@ namespace MediaBrowser.Controller.Entities
public virtual async Task UpdateToRepositoryAsync(ItemUpdateType updateReason, CancellationToken cancellationToken)
=> await LibraryManager.UpdateItemAsync(this, GetParent(), updateReason, cancellationToken).ConfigureAwait(false);
+ public async Task ReattachUserDataAsync(CancellationToken cancellationToken) =>
+ await LibraryManager.ReattachUserDataAsync(this, cancellationToken).ConfigureAwait(false);
+
///
/// Validates that images within the item are still on the filesystem.
///
diff --git a/MediaBrowser.Controller/Entities/TV/Series.cs b/MediaBrowser.Controller/Entities/TV/Series.cs
index 427c2995bc..6396631f99 100644
--- a/MediaBrowser.Controller/Entities/TV/Series.cs
+++ b/MediaBrowser.Controller/Entities/TV/Series.cs
@@ -214,7 +214,7 @@ namespace MediaBrowser.Controller.Entities.TV
query.AncestorWithPresentationUniqueKey = null;
query.SeriesPresentationUniqueKey = seriesKey;
query.IncludeItemTypes = new[] { BaseItemKind.Season };
- query.OrderBy = new[] { (ItemSortBy.IndexNumber, SortOrder.Ascending) };
+ query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
if (user is not null && !user.DisplayMissingEpisodes)
{
@@ -247,6 +247,10 @@ namespace MediaBrowser.Controller.Entities.TV
query.AncestorWithPresentationUniqueKey = null;
query.SeriesPresentationUniqueKey = seriesKey;
+ if (query.OrderBy.Count == 0)
+ {
+ query.OrderBy = new[] { (ItemSortBy.SortName, SortOrder.Ascending) };
+ }
if (query.IncludeItemTypes.Length == 0)
{
diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs
index fcc5ed672a..df1c98f3f7 100644
--- a/MediaBrowser.Controller/Library/ILibraryManager.cs
+++ b/MediaBrowser.Controller/Library/ILibraryManager.cs
@@ -281,6 +281,14 @@ namespace MediaBrowser.Controller.Library
/// Returns a Task that can be awaited.
Task UpdateItemAsync(BaseItem item, BaseItem parent, ItemUpdateType updateReason, CancellationToken cancellationToken);
+ ///
+ /// Reattaches the user data to the item.
+ ///
+ /// The item.
+ /// The cancellation token.
+ /// A task that represents the asynchronous reattachment operation.
+ Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken);
+
///
/// Retrieves the item.
///
@@ -652,5 +660,12 @@ namespace MediaBrowser.Controller.Library
/// This exists so plugins can trigger a library scan.
///
void QueueLibraryScan();
+
+ ///
+ /// Add mblink file for a media path.
+ ///
+ /// The path to the virtualfolder.
+ /// The new virtualfolder.
+ public void CreateShortcut(string virtualFolderPath, MediaPathInfo pathInfo);
}
}
diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
index cf13d4d87e..04fe870738 100644
--- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj
+++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj
@@ -8,7 +8,7 @@
Jellyfin Contributors
Jellyfin.Controller
- 10.11.5
+ 10.11.6
https://github.com/jellyfin/jellyfin
GPL-3.0-only
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index 62ae61e9a6..c8ab74a524 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -6359,6 +6359,21 @@ namespace MediaBrowser.Controller.MediaEncoding
}
}
+ // Block unsupported H.264 Hi422P and Hi444PP profiles, which can be encoded with 4:2:0 pixel format
+ if (string.Equals(videoStream.Codec, "h264", StringComparison.OrdinalIgnoreCase))
+ {
+ if (videoStream.Profile.Contains("4:2:2", StringComparison.OrdinalIgnoreCase)
+ || videoStream.Profile.Contains("4:4:4", StringComparison.OrdinalIgnoreCase))
+ {
+ // VideoToolbox on Apple Silicon has H.264 Hi444PP and theoretically also has Hi422P
+ if (!(hardwareAccelerationType == HardwareAccelerationType.videotoolbox
+ && RuntimeInformation.OSArchitecture.Equals(Architecture.Arm64)))
+ {
+ return null;
+ }
+ }
+ }
+
var decoder = hardwareAccelerationType switch
{
HardwareAccelerationType.vaapi => GetVaapiVidDecoder(state, options, videoStream, bitDepth),
diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs
index 0026ab2b5f..790efb86a6 100644
--- a/MediaBrowser.Controller/Persistence/IItemRepository.cs
+++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs
@@ -35,6 +35,14 @@ public interface IItemRepository
void SaveImages(BaseItem item);
+ ///
+ /// Reattaches the user data to the item.
+ ///
+ /// The item.
+ /// The cancellation token.
+ /// A task that represents the asynchronous reattachment operation.
+ Task ReattachUserDataAsync(BaseItem item, CancellationToken cancellationToken);
+
///
/// Retrieves the item.
///
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index eb312029a1..55662e4013 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -299,9 +299,12 @@ namespace MediaBrowser.MediaEncoding.Probing
// Handle WebM
else if (string.Equals(splitFormat[i], "webm", StringComparison.OrdinalIgnoreCase))
{
- // Limit WebM to supported codecs
- if (mediaStreams.Any(stream => (stream.Type == MediaStreamType.Video && !_webmVideoCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase))
- || (stream.Type == MediaStreamType.Audio && !_webmAudioCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase))))
+ // Limit WebM to supported stream types and codecs.
+ // FFprobe can report "matroska,webm" for Matroska-like containers, so only keep "webm" if all streams are WebM-compatible.
+ // Any stream that is not video nor audio is not supported in WebM and should disqualify the webm container probe result.
+ if (mediaStreams.Any(stream => stream.Type is not MediaStreamType.Video and not MediaStreamType.Audio)
+ || mediaStreams.Any(stream => (stream.Type == MediaStreamType.Video && !_webmVideoCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase))
+ || (stream.Type == MediaStreamType.Audio && !_webmAudioCodecs.Contains(stream.Codec, StringComparison.OrdinalIgnoreCase))))
{
splitFormat[i] = string.Empty;
}
@@ -853,7 +856,12 @@ namespace MediaBrowser.MediaEncoding.Probing
}
// http://stackoverflow.com/questions/17353387/how-to-detect-anamorphic-video-with-ffprobe
- if (string.Equals(streamInfo.SampleAspectRatio, "1:1", StringComparison.Ordinal))
+ if (string.IsNullOrEmpty(streamInfo.SampleAspectRatio)
+ && string.IsNullOrEmpty(streamInfo.DisplayAspectRatio))
+ {
+ stream.IsAnamorphic = false;
+ }
+ else if (string.Equals(streamInfo.SampleAspectRatio, "1:1", StringComparison.Ordinal))
{
stream.IsAnamorphic = false;
}
diff --git a/MediaBrowser.Model/MediaBrowser.Model.csproj b/MediaBrowser.Model/MediaBrowser.Model.csproj
index 7959bc240f..41ce9fab8c 100644
--- a/MediaBrowser.Model/MediaBrowser.Model.csproj
+++ b/MediaBrowser.Model/MediaBrowser.Model.csproj
@@ -8,7 +8,7 @@
Jellyfin Contributors
Jellyfin.Model
- 10.11.5
+ 10.11.6
https://github.com/jellyfin/jellyfin
GPL-3.0-only
diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs
index a2102ca9cd..e9cb46eab5 100644
--- a/MediaBrowser.Providers/Manager/MetadataService.cs
+++ b/MediaBrowser.Providers/Manager/MetadataService.cs
@@ -153,7 +153,7 @@ namespace MediaBrowser.Providers.Manager
if (isFirstRefresh)
{
- await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, cancellationToken).ConfigureAwait(false);
+ await SaveItemAsync(metadataResult, ItemUpdateType.MetadataImport, false, cancellationToken).ConfigureAwait(false);
}
// Next run metadata providers
@@ -247,7 +247,7 @@ namespace MediaBrowser.Providers.Manager
}
// Save to database
- await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false);
+ await SaveItemAsync(metadataResult, updateType, isFirstRefresh, cancellationToken).ConfigureAwait(false);
}
return updateType;
@@ -275,9 +275,14 @@ namespace MediaBrowser.Providers.Manager
}
}
- protected async Task SaveItemAsync(MetadataResult result, ItemUpdateType reason, CancellationToken cancellationToken)
+ protected async Task SaveItemAsync(MetadataResult result, ItemUpdateType reason, bool reattachUserData, CancellationToken cancellationToken)
{
await result.Item.UpdateToRepositoryAsync(reason, cancellationToken).ConfigureAwait(false);
+ if (reattachUserData)
+ {
+ await result.Item.ReattachUserDataAsync(cancellationToken).ConfigureAwait(false);
+ }
+
if (result.Item.SupportsPeople && result.People is not null)
{
var baseItem = result.Item;
diff --git a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs
index 8df15e4408..e0a4c4f320 100644
--- a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs
+++ b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs
@@ -72,7 +72,7 @@ public class PlaylistMetadataService : MetadataService
}
else
{
- targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray();
+ targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).DistinctBy(i => i.Path).ToArray();
}
if (replaceData || targetItem.Shares.Count == 0)
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
index 414a0a3c9b..2beb34e43b 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
@@ -303,9 +303,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
CrewMember = crewMember,
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
})
- .Where(entry =>
- TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
- TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
+ .Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType));
if (config.HideMissingCrewMembers)
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
index e30c555cb4..f0e159f098 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
@@ -275,9 +275,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
CrewMember = crewMember,
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
})
- .Where(entry =>
- TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
- TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
+ .Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType));
if (config.HideMissingCrewMembers)
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
index 1b429039e7..0905a3bdcb 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
@@ -120,9 +120,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
CrewMember = crewMember,
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
})
- .Where(entry =>
- TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
- TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
+ .Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType));
if (config.HideMissingCrewMembers)
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
index f0828e8263..82d4e58384 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
@@ -367,9 +367,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
CrewMember = crewMember,
PersonType = TmdbUtils.MapCrewToPersonType(crewMember)
})
- .Where(entry =>
- TmdbUtils.WantedCrewKinds.Contains(entry.PersonType) ||
- TmdbUtils.WantedCrewTypes.Contains(entry.CrewMember.Job ?? string.Empty, StringComparison.OrdinalIgnoreCase));
+ .Where(entry => TmdbUtils.WantedCrewKinds.Contains(entry.PersonType));
if (config.HideMissingCrewMembers)
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
index f5e59a2789..d6e66a0e61 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbUtils.cs
@@ -70,18 +70,19 @@ namespace MediaBrowser.Providers.Plugins.Tmdb
public static PersonKind MapCrewToPersonType(Crew crew)
{
if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase)
- && crew.Job.Contains("director", StringComparison.OrdinalIgnoreCase))
+ && crew.Job.Equals("director", StringComparison.OrdinalIgnoreCase))
{
return PersonKind.Director;
}
if (crew.Department.Equals("production", StringComparison.OrdinalIgnoreCase)
- && crew.Job.Contains("producer", StringComparison.OrdinalIgnoreCase))
+ && crew.Job.Equals("producer", StringComparison.OrdinalIgnoreCase))
{
return PersonKind.Producer;
}
- if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase))
+ if (crew.Department.Equals("writing", StringComparison.OrdinalIgnoreCase)
+ && crew.Job.Equals("writer", StringComparison.OrdinalIgnoreCase))
{
return PersonKind.Writer;
}
diff --git a/SharedVersion.cs b/SharedVersion.cs
index de59b5d80a..27170e0d12 100644
--- a/SharedVersion.cs
+++ b/SharedVersion.cs
@@ -1,4 +1,4 @@
using System.Reflection;
-[assembly: AssemblyVersion("10.11.5")]
-[assembly: AssemblyFileVersion("10.11.5")]
+[assembly: AssemblyVersion("10.11.6")]
+[assembly: AssemblyFileVersion("10.11.6")]
diff --git a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
index d366d666d8..56fbd13ae7 100644
--- a/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
+++ b/src/Jellyfin.Extensions/Jellyfin.Extensions.csproj
@@ -15,7 +15,7 @@
Jellyfin Contributors
Jellyfin.Extensions
- 10.11.5
+ 10.11.6
https://github.com/jellyfin/jellyfin
GPL-3.0-only
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
index 94710a0957..8a2f84734e 100644
--- a/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
+++ b/tests/Jellyfin.MediaEncoding.Tests/Probing/ProbeResultNormalizerTests.cs
@@ -195,6 +195,18 @@ namespace Jellyfin.MediaEncoding.Tests.Probing
Assert.False(res.MediaStreams[0].IsAVC);
}
+ [Fact]
+ public void GetMediaInfo_WebM_Like_Mkv()
+ {
+ var bytes = File.ReadAllBytes("Test Data/Probing/video_web_like_mkv_with_subtitle.json");
+ var internalMediaInfoResult = JsonSerializer.Deserialize(bytes, _jsonOptions);
+
+ MediaInfo res = _probeResultNormalizer.GetMediaInfo(internalMediaInfoResult, VideoType.VideoFile, false, "Test Data/Probing/video_metadata.mkv", MediaProtocol.File);
+
+ Assert.Equal("mkv", res.Container);
+ Assert.Equal(3, res.MediaStreams.Count);
+ }
+
[Fact]
public void GetMediaInfo_ProgressiveVideoNoFieldOrder_Success()
{
diff --git a/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_web_like_mkv_with_subtitle.json b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_web_like_mkv_with_subtitle.json
new file mode 100644
index 0000000000..4f52dd90dc
--- /dev/null
+++ b/tests/Jellyfin.MediaEncoding.Tests/Test Data/Probing/video_web_like_mkv_with_subtitle.json
@@ -0,0 +1,137 @@
+{
+ "streams": [
+ {
+ "index": 0,
+ "codec_name": "vp8",
+ "codec_long_name": "On2 VP8",
+ "profile": "1",
+ "codec_type": "video",
+ "codec_tag_string": "[0][0][0][0]",
+ "codec_tag": "0x0000",
+ "width": 540,
+ "height": 360,
+ "coded_width": 540,
+ "coded_height": 360,
+ "closed_captions": 0,
+ "film_grain": 0,
+ "has_b_frames": 0,
+ "sample_aspect_ratio": "1:1",
+ "display_aspect_ratio": "3:2",
+ "pix_fmt": "yuv420p",
+ "level": -99,
+ "field_order": "progressive",
+ "refs": 1,
+ "r_frame_rate": "2997/125",
+ "avg_frame_rate": "2997/125",
+ "time_base": "1/1000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "disposition": {
+ "default": 1,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0,
+ "captions": 0,
+ "descriptions": 0,
+ "metadata": 0,
+ "dependent": 0,
+ "still_image": 0
+ },
+ "tags": {
+ "language": "eng"
+ }
+ },
+ {
+ "index": 1,
+ "codec_name": "vorbis",
+ "codec_long_name": "Vorbis",
+ "codec_type": "audio",
+ "codec_tag_string": "[0][0][0][0]",
+ "codec_tag": "0x0000",
+ "sample_fmt": "fltp",
+ "sample_rate": "44100",
+ "channels": 2,
+ "channel_layout": "stereo",
+ "bits_per_sample": 0,
+ "r_frame_rate": "0/0",
+ "avg_frame_rate": "0/0",
+ "time_base": "1/1000",
+ "start_pts": 0,
+ "start_time": "0.000000",
+ "duration": "117.707000",
+ "bit_rate": "127998",
+ "disposition": {
+ "default": 1,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0,
+ "captions": 0,
+ "descriptions": 0,
+ "metadata": 0,
+ "dependent": 0,
+ "still_image": 0
+ },
+ "tags": {
+ "language": "eng"
+ }
+ },
+ {
+ "index": 2,
+ "codec_name": "subrip",
+ "codec_long_name": "SubRip subtitle",
+ "codec_type": "subtitle",
+ "codec_tag_string": "[0][0][0][0]",
+ "codec_tag": "0x0000",
+ "disposition": {
+ "default": 0,
+ "dub": 0,
+ "original": 0,
+ "comment": 0,
+ "lyrics": 0,
+ "karaoke": 0,
+ "forced": 0,
+ "hearing_impaired": 0,
+ "visual_impaired": 0,
+ "clean_effects": 0,
+ "attached_pic": 0,
+ "timed_thumbnails": 0,
+ "captions": 0,
+ "descriptions": 0,
+ "metadata": 0,
+ "dependent": 0,
+ "still_image": 0
+ },
+ "tags": {
+ "language": "eng"
+ }
+ }
+ ],
+ "format": {
+ "filename": "sample.mkv",
+ "nb_streams": 3,
+ "nb_programs": 0,
+ "format_name": "matroska,webm",
+ "format_long_name": "Matroska / WebM",
+ "start_time": "0.000000",
+ "duration": "117.700914",
+ "size": "8566268",
+ "bit_rate": "582239",
+ "probe_score": 100
+ }
+}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs
index 07061cfc77..4cb6cb9607 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Library/IgnorePatternsTests.cs
@@ -19,7 +19,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("/media/movies/#recycle", true)]
[InlineData("thumbs.db", true)]
[InlineData(@"C:\media\movies\movie.avi", false)]
- [InlineData("/media/.hiddendir/file.mp4", false)]
+ [InlineData("/media/.hiddendir/file.mp4", true)]
[InlineData("/media/dir/.hiddenfile.mp4", true)]
[InlineData("/media/dir/._macjunk.mp4", true)]
[InlineData("/volume1/video/Series/@eaDir", true)]
@@ -32,7 +32,7 @@ namespace Jellyfin.Server.Implementations.Tests.Library
[InlineData("/media/music/Foo B.A.R", false)]
[InlineData("/media/music/Foo B.A.R.", false)]
[InlineData("/movies/.zfs/snapshot/AutoM-2023-09", true)]
- public void PathIgnored(string path, bool expected)
+ public void PathIgnored(string path, bool expected)
{
Assert.Equal(expected, IgnorePatterns.ShouldIgnore(path));
}
diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
index 6d6bba4fc4..e60522bf78 100644
--- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
+++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs
@@ -203,6 +203,25 @@ namespace Jellyfin.Server.Implementations.Tests.Localization
Assert.Null(localizationManager.GetRatingScore(value));
}
+ [Theory]
+ [InlineData("TV-MA", "DE", 17, 1)] // US-only rating, DE country code
+ [InlineData("PG-13", "FR", 13, 0)] // US-only rating, FR country code
+ [InlineData("R", "JP", 17, 0)] // US-only rating, JP country code
+ public async Task GetRatingScore_FallbackPrioritizesUS_Success(string rating, string countryCode, int expectedScore, int? expectedSubScore)
+ {
+ var localizationManager = Setup(new ServerConfiguration()
+ {
+ MetadataCountryCode = countryCode
+ });
+ await localizationManager.LoadAll();
+
+ var score = localizationManager.GetRatingScore(rating);
+
+ Assert.NotNull(score);
+ Assert.Equal(expectedScore, score.Score);
+ Assert.Equal(expectedSubScore, score.SubScore);
+ }
+
[Theory]
[InlineData("Default", "Default")]
[InlineData("HeaderLiveTV", "Live TV")]