diff --git a/.github/ISSUE_TEMPLATE/issue report.yml b/.github/ISSUE_TEMPLATE/issue report.yml
index 31ae502634..85590c0b0a 100644
--- a/.github/ISSUE_TEMPLATE/issue report.yml
+++ b/.github/ISSUE_TEMPLATE/issue report.yml
@@ -86,7 +86,7 @@ body:
label: Jellyfin Server version
description: What version of Jellyfin are you using?
options:
- - 10.9.7
+ - 10.9.8+
- Master
- Unstable
- Older*
diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml
index c6ea1d7ca9..ba66526e00 100644
--- a/.github/workflows/ci-codeql-analysis.yml
+++ b/.github/workflows/ci-codeql-analysis.yml
@@ -27,11 +27,11 @@ jobs:
dotnet-version: '8.0.x'
- name: Initialize CodeQL
- uses: github/codeql-action/init@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12
+ uses: github/codeql-action/init@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Autobuild
- uses: github/codeql-action/autobuild@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12
+ uses: github/codeql-action/autobuild@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
- name: Perform CodeQL Analysis
- uses: github/codeql-action/analyze@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12
+ uses: github/codeql-action/analyze@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15
diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml
index 54a0615567..d5dc1a8602 100644
--- a/.github/workflows/ci-openapi.yml
+++ b/.github/workflows/ci-openapi.yml
@@ -27,7 +27,7 @@ jobs:
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
+ uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
with:
name: openapi-head
retention-days: 14
@@ -61,7 +61,7 @@ jobs:
- name: Generate openapi.json
run: dotnet test tests/Jellyfin.Server.Integration.Tests/Jellyfin.Server.Integration.Tests.csproj -c Release --filter "Jellyfin.Server.Integration.Tests.OpenApiSpecTests"
- name: Upload openapi.json
- uses: actions/upload-artifact@0b2256b8c012f0828dc542b3febcab082c67f72b # v4.3.4
+ uses: actions/upload-artifact@89ef406dd8d7e03cfd12d9e0a4a378f454709029 # v4.3.5
with:
name: openapi-base
retention-days: 14
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index edbc846d63..7c2e72327a 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -185,6 +185,7 @@
- [Vedant](https://github.com/viktory36/)
- [NotSaifA](https://github.com/NotSaifA)
- [HonestlyWhoKnows](https://github.com/honestlywhoknows)
+ - [TheMelmacian](https://github.com/TheMelmacian)
- [ItsAllAboutTheCode](https://github.com/ItsAllAboutTheCode)
# Emby Contributors
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 825301bfcb..683f2eee32 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,7 +4,7 @@
-
+
@@ -22,7 +22,7 @@
-
+
@@ -58,7 +58,7 @@
-
+
@@ -72,7 +72,7 @@
-
+
@@ -81,6 +81,7 @@
+
diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs
index fc9ee8e569..45b91971bf 100644
--- a/Emby.Naming/TV/SeasonPathParser.cs
+++ b/Emby.Naming/TV/SeasonPathParser.cs
@@ -24,6 +24,8 @@ namespace Emby.Naming.TV
"stagione"
};
+ private static readonly char[] _splitChars = ['.', '_', ' ', '-'];
+
///
/// Attempts to parse season number from path.
///
@@ -83,14 +85,9 @@ namespace Emby.Naming.TV
}
}
- if (filename.StartsWith("s", StringComparison.OrdinalIgnoreCase))
+ if (TryGetSeasonNumberFromPart(filename, out int seasonNumber))
{
- var testFilename = filename.AsSpan().Slice(1);
-
- if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val))
- {
- return (val, true);
- }
+ return (seasonNumber, true);
}
// Look for one of the season folder names
@@ -108,10 +105,10 @@ namespace Emby.Naming.TV
}
}
- var parts = filename.Split(new[] { '.', '_', ' ', '-' }, StringSplitOptions.RemoveEmptyEntries);
+ var parts = filename.Split(_splitChars, StringSplitOptions.RemoveEmptyEntries);
foreach (var part in parts)
{
- if (TryGetSeasonNumberFromPart(part, out int seasonNumber))
+ if (TryGetSeasonNumberFromPart(part, out seasonNumber))
{
return (seasonNumber, true);
}
diff --git a/Emby.Photos/PhotoProvider.cs b/Emby.Photos/PhotoProvider.cs
index e2f1ca813c..ac6c41ca52 100644
--- a/Emby.Photos/PhotoProvider.cs
+++ b/Emby.Photos/PhotoProvider.cs
@@ -26,7 +26,7 @@ public class PhotoProvider : ICustomMetadataProvider, IForcedProvider, IH
private readonly ILogger _logger;
private readonly IImageProcessor _imageProcessor;
- // These are causing taglib to hang
+ // Other extensions might cause taglib to hang
private readonly string[] _includeExtensions = [".jpg", ".jpeg", ".png", ".tiff", ".cr2", ".webp", ".avif"];
///
diff --git a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
index 634eaf85ef..bfdcc08f42 100644
--- a/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
+++ b/Emby.Server.Implementations/Data/SqliteUserDataRepository.cs
@@ -333,10 +333,10 @@ namespace Emby.Server.Implementations.Data
/// The user item data.
private UserItemData ReadRow(SqliteDataReader reader)
{
- var userData = new UserItemData();
-
- userData.Key = reader[0].ToString();
- // userData.UserId = reader[1].ReadGuidFromBlob();
+ var userData = new UserItemData
+ {
+ Key = reader.GetString(0)
+ };
if (reader.TryGetDouble(2, out var rating))
{
diff --git a/Emby.Server.Implementations/Dto/DtoService.cs b/Emby.Server.Implementations/Dto/DtoService.cs
index 19902b26a0..0c0ba74533 100644
--- a/Emby.Server.Implementations/Dto/DtoService.cs
+++ b/Emby.Server.Implementations/Dto/DtoService.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -83,12 +81,12 @@ namespace Emby.Server.Implementations.Dto
private ILiveTvManager LivetvManager => _livetvManagerFactory.Value;
///
- public IReadOnlyList GetBaseItemDtos(IReadOnlyList items, DtoOptions options, User user = null, BaseItem owner = null)
+ public IReadOnlyList GetBaseItemDtos(IReadOnlyList items, DtoOptions options, User? user = null, BaseItem? owner = null)
{
var accessibleItems = user is null ? items : items.Where(x => x.IsVisible(user)).ToList();
var returnItems = new BaseItemDto[accessibleItems.Count];
- List<(BaseItem, BaseItemDto)> programTuples = null;
- List<(BaseItemDto, LiveTvChannel)> channelTuples = null;
+ List<(BaseItem, BaseItemDto)>? programTuples = null;
+ List<(BaseItemDto, LiveTvChannel)>? channelTuples = null;
for (int index = 0; index < accessibleItems.Count; index++)
{
@@ -137,7 +135,7 @@ namespace Emby.Server.Implementations.Dto
return returnItems;
}
- public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null)
+ public BaseItemDto GetBaseItemDto(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
{
var dto = GetBaseItemDtoInternal(item, options, user, owner);
if (item is LiveTvChannel tvChannel)
@@ -167,7 +165,7 @@ namespace Emby.Server.Implementations.Dto
return dto;
}
- private static IList GetTaggedItems(IItemByName byName, User user, DtoOptions options)
+ private static IList GetTaggedItems(IItemByName byName, User? user, DtoOptions options)
{
return byName.GetTaggedItems(
new InternalItemsQuery(user)
@@ -177,7 +175,7 @@ namespace Emby.Server.Implementations.Dto
});
}
- private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User user = null, BaseItem owner = null)
+ private BaseItemDto GetBaseItemDtoInternal(BaseItem item, DtoOptions options, User? user = null, BaseItem? owner = null)
{
var dto = new BaseItemDto
{
@@ -292,7 +290,7 @@ namespace Emby.Server.Implementations.Dto
}
var path = mediaSource.Path;
- string fileExtensionContainer = null;
+ string? fileExtensionContainer = null;
if (!string.IsNullOrEmpty(path))
{
@@ -316,7 +314,8 @@ namespace Emby.Server.Implementations.Dto
}
}
- public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List taggedItems, User user = null)
+ ///
+ public BaseItemDto GetItemByNameDto(BaseItem item, DtoOptions options, List? taggedItems, User? user = null)
{
var dto = GetBaseItemDtoInternal(item, options, user);
@@ -486,10 +485,10 @@ namespace Emby.Server.Implementations.Dto
return images
.Select(p => GetImageCacheTag(item, p))
.Where(i => i is not null)
- .ToArray();
+ .ToArray()!; // null values got filtered out
}
- private string GetImageCacheTag(BaseItem item, ItemImageInfo image)
+ private string? GetImageCacheTag(BaseItem item, ItemImageInfo image)
{
try
{
@@ -508,7 +507,7 @@ namespace Emby.Server.Implementations.Dto
/// The dto.
/// The item.
/// The requesting user.
- private void AttachPeople(BaseItemDto dto, BaseItem item, User user = null)
+ private void AttachPeople(BaseItemDto dto, BaseItem item, User? user = null)
{
// Ordering by person type to ensure actors and artists are at the front.
// This is taking advantage of the fact that they both begin with A
@@ -552,7 +551,7 @@ namespace Emby.Server.Implementations.Dto
var list = new List();
- var dictionary = people.Select(p => p.Name)
+ Dictionary dictionary = people.Select(p => p.Name)
.Distinct(StringComparer.OrdinalIgnoreCase).Select(c =>
{
try
@@ -565,9 +564,9 @@ namespace Emby.Server.Implementations.Dto
return null;
}
}).Where(i => i is not null)
- .Where(i => user is null || i.IsVisible(user))
- .DistinctBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
- .ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase);
+ .Where(i => user is null || i!.IsVisible(user))
+ .DistinctBy(x => x!.Name, StringComparer.OrdinalIgnoreCase)
+ .ToDictionary(i => i!.Name, StringComparer.OrdinalIgnoreCase)!; // null values got filtered out
for (var i = 0; i < people.Count; i++)
{
@@ -580,7 +579,7 @@ namespace Emby.Server.Implementations.Dto
Type = person.Type
};
- if (dictionary.TryGetValue(person.Name, out Person entity))
+ if (dictionary.TryGetValue(person.Name, out Person? entity))
{
baseItemPerson.PrimaryImageTag = GetTagAndFillBlurhash(dto, entity, ImageType.Primary);
baseItemPerson.Id = entity.Id;
@@ -650,7 +649,7 @@ namespace Emby.Server.Implementations.Dto
return _libraryManager.GetGenreId(name);
}
- private string GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ImageType imageType, int imageIndex = 0)
+ private string? GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ImageType imageType, int imageIndex = 0)
{
var image = item.GetImageInfo(imageType, imageIndex);
if (image is not null)
@@ -661,9 +660,14 @@ namespace Emby.Server.Implementations.Dto
return null;
}
- private string GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ItemImageInfo image)
+ private string? GetTagAndFillBlurhash(BaseItemDto dto, BaseItem item, ItemImageInfo image)
{
var tag = GetImageCacheTag(item, image);
+ if (tag is null)
+ {
+ return null;
+ }
+
if (!string.IsNullOrEmpty(image.BlurHash))
{
dto.ImageBlurHashes ??= new Dictionary>();
@@ -716,7 +720,7 @@ namespace Emby.Server.Implementations.Dto
/// The item.
/// The owner.
/// The options.
- private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem owner, DtoOptions options)
+ private void AttachBasicFields(BaseItemDto dto, BaseItem item, BaseItem? owner, DtoOptions options)
{
if (options.ContainsField(ItemFields.DateCreated))
{
@@ -1097,7 +1101,7 @@ namespace Emby.Server.Implementations.Dto
}
}
- BaseItem[] allExtras = null;
+ BaseItem[]? allExtras = null;
if (options.ContainsField(ItemFields.SpecialFeatureCount))
{
@@ -1134,7 +1138,7 @@ namespace Emby.Server.Implementations.Dto
dto.SeasonId = episode.SeasonId;
dto.SeriesId = episode.SeriesId;
- Series episodeSeries = null;
+ Series? episodeSeries = null;
// this block will add the series poster for episodes without a poster
// TODO maybe remove the if statement entirely
@@ -1162,8 +1166,10 @@ namespace Emby.Server.Implementations.Dto
}
// Add SeriesInfo
- if (item is Series series)
+ Series? series;
+ if (item is Series tmp)
{
+ series = tmp;
dto.AirDays = series.AirDays;
dto.AirTime = series.AirTime;
dto.Status = series.Status?.ToString();
@@ -1264,7 +1270,7 @@ namespace Emby.Server.Implementations.Dto
}
}
- private BaseItem GetImageDisplayParent(BaseItem currentItem, BaseItem originalItem)
+ private BaseItem? GetImageDisplayParent(BaseItem currentItem, BaseItem originalItem)
{
if (currentItem is MusicAlbum musicAlbum)
{
@@ -1285,7 +1291,7 @@ namespace Emby.Server.Implementations.Dto
return parent;
}
- private void AddInheritedImages(BaseItemDto dto, BaseItem item, DtoOptions options, BaseItem owner)
+ private void AddInheritedImages(BaseItemDto dto, BaseItem item, DtoOptions options, BaseItem? owner)
{
if (!item.SupportsInheritedParentImages)
{
@@ -1305,7 +1311,7 @@ namespace Emby.Server.Implementations.Dto
return;
}
- BaseItem parent = null;
+ BaseItem? parent = null;
var isFirst = true;
var imageTags = dto.ImageTags;
@@ -1378,7 +1384,7 @@ namespace Emby.Server.Implementations.Dto
}
}
- private string GetMappedPath(BaseItem item, BaseItem ownerItem)
+ private string GetMappedPath(BaseItem item, BaseItem? ownerItem)
{
var path = item.Path;
diff --git a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
index 957ad9c01b..47f9dfbc87 100644
--- a/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
+++ b/Emby.Server.Implementations/EntryPoints/UserDataChangeNotifier.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -138,13 +137,13 @@ namespace Emby.Server.Implementations.EntryPoints
return new UserDataChangeInfo
{
- UserId = userId.ToString("N", CultureInfo.InvariantCulture),
+ UserId = userId,
UserDataList = changedItems
.DistinctBy(x => x.Id)
.Select(i =>
{
var dto = _userDataManager.GetUserDataDto(i, user);
- dto.ItemId = i.Id.ToString("N", CultureInfo.InvariantCulture);
+ dto.ItemId = i.Id;
return dto;
})
.ToArray()
diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs
index bb22ca82fa..90a01c052c 100644
--- a/Emby.Server.Implementations/Library/MediaSourceManager.cs
+++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs
@@ -379,7 +379,8 @@ namespace Emby.Server.Implementations.Library
private void SetDefaultSubtitleStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
{
- if (userData.SubtitleStreamIndex.HasValue
+ if (userData is not null
+ && userData.SubtitleStreamIndex.HasValue
&& user.RememberSubtitleSelections
&& user.SubtitleMode != SubtitlePlaybackMode.None
&& allowRememberingSelection)
@@ -411,7 +412,7 @@ namespace Emby.Server.Implementations.Library
private void SetDefaultAudioStreamIndex(MediaSourceInfo source, UserItemData userData, User user, bool allowRememberingSelection)
{
- if (userData.AudioStreamIndex.HasValue && user.RememberAudioSelections && allowRememberingSelection)
+ if (userData is not null && userData.AudioStreamIndex.HasValue && user.RememberAudioSelections && allowRememberingSelection)
{
var index = userData.AudioStreamIndex.Value;
// Make sure the saved index is still valid
@@ -434,7 +435,7 @@ namespace Emby.Server.Implementations.Library
if (mediaType == MediaType.Video)
{
- var userData = item is null ? new UserItemData() : _userDataManager.GetUserData(user, item);
+ var userData = item is null ? null : _userDataManager.GetUserData(user, item);
var allowRememberingSelection = item is null || item.EnableRememberingTrackSelections;
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
index 955055313e..4b15073858 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/BoxSetResolver.cs
@@ -68,11 +68,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
var justName = Path.GetFileName(item.Path.AsSpan());
var id = justName.GetAttributeValue("tmdbid");
-
- if (!string.IsNullOrEmpty(id))
- {
- item.SetProviderId(MetadataProvider.Tmdb, id);
- }
+ item.TrySetProviderId(MetadataProvider.Tmdb, id);
}
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
index 1a210e3cc8..4debe722b9 100644
--- a/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/Movies/MovieResolver.cs
@@ -373,22 +373,14 @@ namespace Emby.Server.Implementations.Library.Resolvers.Movies
{
// Check for TMDb id
var tmdbid = justName.GetAttributeValue("tmdbid");
-
- if (!string.IsNullOrWhiteSpace(tmdbid))
- {
- item.SetProviderId(MetadataProvider.Tmdb, tmdbid);
- }
+ item.TrySetProviderId(MetadataProvider.Tmdb, tmdbid);
}
if (!string.IsNullOrEmpty(item.Path))
{
// Check for IMDb id - we use full media path, as we can assume that this will match in any use case (whether id in parent dir or in file name)
var imdbid = item.Path.AsSpan().GetAttributeValue("imdbid");
-
- if (!string.IsNullOrWhiteSpace(imdbid))
- {
- item.SetProviderId(MetadataProvider.Imdb, imdbid);
- }
+ item.TrySetProviderId(MetadataProvider.Imdb, imdbid);
}
}
}
diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
index 1484c34bcc..fb48d7bf17 100644
--- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
+++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs
@@ -186,46 +186,25 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV
var justName = Path.GetFileName(path.AsSpan());
var imdbId = justName.GetAttributeValue("imdbid");
- if (!string.IsNullOrEmpty(imdbId))
- {
- item.SetProviderId(MetadataProvider.Imdb, imdbId);
- }
+ item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
var tvdbId = justName.GetAttributeValue("tvdbid");
- if (!string.IsNullOrEmpty(tvdbId))
- {
- item.SetProviderId(MetadataProvider.Tvdb, tvdbId);
- }
+ item.TrySetProviderId(MetadataProvider.Tvdb, tvdbId);
var tvmazeId = justName.GetAttributeValue("tvmazeid");
- if (!string.IsNullOrEmpty(tvmazeId))
- {
- item.SetProviderId(MetadataProvider.TvMaze, tvmazeId);
- }
+ item.TrySetProviderId(MetadataProvider.TvMaze, tvmazeId);
var tmdbId = justName.GetAttributeValue("tmdbid");
- if (!string.IsNullOrEmpty(tmdbId))
- {
- item.SetProviderId(MetadataProvider.Tmdb, tmdbId);
- }
+ item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId);
var anidbId = justName.GetAttributeValue("anidbid");
- if (!string.IsNullOrEmpty(anidbId))
- {
- item.SetProviderId("AniDB", anidbId);
- }
+ item.TrySetProviderId("AniDB", anidbId);
var aniListId = justName.GetAttributeValue("anilistid");
- if (!string.IsNullOrEmpty(aniListId))
- {
- item.SetProviderId("AniList", aniListId);
- }
+ item.TrySetProviderId("AniList", aniListId);
var aniSearchId = justName.GetAttributeValue("anisearchid");
- if (!string.IsNullOrEmpty(aniSearchId))
- {
- item.SetProviderId("AniSearch", aniSearchId);
- }
+ item.TrySetProviderId("AniSearch", aniSearchId);
}
}
}
diff --git a/Emby.Server.Implementations/Localization/Core/es-AR.json b/Emby.Server.Implementations/Localization/Core/es-AR.json
index 8bd3c5defe..9433da28b1 100644
--- a/Emby.Server.Implementations/Localization/Core/es-AR.json
+++ b/Emby.Server.Implementations/Localization/Core/es-AR.json
@@ -124,5 +124,11 @@
"External": "Externo",
"TaskKeyframeExtractorDescription": "Extrae Fotogramas Clave de los archivos de vídeo para crear Listas de Reprodución HLS más precisas. Esta tarea puede durar mucho tiempo.",
"TaskKeyframeExtractor": "Extractor de Fotogramas Clave",
- "HearingImpaired": "Discapacidad Auditiva"
+ "HearingImpaired": "Discapacidad Auditiva",
+ "TaskRefreshTrickplayImages": "Generar imágenes de Trickplay",
+ "TaskRefreshTrickplayImagesDescription": "Crea vistas previas de reproducción engañosa para videos en bibliotecas habilitadas.",
+ "TaskAudioNormalization": "Normalización de audio",
+ "TaskAudioNormalizationDescription": "Escanea archivos en busca de datos de normalización de audio.",
+ "TaskCleanCollectionsAndPlaylists": "Limpiar colecciones y listas de reproducción",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Elimina elementos de colecciones y listas de reproducción que ya no existen."
}
diff --git a/Emby.Server.Implementations/Localization/Core/fil.json b/Emby.Server.Implementations/Localization/Core/fil.json
index 55ee1abaab..28c1d2be5e 100644
--- a/Emby.Server.Implementations/Localization/Core/fil.json
+++ b/Emby.Server.Implementations/Localization/Core/fil.json
@@ -69,7 +69,7 @@
"HeaderLiveTV": "Live TV",
"HeaderFavoriteSongs": "Mga Paboritong Kanta",
"HeaderFavoriteShows": "Mga Paboritong Pelikula",
- "HeaderFavoriteEpisodes": "Mga Paboritong Episode",
+ "HeaderFavoriteEpisodes": "Mga Paboritong Yugto",
"HeaderFavoriteArtists": "Mga Paboritong Artista",
"HeaderFavoriteAlbums": "Mga Paboritong Album",
"HeaderContinueWatching": "Magpatuloy sa Panonood",
diff --git a/Emby.Server.Implementations/Localization/Core/hr.json b/Emby.Server.Implementations/Localization/Core/hr.json
index 6a5b8c5615..a7dabaa19f 100644
--- a/Emby.Server.Implementations/Localization/Core/hr.json
+++ b/Emby.Server.Implementations/Localization/Core/hr.json
@@ -11,7 +11,7 @@
"Collections": "Kolekcije",
"DeviceOfflineWithName": "{0} je prekinuo vezu",
"DeviceOnlineWithName": "{0} je povezan",
- "FailedLoginAttemptWithUserName": "Neuspjeli pokušaj prijave od {0}",
+ "FailedLoginAttemptWithUserName": "Neuspješan pokušaj prijave od {0}",
"Favorites": "Favoriti",
"Folders": "Mape",
"Genres": "Žanrovi",
@@ -127,5 +127,8 @@
"HearingImpaired": "Oštećen sluh",
"TaskRefreshTrickplayImages": "Generiraj Trickplay Slike",
"TaskRefreshTrickplayImagesDescription": "Kreira trickplay pretpreglede za videe u omogućenim knjižnicama.",
- "TaskAudioNormalization": "Normalizacija zvuka"
+ "TaskAudioNormalization": "Normalizacija zvuka",
+ "TaskAudioNormalizationDescription": "Skenira datoteke u potrazi za podacima o normalizaciji zvuka.",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Uklanja stavke iz zbirki i popisa za reprodukciju koje više ne postoje.",
+ "TaskCleanCollectionsAndPlaylists": "Očisti zbirke i popise za reprodukciju"
}
diff --git a/Emby.Server.Implementations/Localization/Core/ko.json b/Emby.Server.Implementations/Localization/Core/ko.json
index b91889594b..a739cba358 100644
--- a/Emby.Server.Implementations/Localization/Core/ko.json
+++ b/Emby.Server.Implementations/Localization/Core/ko.json
@@ -125,5 +125,10 @@
"TaskKeyframeExtractor": "키프레임 추출",
"External": "외부",
"HearingImpaired": "청각 장애",
- "TaskCleanCollectionsAndPlaylists": "컬렉션과 재생목록 정리"
+ "TaskCleanCollectionsAndPlaylists": "컬렉션과 재생목록 정리",
+ "TaskAudioNormalization": "오디오의 볼륨 수준을 일정하게 조정",
+ "TaskAudioNormalizationDescription": "오디오의 볼륨 수준을 일정하게 조정하기 위해 파일을 스캔합니다.",
+ "TaskRefreshTrickplayImages": "비디오 탐색용 미리보기 썸네일 생성",
+ "TaskRefreshTrickplayImagesDescription": "활성화된 라이브러리에서 비디오의 트릭플레이 미리보기를 생성합니다.",
+ "TaskCleanCollectionsAndPlaylistsDescription": "더 이상 존재하지 않는 컬렉션 및 재생 목록에서 항목을 제거합니다."
}
diff --git a/Emby.Server.Implementations/Localization/Core/lv.json b/Emby.Server.Implementations/Localization/Core/lv.json
index 78c3d0a409..62277fd94a 100644
--- a/Emby.Server.Implementations/Localization/Core/lv.json
+++ b/Emby.Server.Implementations/Localization/Core/lv.json
@@ -76,7 +76,7 @@
"Genres": "Žanri",
"Folders": "Mapes",
"Favorites": "Izlase",
- "FailedLoginAttemptWithUserName": "Neveiksmīgs ielogošanos mēģinājums no {0}",
+ "FailedLoginAttemptWithUserName": "Neizdevies ieiešanas mēģinājums no {0}",
"DeviceOnlineWithName": "Savienojums ar {0} ir izveidots",
"DeviceOfflineWithName": "Savienojums ar {0} ir pārtraukts",
"Collections": "Kolekcijas",
@@ -95,7 +95,7 @@
"TaskRefreshChapterImages": "Izvilkt nodaļu attēlus",
"TasksApplicationCategory": "Lietotne",
"TasksLibraryCategory": "Bibliotēka",
- "TaskDownloadMissingSubtitlesDescription": "Meklē trūkstošus subtitrus internēta balstoties uz metadatu uzstādījumiem.",
+ "TaskDownloadMissingSubtitlesDescription": "Meklē internetā trūkstošos subtitrus, pamatojoties uz metadatu konfigurāciju.",
"TaskDownloadMissingSubtitles": "Lejupielādēt trūkstošos subtitrus",
"TaskRefreshChannelsDescription": "Atjauno interneta kanālu informāciju.",
"TaskRefreshChannels": "Atjaunot kanālus",
@@ -127,7 +127,7 @@
"TaskRefreshTrickplayImages": "Ģenerēt partīšanas attēlus",
"TaskRefreshTrickplayImagesDescription": "Izveido priekšskatījumus videoklipu pārtīšanai iespējotajās bibliotēkās.",
"TaskAudioNormalization": "Audio normalizācija",
- "TaskCleanCollectionsAndPlaylistsDescription": "Noņem elemēntus no kolekcijām un atskaņošanas sarakstiem, kuri vairs neeksistē.",
+ "TaskCleanCollectionsAndPlaylistsDescription": "Noņem vairs neeksistējošus vienumus no kolekcijām un atskaņošanas sarakstiem.",
"TaskAudioNormalizationDescription": "Skanē failus priekš audio normālizācijas informācijas.",
"TaskCleanCollectionsAndPlaylists": "Notīrīt kolekcijas un atskaņošanas sarakstus"
}
diff --git a/Jellyfin.Api/Controllers/DynamicHlsController.cs b/Jellyfin.Api/Controllers/DynamicHlsController.cs
index 329dd2c4cb..130c1192f7 100644
--- a/Jellyfin.Api/Controllers/DynamicHlsController.cs
+++ b/Jellyfin.Api/Controllers/DynamicHlsController.cs
@@ -1733,7 +1733,7 @@ public class DynamicHlsController : BaseJellyfinApiController
var channels = state.OutputAudioChannels;
- var useDownMixAlgorithm = state.AudioStream.Channels is 6 && _encodingOptions.DownMixStereoAlgorithm != DownMixStereoAlgorithms.None;
+ var useDownMixAlgorithm = DownMixAlgorithmsHelper.AlgorithmFilterStrings.ContainsKey((_encodingOptions.DownMixStereoAlgorithm, DownMixAlgorithmsHelper.InferChannelLayout(state.AudioStream)));
if (channels.HasValue
&& (channels.Value != 2
diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs
index 2b26c01f88..6c3d011036 100644
--- a/Jellyfin.Api/Controllers/LiveTvController.cs
+++ b/Jellyfin.Api/Controllers/LiveTvController.cs
@@ -656,7 +656,7 @@ public class LiveTvController : BaseJellyfinApiController
var query = new InternalItemsQuery(user)
{
- ChannelIds = body.ChannelIds,
+ ChannelIds = body.ChannelIds ?? [],
HasAired = body.HasAired,
IsAiring = body.IsAiring,
EnableTotalRecordCount = body.EnableTotalRecordCount,
@@ -666,31 +666,31 @@ public class LiveTvController : BaseJellyfinApiController
MaxEndDate = body.MaxEndDate,
StartIndex = body.StartIndex,
Limit = body.Limit,
- OrderBy = RequestHelpers.GetOrderBy(body.SortBy, body.SortOrder),
+ OrderBy = RequestHelpers.GetOrderBy(body.SortBy ?? [], body.SortOrder ?? []),
IsNews = body.IsNews,
IsMovie = body.IsMovie,
IsSeries = body.IsSeries,
IsKids = body.IsKids,
IsSports = body.IsSports,
SeriesTimerId = body.SeriesTimerId,
- Genres = body.Genres,
- GenreIds = body.GenreIds
+ Genres = body.Genres ?? [],
+ GenreIds = body.GenreIds ?? []
};
- if (!body.LibrarySeriesId.IsEmpty())
+ if (!body.LibrarySeriesId.IsNullOrEmpty())
{
query.IsSeries = true;
- var series = _libraryManager.GetItemById(body.LibrarySeriesId);
+ var series = _libraryManager.GetItemById(body.LibrarySeriesId.Value);
if (series is not null)
{
query.Name = series.Name;
}
}
- var dtoOptions = new DtoOptions { Fields = body.Fields }
+ var dtoOptions = new DtoOptions { Fields = body.Fields ?? [] }
.AddClientFields(User)
- .AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes);
+ .AddAdditionalDtoOptions(body.EnableImages, body.EnableUserData, body.ImageTypeLimit, body.EnableImageTypes ?? []);
return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false);
}
diff --git a/Jellyfin.Api/Extensions/DtoExtensions.cs b/Jellyfin.Api/Extensions/DtoExtensions.cs
index 3d17dbda18..f52b58babf 100644
--- a/Jellyfin.Api/Extensions/DtoExtensions.cs
+++ b/Jellyfin.Api/Extensions/DtoExtensions.cs
@@ -26,8 +26,6 @@ public static class DtoExtensions
internal static DtoOptions AddClientFields(
this DtoOptions dtoOptions, ClaimsPrincipal user)
{
- dtoOptions.Fields ??= Array.Empty();
-
string? client = user.GetClient();
// No client in claim
diff --git a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
index cb178a61d8..0690f0c8d7 100644
--- a/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
+++ b/Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs
@@ -38,7 +38,7 @@ public static class FileStreamResponseHelpers
}
// Can't dispose the response as it's required up the call chain.
- var response = await httpClient.GetAsync(new Uri(state.MediaPath), cancellationToken).ConfigureAwait(false);
+ var response = await httpClient.GetAsync(new Uri(state.MediaPath), HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
var contentType = response.Content.Headers.ContentType?.ToString() ?? MediaTypeNames.Text.Plain;
httpContext.Response.Headers[HeaderNames.AcceptRanges] = "none";
diff --git a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
index 8482b1cf1c..7210cc8f7e 100644
--- a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
+++ b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Text.Json.Serialization;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions.Json.Converters;
@@ -17,7 +18,7 @@ public class GetProgramsDto
/// Gets or sets the channels to return guide information for.
///
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList ChannelIds { get; set; } = Array.Empty();
+ public IReadOnlyList? ChannelIds { get; set; }
///
/// Gets or sets optional. Filter by user id.
@@ -26,153 +27,133 @@ public class GetProgramsDto
///
/// Gets or sets the minimum premiere start date.
- /// Optional.
///
public DateTime? MinStartDate { get; set; }
///
/// Gets or sets filter by programs that have completed airing, or not.
- /// Optional.
///
public bool? HasAired { get; set; }
///
/// Gets or sets filter by programs that are currently airing, or not.
- /// Optional.
///
public bool? IsAiring { get; set; }
///
/// Gets or sets the maximum premiere start date.
- /// Optional.
///
public DateTime? MaxStartDate { get; set; }
///
/// Gets or sets the minimum premiere end date.
- /// Optional.
///
public DateTime? MinEndDate { get; set; }
///
/// Gets or sets the maximum premiere end date.
- /// Optional.
///
public DateTime? MaxEndDate { get; set; }
///
/// Gets or sets filter for movies.
- /// Optional.
///
public bool? IsMovie { get; set; }
///
/// Gets or sets filter for series.
- /// Optional.
///
public bool? IsSeries { get; set; }
///
/// Gets or sets filter for news.
- /// Optional.
///
public bool? IsNews { get; set; }
///
/// Gets or sets filter for kids.
- /// Optional.
///
public bool? IsKids { get; set; }
///
/// Gets or sets filter for sports.
- /// Optional.
///
public bool? IsSports { get; set; }
///
/// Gets or sets the record index to start at. All items with a lower index will be dropped from the results.
- /// Optional.
///
public int? StartIndex { get; set; }
///
/// Gets or sets the maximum number of records to return.
- /// Optional.
///
public int? Limit { get; set; }
///
/// Gets or sets specify one or more sort orders, comma delimited. Options: Name, StartDate.
- /// Optional.
///
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList SortBy { get; set; } = Array.Empty();
+ public IReadOnlyList? SortBy { get; set; }
///
- /// Gets or sets sort Order - Ascending,Descending.
+ /// Gets or sets sort order.
///
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList SortOrder { get; set; } = Array.Empty();
+ public IReadOnlyList? SortOrder { get; set; }
///
/// Gets or sets the genres to return guide information for.
///
[JsonConverter(typeof(JsonPipeDelimitedArrayConverterFactory))]
- public IReadOnlyList Genres { get; set; } = Array.Empty();
+ public IReadOnlyList? Genres { get; set; }
///
/// Gets or sets the genre ids to return guide information for.
///
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList GenreIds { get; set; } = Array.Empty();
+ public IReadOnlyList? GenreIds { get; set; }
///
/// Gets or sets include image information in output.
- /// Optional.
///
public bool? EnableImages { get; set; }
///
/// Gets or sets a value indicating whether retrieve total record count.
///
+ [DefaultValue(true)]
public bool EnableTotalRecordCount { get; set; } = true;
///
/// Gets or sets the max number of images to return, per image type.
- /// Optional.
///
public int? ImageTypeLimit { get; set; }
///
/// Gets or sets the image types to include in the output.
- /// Optional.
///
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList EnableImageTypes { get; set; } = Array.Empty();
+ public IReadOnlyList? EnableImageTypes { get; set; }
///
/// Gets or sets include user data.
- /// Optional.
///
public bool? EnableUserData { get; set; }
///
/// Gets or sets filter by series timer id.
- /// Optional.
///
public string? SeriesTimerId { get; set; }
///
/// Gets or sets filter by library series id.
- /// Optional.
///
- public Guid LibrarySeriesId { get; set; }
+ public Guid? LibrarySeriesId { get; set; }
///
- /// Gets or sets specify additional fields of information to return in the output. This allows multiple, comma delimited. Options: Budget, Chapters, DateCreated, Genres, HomePageUrl, IndexOptions, MediaStreams, Overview, ParentId, Path, People, ProviderIds, PrimaryImageAspectRatio, Revenue, SortName, Studios, Taglines.
- /// Optional.
+ /// Gets or sets specify additional fields of information to return in the output.
///
[JsonConverter(typeof(JsonCommaDelimitedArrayConverterFactory))]
- public IReadOnlyList Fields { get; set; } = Array.Empty();
+ public IReadOnlyList? Fields { get; set; }
}
diff --git a/MediaBrowser.Controller/Dto/DtoOptions.cs b/MediaBrowser.Controller/Dto/DtoOptions.cs
index ecc833154d..cb638cf90b 100644
--- a/MediaBrowser.Controller/Dto/DtoOptions.cs
+++ b/MediaBrowser.Controller/Dto/DtoOptions.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
diff --git a/MediaBrowser.Controller/Entities/UserItemData.cs b/MediaBrowser.Controller/Entities/UserItemData.cs
index ecca440f05..15bd41a9c3 100644
--- a/MediaBrowser.Controller/Entities/UserItemData.cs
+++ b/MediaBrowser.Controller/Entities/UserItemData.cs
@@ -1,5 +1,3 @@
-#nullable disable
-
#pragma warning disable CS1591
using System;
@@ -19,17 +17,11 @@ namespace MediaBrowser.Controller.Entities
///
private double? _rating;
- ///
- /// Gets or sets the user id.
- ///
- /// The user id.
- public Guid UserId { get; set; }
-
///
/// Gets or sets the key.
///
/// The key.
- public string Key { get; set; }
+ public required string Key { get; set; }
///
/// Gets or sets the users 0-10 rating.
diff --git a/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs b/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs
new file mode 100644
index 0000000000..749f872710
--- /dev/null
+++ b/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs
@@ -0,0 +1,74 @@
+using System.Collections.Generic;
+using MediaBrowser.Model.Entities;
+
+namespace MediaBrowser.Controller.MediaEncoding;
+
+///
+/// Describes the downmix algorithms capabilities.
+///
+public static class DownMixAlgorithmsHelper
+{
+ ///
+ /// The filter string of the DownMixStereoAlgorithms.
+ /// The index is the tuple of (algorithm, layout).
+ ///
+ public static readonly Dictionary<(DownMixStereoAlgorithms, string), string> AlgorithmFilterStrings = new()
+ {
+ { (DownMixStereoAlgorithms.Dave750, "5.1"), "pan=stereo|c0=0.5*c2+0.707*c0+0.707*c4+0.5*c3|c1=0.5*c2+0.707*c1+0.707*c5+0.5*c3" },
+ // Use AC-4 algorithm to downmix 7.1 inputs to 5.1 first
+ { (DownMixStereoAlgorithms.Dave750, "7.1"), "pan=5.1(side)|c0=c0|c1=c1|c2=c2|c3=c3|c4=0.707*c4+0.707*c6|c5=0.707*c5+0.707*c7,pan=stereo|c0=0.5*c2+0.707*c0+0.707*c4+0.5*c3|c1=0.5*c2+0.707*c1+0.707*c5+0.5*c3" },
+ { (DownMixStereoAlgorithms.NightmodeDialogue, "5.1"), "pan=stereo|c0=c2+0.30*c0+0.30*c4|c1=c2+0.30*c1+0.30*c5" },
+ // Use AC-4 algorithm to downmix 7.1 inputs to 5.1 first
+ { (DownMixStereoAlgorithms.NightmodeDialogue, "7.1"), "pan=5.1(side)|c0=c0|c1=c1|c2=c2|c3=c3|c4=0.707*c4+0.707*c6|c5=0.707*c5+0.707*c7,pan=stereo|c0=c2+0.30*c0+0.30*c4|c1=c2+0.30*c1+0.30*c5" },
+ { (DownMixStereoAlgorithms.Rfc7845, "3.0"), "pan=stereo|c0=0.414214*c2+0.585786*c0|c1=0.414214*c2+0.585786*c1" },
+ { (DownMixStereoAlgorithms.Rfc7845, "quad"), "pan=stereo|c0=0.422650*c0+0.366025*c2+0.211325*c3|c1=0.422650*c1+0.366025*c3+0.211325*c2" },
+ { (DownMixStereoAlgorithms.Rfc7845, "5.0"), "pan=stereo|c0=0.460186*c2+0.650802*c0+0.563611*c3+0.325401*c4|c1=0.460186*c2+0.650802*c1+0.563611*c4+0.325401*c3" },
+ { (DownMixStereoAlgorithms.Rfc7845, "5.1"), "pan=stereo|c0=0.374107*c2+0.529067*c0+0.458186*c4+0.264534*c5+0.374107*c3|c1=0.374107*c2+0.529067*c1+0.458186*c5+0.264534*c4+0.374107*c3" },
+ { (DownMixStereoAlgorithms.Rfc7845, "6.1"), "pan=stereo|c0=0.321953*c2+0.455310*c0+0.394310*c5+0.227655*c6+0.278819*c4+0.321953*c3|c1=0.321953*c2+0.455310*c1+0.394310*c6+0.227655*c5+0.278819*c4+0.321953*c3" },
+ { (DownMixStereoAlgorithms.Rfc7845, "7.1"), "pan=stereo|c0=0.274804*c2+0.388631*c0+0.336565*c6+0.194316*c7+0.336565*c4+0.194316*c5+0.274804*c3|c1=0.274804*c2+0.388631*c1+0.336565*c7+0.194316*c6+0.336565*c5+0.194316*c4+0.274804*c3" },
+ { (DownMixStereoAlgorithms.Ac4, "3.0"), "pan=stereo|c0=c0+0.707*c2|c1=c1+0.707*c2" },
+ { (DownMixStereoAlgorithms.Ac4, "5.0"), "pan=stereo|c0=c0+0.707*c2+0.707*c3|c1=c1+0.707*c2+0.707*c4" },
+ { (DownMixStereoAlgorithms.Ac4, "5.1"), "pan=stereo|c0=c0+0.707*c2+0.707*c4|c1=c1+0.707*c2+0.707*c5" },
+ { (DownMixStereoAlgorithms.Ac4, "7.0"), "pan=5.0(side)|c0=c0|c1=c1|c2=c2|c3=0.707*c3+0.707*c5|c4=0.707*c4+0.707*c6,pan=stereo|c0=c0+0.707*c2+0.707*c3|c1=c1+0.707*c2+0.707*c4" },
+ { (DownMixStereoAlgorithms.Ac4, "7.1"), "pan=5.1(side)|c0=c0|c1=c1|c2=c2|c3=c3|c4=0.707*c4+0.707*c6|c5=0.707*c5+0.707*c7,pan=stereo|c0=c0+0.707*c2+0.707*c4|c1=c1+0.707*c2+0.707*c5" },
+ };
+
+ ///
+ /// Get the audio channel layout string from the audio stream
+ /// If the input audio string does not have a valid layout string, guess from channel count.
+ ///
+ /// The audio stream to get layout.
+ /// Channel Layout string.
+ public static string InferChannelLayout(MediaStream audioStream)
+ {
+ if (!string.IsNullOrWhiteSpace(audioStream.ChannelLayout))
+ {
+ // Note: BDMVs do not derive this string from ffmpeg, which would cause ambiguity with 4-channel audio
+ // "quad" => 2 front and 2 rear, "4.0" => 3 front and 1 rear
+ // BDMV will always use "4.0" in this case
+ // Because the quad layout is super rare in BDs, we will use "4.0" as is here
+ return audioStream.ChannelLayout;
+ }
+
+ if (audioStream.Channels is null)
+ {
+ return string.Empty;
+ }
+
+ // When we don't have definitive channel layout, we have to guess from the channel count
+ // Guessing is not always correct, but for most videos we don't have to guess like this as the definitive layout is recorded during scan
+ var inferredLayout = audioStream.Channels.Value switch
+ {
+ 1 => "mono",
+ 2 => "stereo",
+ 3 => "2.1", // Could also be 3.0, prefer 2.1
+ 4 => "4.0", // Could also be quad (with rear left and rear right) and 3.1 with LFE. prefer 4.0 with front center and back center
+ 5 => "5.0",
+ 6 => "5.1", // Could also be 6.0 or hexagonal, prefer 5.1
+ 7 => "6.1", // Could also be 7.0, prefer 6.1
+ 8 => "7.1", // Could also be 8.0, prefer 7.1
+ _ => string.Empty // Return empty string for not supported layout
+ };
+ return inferredLayout;
+ }
+}
diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
index eb80bab2da..c068cf0559 100644
--- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
+++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs
@@ -64,6 +64,7 @@ namespace MediaBrowser.Controller.MediaEncoding
private readonly Version _minFFmpegSvtAv1Params = new Version(5, 1);
private readonly Version _minFFmpegVaapiH26xEncA53CcSei = new Version(6, 0);
private readonly Version _minFFmpegReadrateOption = new Version(5, 0);
+ private readonly Version _minFFmpegWorkingVtHwSurface = new Version(7, 0, 1);
private readonly Version _minFFmpegDisplayRotationOption = new Version(6, 0);
private static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled);
@@ -2673,28 +2674,17 @@ namespace MediaBrowser.Controller.MediaEncoding
var filters = new List();
- if (channels.HasValue
- && channels.Value == 2
- && state.AudioStream is not null
- && state.AudioStream.Channels.HasValue
- && state.AudioStream.Channels.Value == 6)
+ if (channels is 2 && state.AudioStream?.Channels is > 2)
{
- if (!encodingOptions.DownMixAudioBoost.Equals(1))
+ var hasDownMixFilter = DownMixAlgorithmsHelper.AlgorithmFilterStrings.TryGetValue((encodingOptions.DownMixStereoAlgorithm, DownMixAlgorithmsHelper.InferChannelLayout(state.AudioStream)), out var downMixFilterString);
+ if (hasDownMixFilter)
{
- filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture));
+ filters.Add(downMixFilterString);
}
- switch (encodingOptions.DownMixStereoAlgorithm)
+ if (!encodingOptions.DownMixAudioBoost.Equals(1))
{
- case DownMixStereoAlgorithms.Dave750:
- filters.Add("pan=stereo|c0=0.5*c2+0.707*c0+0.707*c4+0.5*c3|c1=0.5*c2+0.707*c1+0.707*c5+0.5*c3");
- break;
- case DownMixStereoAlgorithms.NightmodeDialogue:
- filters.Add("pan=stereo|c0=c2+0.30*c0+0.30*c4|c1=c2+0.30*c1+0.30*c5");
- break;
- case DownMixStereoAlgorithms.None:
- default:
- break;
+ filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture));
}
}
@@ -5300,6 +5290,7 @@ namespace MediaBrowser.Controller.MediaEncoding
string vidEncoder)
{
var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
+ var isVtDecoder = vidDecoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase);
if (!isVtEncoder)
{
@@ -5320,6 +5311,7 @@ namespace MediaBrowser.Controller.MediaEncoding
var doDeintH2645 = doDeintH264 || doDeintHevc;
var doVtTonemap = IsVideoToolboxTonemapAvailable(state, options);
var doMetalTonemap = !doVtTonemap && IsHwTonemapAvailable(state, options);
+ var usingHwSurface = isVtDecoder && (_mediaEncoder.EncoderVersion >= _minFFmpegWorkingVtHwSurface);
var rotation = state.VideoStream?.Rotation ?? 0;
var tranposeDir = rotation == 0 ? string.Empty : GetVideoTransposeDirection(state);
@@ -5414,23 +5406,25 @@ namespace MediaBrowser.Controller.MediaEncoding
subFilters.Add(subTextSubtitlesFilter);
}
- subFilters.Add("hwupload=derive_device=videotoolbox");
+ subFilters.Add("hwupload");
overlayFilters.Add("overlay_videotoolbox=eof_action=pass:repeatlast=0");
}
+ if (usingHwSurface)
+ {
+ return (mainFilters, subFilters, overlayFilters);
+ }
+
+ // For old jellyfin-ffmpeg that has broken hwsurface, add a hwupload
var needFiltering = mainFilters.Any(f => !string.IsNullOrEmpty(f)) ||
subFilters.Any(f => !string.IsNullOrEmpty(f)) ||
overlayFilters.Any(f => !string.IsNullOrEmpty(f));
-
- // This is a workaround for ffmpeg's hwupload implementation
- // For VideoToolbox encoders, a hwupload without a valid filter actually consuming its frame
- // will cause the encoder to produce incorrect frames.
if (needFiltering)
{
// INPUT videotoolbox/memory surface(vram/uma)
// this will pass-through automatically if in/out format matches.
mainFilters.Insert(0, "format=nv12|p010le|videotoolbox_vld");
- mainFilters.Insert(0, "hwupload=derive_device=videotoolbox");
+ mainFilters.Insert(0, "hwupload");
}
return (mainFilters, subFilters, overlayFilters);
@@ -6458,22 +6452,20 @@ namespace MediaBrowser.Controller.MediaEncoding
|| string.Equals("yuvj420p", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
var is8_10bitSwFormatsVt = is8bitSwFormatsVt || string.Equals("yuv420p10le", videoStream.PixelFormat, StringComparison.OrdinalIgnoreCase);
- // VideoToolbox's Hardware surface in ffmpeg is not only slower than hwupload, but also breaks HDR in many cases.
- // For example: https://trac.ffmpeg.org/ticket/10884
- // Disable it for now.
- const bool UseHwSurface = false;
+ // The related patches make videotoolbox hardware surface working is only available in jellyfin-ffmpeg 7.0.1 at the moment.
+ bool useHwSurface = (_mediaEncoder.EncoderVersion >= _minFFmpegWorkingVtHwSurface) && IsVideoToolboxFullSupported();
if (is8bitSwFormatsVt)
{
if (string.Equals("avc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
|| string.Equals("h264", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
- return GetHwaccelType(state, options, "h264", bitDepth, UseHwSurface);
+ return GetHwaccelType(state, options, "h264", bitDepth, useHwSurface);
}
if (string.Equals("vp8", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
- return GetHwaccelType(state, options, "vp8", bitDepth, UseHwSurface);
+ return GetHwaccelType(state, options, "vp8", bitDepth, useHwSurface);
}
}
@@ -6482,12 +6474,12 @@ namespace MediaBrowser.Controller.MediaEncoding
if (string.Equals("hevc", videoStream.Codec, StringComparison.OrdinalIgnoreCase)
|| string.Equals("h265", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
- return GetHwaccelType(state, options, "hevc", bitDepth, UseHwSurface);
+ return GetHwaccelType(state, options, "hevc", bitDepth, useHwSurface);
}
if (string.Equals("vp9", videoStream.Codec, StringComparison.OrdinalIgnoreCase))
{
- return GetHwaccelType(state, options, "vp9", bitDepth, UseHwSurface);
+ return GetHwaccelType(state, options, "vp9", bitDepth, useHwSurface);
}
}
@@ -7172,7 +7164,10 @@ namespace MediaBrowser.Controller.MediaEncoding
var channels = state.OutputAudioChannels;
- if (channels.HasValue && ((channels.Value != 2 && state.AudioStream?.Channels != 6) || encodingOptions.DownMixStereoAlgorithm == DownMixStereoAlgorithms.None))
+ var useDownMixAlgorithm = state.AudioStream is not null
+ && DownMixAlgorithmsHelper.AlgorithmFilterStrings.ContainsKey((encodingOptions.DownMixStereoAlgorithm, DownMixAlgorithmsHelper.InferChannelLayout(state.AudioStream)));
+
+ if (channels.HasValue && !useDownMixAlgorithm)
{
args += " -ac " + channels.Value;
}
diff --git a/MediaBrowser.Controller/Providers/MetadataResult.cs b/MediaBrowser.Controller/Providers/MetadataResult.cs
index 952dd48703..cfff3eb144 100644
--- a/MediaBrowser.Controller/Providers/MetadataResult.cs
+++ b/MediaBrowser.Controller/Providers/MetadataResult.cs
@@ -2,9 +2,7 @@
#pragma warning disable CA1002, CA2227, CS1591
-using System;
using System.Collections.Generic;
-using System.Globalization;
using MediaBrowser.Controller.Entities;
using MediaBrowser.Model.Entities;
@@ -33,8 +31,6 @@ namespace MediaBrowser.Controller.Providers
set => _remoteImages = value;
}
- public List UserDataList { get; set; }
-
public List People { get; set; }
public bool HasMetadata { get; set; }
@@ -68,32 +64,5 @@ namespace MediaBrowser.Controller.Providers
People.Clear();
}
}
-
- public UserItemData GetOrAddUserData(string userId)
- {
- UserDataList ??= new List();
-
- UserItemData userData = null;
-
- foreach (var i in UserDataList)
- {
- if (string.Equals(userId, i.UserId.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase))
- {
- userData = i;
- }
- }
-
- if (userData is null)
- {
- userData = new UserItemData()
- {
- UserId = new Guid(userId)
- };
-
- UserDataList.Add(userData);
- }
-
- return userData;
- }
}
}
diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
index a7e027d94a..e4ac59b676 100644
--- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
+++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs
@@ -365,10 +365,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
break;
case "CollectionNumber":
var tmdbCollection = reader.ReadNormalizedString();
- if (!string.IsNullOrEmpty(tmdbCollection))
- {
- item.SetProviderId(MetadataProvider.TmdbCollection, tmdbCollection);
- }
+ item.TrySetProviderId(MetadataProvider.TmdbCollection, tmdbCollection);
break;
@@ -502,10 +499,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
if (_validProviderIds!.TryGetValue(readerName, out string? providerIdValue))
{
var id = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(id))
- {
- item.SetProviderId(providerIdValue, id);
- }
+ item.TrySetProviderId(providerIdValue, id);
}
else
{
diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
index f684069102..334796f585 100644
--- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
+++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs
@@ -1325,38 +1325,23 @@ namespace MediaBrowser.MediaEncoding.Probing
// These support multiple values, but for now we only store the first.
var mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Album Artist Id"))
?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_ALBUMARTISTID"));
- if (!string.IsNullOrEmpty(mb))
- {
- audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, mb);
- }
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, mb);
mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Artist Id"))
?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_ARTISTID"));
- if (!string.IsNullOrEmpty(mb))
- {
- audio.SetProviderId(MetadataProvider.MusicBrainzArtist, mb);
- }
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, mb);
mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Album Id"))
?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_ALBUMID"));
- if (!string.IsNullOrEmpty(mb))
- {
- audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, mb);
- }
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, mb);
mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Release Group Id"))
?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_RELEASEGROUPID"));
- if (!string.IsNullOrEmpty(mb))
- {
- audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, mb);
- }
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, mb);
mb = GetMultipleMusicBrainzId(tags.GetValueOrDefault("MusicBrainz Release Track Id"))
?? GetMultipleMusicBrainzId(tags.GetValueOrDefault("MUSICBRAINZ_RELEASETRACKID"));
- if (!string.IsNullOrEmpty(mb))
- {
- audio.SetProviderId(MetadataProvider.MusicBrainzTrack, mb);
- }
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, mb);
}
private string GetMultipleMusicBrainzId(string value)
diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs
index d37528ede8..d2715e2ac7 100644
--- a/MediaBrowser.Model/Dlna/StreamBuilder.cs
+++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs
@@ -908,7 +908,18 @@ namespace MediaBrowser.Model.Dlna
}
}
- var directAudioStream = candidateAudioStreams.FirstOrDefault(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec));
+ var audioStreamWithSupportedCodec = candidateAudioStreams.Where(stream => ContainerProfile.ContainsContainer(audioCodecs, stream.Codec)).FirstOrDefault();
+
+ var directAudioStream = audioStreamWithSupportedCodec?.Channels is not null && audioStreamWithSupportedCodec.Channels.Value <= (playlistItem.TranscodingMaxAudioChannels ?? int.MaxValue) ? audioStreamWithSupportedCodec : null;
+
+ var channelsExceedsLimit = audioStreamWithSupportedCodec is not null && directAudioStream is null;
+
+ if (channelsExceedsLimit && playlistItem.TargetAudioStream is not null)
+ {
+ playlistItem.TranscodeReasons |= TranscodeReason.AudioChannelsNotSupported;
+ playlistItem.TargetAudioStream.Channels = playlistItem.TranscodingMaxAudioChannels;
+ }
+
playlistItem.AudioCodecs = audioCodecs;
if (directAudioStream is not null)
{
@@ -971,7 +982,7 @@ namespace MediaBrowser.Model.Dlna
}
// Honor requested max channels
- playlistItem.GlobalMaxAudioChannels = options.MaxAudioChannels;
+ playlistItem.GlobalMaxAudioChannels = channelsExceedsLimit ? playlistItem.TranscodingMaxAudioChannels : options.MaxAudioChannels;
int audioBitrate = GetAudioBitrate(options.GetMaxBitrate(true) ?? 0, playlistItem.TargetAudioCodec, audioStream, playlistItem);
playlistItem.AudioBitrate = Math.Min(playlistItem.AudioBitrate ?? audioBitrate, audioBitrate);
@@ -1293,7 +1304,7 @@ namespace MediaBrowser.Model.Dlna
// Check audio codec
MediaStream? selectedAudioStream = null;
- if (candidateAudioStreams.Any())
+ if (candidateAudioStreams.Count != 0)
{
selectedAudioStream = candidateAudioStreams.FirstOrDefault(audioStream => directPlayProfile.SupportsAudioCodec(audioStream.Codec));
if (selectedAudioStream is null)
diff --git a/MediaBrowser.Model/Dto/UserItemDataDto.cs b/MediaBrowser.Model/Dto/UserItemDataDto.cs
index adb2cd2ab3..3bb45a0e04 100644
--- a/MediaBrowser.Model/Dto/UserItemDataDto.cs
+++ b/MediaBrowser.Model/Dto/UserItemDataDto.cs
@@ -1,4 +1,3 @@
-#nullable disable
using System;
namespace MediaBrowser.Model.Dto
@@ -66,12 +65,12 @@ namespace MediaBrowser.Model.Dto
/// Gets or sets the key.
///
/// The key.
- public string Key { get; set; }
+ public required string Key { get; set; }
///
/// Gets or sets the item identifier.
///
/// The item identifier.
- public string ItemId { get; set; }
+ public Guid ItemId { get; set; }
}
}
diff --git a/MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs b/MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs
index 385cd6a34e..0c03d430dc 100644
--- a/MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs
+++ b/MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs
@@ -1,8 +1,7 @@
namespace MediaBrowser.Model.Entities;
///
-/// An enum representing an algorithm to downmix 6ch+ to stereo.
-/// Algorithms sourced from https://superuser.com/questions/852400/properly-downmix-5-1-to-stereo-using-ffmpeg/1410620#1410620.
+/// An enum representing an algorithm to downmix surround sound to stereo.
///
public enum DownMixStereoAlgorithms
{
@@ -13,11 +12,24 @@ public enum DownMixStereoAlgorithms
///
/// Algorithm by Dave_750.
+ /// Sourced from https://superuser.com/questions/852400/properly-downmix-5-1-to-stereo-using-ffmpeg/1410620#1410620.
///
Dave750 = 1,
///
/// Nightmode Dialogue algorithm.
+ /// Sourced from https://superuser.com/questions/852400/properly-downmix-5-1-to-stereo-using-ffmpeg/1410620#1410620.
///
- NightmodeDialogue = 2
+ NightmodeDialogue = 2,
+
+ ///
+ /// RFC7845 Section 5.1.1.5 defined algorithm.
+ ///
+ Rfc7845 = 3,
+
+ ///
+ /// AC-4 standard algorithm with its default gain values.
+ /// Defined in ETSI TS 103 190 Section 6.2.17.
+ ///
+ Ac4 = 4
}
diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs
index 844214fae2..7e227c5aa1 100644
--- a/MediaBrowser.Model/Entities/MediaStream.cs
+++ b/MediaBrowser.Model/Entities/MediaStream.cs
@@ -202,7 +202,7 @@ namespace MediaBrowser.Model.Entities
|| dvProfile == 8
|| dvProfile == 9))
{
- var title = "DV Profile " + dvProfile;
+ var title = "Dolby Vision Profile " + dvProfile;
if (dvBlCompatId > 0)
{
@@ -214,6 +214,7 @@ namespace MediaBrowser.Model.Entities
1 => title + " (HDR10)",
2 => title + " (SDR)",
4 => title + " (HLG)",
+ 6 => title + " (HDR10)", // Technically means Blu-ray, but practically always HDR10
_ => title
};
}
@@ -336,7 +337,11 @@ namespace MediaBrowser.Model.Entities
attributes.Add(Codec.ToUpperInvariant());
}
- if (VideoRange != VideoRange.Unknown)
+ if (VideoDoViTitle is not null)
+ {
+ attributes.Add(VideoDoViTitle);
+ }
+ else if (VideoRange != VideoRange.Unknown)
{
attributes.Add(VideoRange.ToString());
}
diff --git a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
index 1c73091f0d..479ec7712d 100644
--- a/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
+++ b/MediaBrowser.Model/Entities/ProviderIdsExtensions.cs
@@ -3,177 +3,214 @@ using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
-namespace MediaBrowser.Model.Entities
+namespace MediaBrowser.Model.Entities;
+
+///
+/// Class ProviderIdsExtensions.
+///
+public static class ProviderIdsExtensions
{
///
- /// Class ProviderIdsExtensions.
+ /// Case insensitive dictionary of string representation.
+ ///
+ private static readonly Dictionary _metadataProviderEnumDictionary =
+ Enum.GetValues()
+ .ToDictionary(
+ enumValue => enumValue.ToString(),
+ enumValue => enumValue.ToString(),
+ StringComparer.OrdinalIgnoreCase);
+
+ ///
+ /// Checks if this instance has an id for the given provider.
+ ///
+ /// The instance.
+ /// The of the provider name.
+ /// true if a provider id with the given name was found; otherwise false.
+ public static bool HasProviderId(this IHasProviderIds instance, string name)
+ => instance.TryGetProviderId(name, out _);
+
+ ///
+ /// Checks if this instance has an id for the given provider.
+ ///
+ /// The instance.
+ /// The provider.
+ /// true if a provider id with the given name was found; otherwise false.
+ public static bool HasProviderId(this IHasProviderIds instance, MetadataProvider provider)
+ => instance.HasProviderId(provider.ToString());
+
+ ///
+ /// Gets a provider id.
///
- public static class ProviderIdsExtensions
+ /// The instance.
+ /// The name.
+ /// The provider id.
+ /// true if a provider id with the given name was found; otherwise false.
+ public static bool TryGetProviderId(this IHasProviderIds instance, string name, [NotNullWhen(true)] out string? id)
{
- ///
- /// Case insensitive dictionary of string representation.
- ///
- private static readonly Dictionary _metadataProviderEnumDictionary =
- Enum.GetValues()
- .ToDictionary(
- enumValue => enumValue.ToString(),
- enumValue => enumValue.ToString(),
- StringComparer.OrdinalIgnoreCase);
-
- ///
- /// Checks if this instance has an id for the given provider.
- ///
- /// The instance.
- /// The of the provider name.
- /// true if a provider id with the given name was found; otherwise false.
- public static bool HasProviderId(this IHasProviderIds instance, string name)
- {
- ArgumentNullException.ThrowIfNull(instance);
+ ArgumentNullException.ThrowIfNull(instance);
- return instance.TryGetProviderId(name, out _);
+ if (instance.ProviderIds is null)
+ {
+ id = null;
+ return false;
}
- ///
- /// Checks if this instance has an id for the given provider.
- ///
- /// The instance.
- /// The provider.
- /// true if a provider id with the given name was found; otherwise false.
- public static bool HasProviderId(this IHasProviderIds instance, MetadataProvider provider)
+ var foundProviderId = instance.ProviderIds.TryGetValue(name, out id);
+ // This occurs when searching with Identify (and possibly in other places)
+ if (string.IsNullOrEmpty(id))
{
- return instance.HasProviderId(provider.ToString());
+ id = null;
+ foundProviderId = false;
}
- ///
- /// Gets a provider id.
- ///
- /// The instance.
- /// The name.
- /// The provider id.
- /// true if a provider id with the given name was found; otherwise false.
- public static bool TryGetProviderId(this IHasProviderIds instance, string name, [NotNullWhen(true)] out string? id)
+ return foundProviderId;
+ }
+
+ ///
+ /// Gets a provider id.
+ ///
+ /// The instance.
+ /// The provider.
+ /// The provider id.
+ /// true if a provider id with the given name was found; otherwise false.
+ public static bool TryGetProviderId(this IHasProviderIds instance, MetadataProvider provider, [NotNullWhen(true)] out string? id)
+ {
+ return instance.TryGetProviderId(provider.ToString(), out id);
+ }
+
+ ///
+ /// Gets a provider id.
+ ///
+ /// The instance.
+ /// The name.
+ /// System.String.
+ public static string? GetProviderId(this IHasProviderIds instance, string name)
+ {
+ instance.TryGetProviderId(name, out string? id);
+ return id;
+ }
+
+ ///
+ /// Gets a provider id.
+ ///
+ /// The instance.
+ /// The provider.
+ /// System.String.
+ public static string? GetProviderId(this IHasProviderIds instance, MetadataProvider provider)
+ {
+ return instance.GetProviderId(provider.ToString());
+ }
+
+ ///
+ /// Sets a provider id.
+ ///
+ /// The instance.
+ /// The name, this should not contain a '=' character.
+ /// The value.
+ /// Due to how deserialization from the database works the name can not contain '='.
+ /// true if the provider id got set successfully; otherwise, false.
+ public static bool TrySetProviderId(this IHasProviderIds instance, string? name, string? value)
+ {
+ ArgumentNullException.ThrowIfNull(instance);
+
+ // When name contains a '=' it can't be deserialized from the database
+ if (string.IsNullOrWhiteSpace(name)
+ || string.IsNullOrWhiteSpace(value)
+ || name.Contains('=', StringComparison.Ordinal))
{
- ArgumentNullException.ThrowIfNull(instance);
-
- if (instance.ProviderIds is null)
- {
- id = null;
- return false;
- }
-
- var foundProviderId = instance.ProviderIds.TryGetValue(name, out id);
- // This occurs when searching with Identify (and possibly in other places)
- if (string.IsNullOrEmpty(id))
- {
- id = null;
- foundProviderId = false;
- }
-
- return foundProviderId;
+ return false;
}
- ///
- /// Gets a provider id.
- ///
- /// The instance.
- /// The provider.
- /// The provider id.
- /// true if a provider id with the given name was found; otherwise false.
- public static bool TryGetProviderId(this IHasProviderIds instance, MetadataProvider provider, [NotNullWhen(true)] out string? id)
+ // Ensure it exists
+ instance.ProviderIds ??= new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ // Match on internal MetadataProvider enum string values before adding arbitrary providers
+ if (_metadataProviderEnumDictionary.TryGetValue(name, out var enumValue))
{
- return instance.TryGetProviderId(provider.ToString(), out id);
+ instance.ProviderIds[enumValue] = value;
}
-
- ///
- /// Gets a provider id.
- ///
- /// The instance.
- /// The name.
- /// System.String.
- public static string? GetProviderId(this IHasProviderIds instance, string name)
+ else
{
- instance.TryGetProviderId(name, out string? id);
- return id;
+ instance.ProviderIds[name] = value;
}
- ///
- /// Gets a provider id.
- ///
- /// The instance.
- /// The provider.
- /// System.String.
- public static string? GetProviderId(this IHasProviderIds instance, MetadataProvider provider)
+ return true;
+ }
+
+ ///
+ /// Sets a provider id.
+ ///
+ /// The instance.
+ /// The provider.
+ /// The value.
+ /// true if the provider id got set successfully; otherwise, false.
+ public static bool TrySetProviderId(this IHasProviderIds instance, MetadataProvider provider, string? value)
+ => instance.TrySetProviderId(provider.ToString(), value);
+
+ ///
+ /// Sets a provider id.
+ ///
+ /// The instance.
+ /// The name, this should not contain a '=' character.
+ /// The value.
+ /// Due to how deserialization from the database works the name can not contain '='.
+ public static void SetProviderId(this IHasProviderIds instance, string name, string value)
+ {
+ ArgumentNullException.ThrowIfNull(instance);
+ ArgumentException.ThrowIfNullOrWhiteSpace(name);
+ ArgumentException.ThrowIfNullOrWhiteSpace(value);
+
+ // When name contains a '=' it can't be deserialized from the database
+ if (name.Contains('=', StringComparison.Ordinal))
{
- return instance.GetProviderId(provider.ToString());
+ throw new ArgumentException("Provider id name cannot contain '='", nameof(name));
}
- ///
- /// Sets a provider id.
- ///
- /// The instance.
- /// The name, this should not contain a '=' character.
- /// The value.
- /// Due to how deserialization from the database works the name can not contain '='.
- public static void SetProviderId(this IHasProviderIds instance, string name, string value)
+ // Ensure it exists
+ instance.ProviderIds ??= new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ // Match on internal MetadataProvider enum string values before adding arbitrary providers
+ if (_metadataProviderEnumDictionary.TryGetValue(name, out var enumValue))
{
- ArgumentNullException.ThrowIfNull(instance);
- ArgumentException.ThrowIfNullOrEmpty(name);
- ArgumentException.ThrowIfNullOrEmpty(value);
-
- // When name contains a '=' it can't be deserialized from the database
- if (name.Contains('=', StringComparison.Ordinal))
- {
- throw new ArgumentException("Provider id name cannot contain '='", nameof(name));
- }
-
- // Ensure it exists
- instance.ProviderIds ??= new Dictionary(StringComparer.OrdinalIgnoreCase);
-
- // Match on internal MetadataProvider enum string values before adding arbitrary providers
- if (_metadataProviderEnumDictionary.TryGetValue(name, out var enumValue))
- {
- instance.ProviderIds[enumValue] = value;
- }
- else
- {
- instance.ProviderIds[name] = value;
- }
+ instance.ProviderIds[enumValue] = value;
}
-
- ///
- /// Sets a provider id.
- ///
- /// The instance.
- /// The provider.
- /// The value.
- public static void SetProviderId(this IHasProviderIds instance, MetadataProvider provider, string value)
+ else
{
- instance.SetProviderId(provider.ToString(), value);
+ instance.ProviderIds[name] = value;
}
+ }
- ///
- /// Removes a provider id.
- ///
- /// The instance.
- /// The name.
- public static void RemoveProviderId(this IHasProviderIds instance, string name)
- {
- ArgumentNullException.ThrowIfNull(instance);
- ArgumentException.ThrowIfNullOrEmpty(name);
+ ///
+ /// Sets a provider id.
+ ///
+ /// The instance.
+ /// The provider.
+ /// The value.
+ public static void SetProviderId(this IHasProviderIds instance, MetadataProvider provider, string value)
+ => instance.SetProviderId(provider.ToString(), value);
- instance.ProviderIds?.Remove(name);
- }
+ ///
+ /// Removes a provider id.
+ ///
+ /// The instance.
+ /// The name.
+ public static void RemoveProviderId(this IHasProviderIds instance, string name)
+ {
+ ArgumentNullException.ThrowIfNull(instance);
+ ArgumentException.ThrowIfNullOrEmpty(name);
- ///
- /// Removes a provider id.
- ///
- /// The instance.
- /// The provider.
- public static void RemoveProviderId(this IHasProviderIds instance, MetadataProvider provider)
- {
- ArgumentNullException.ThrowIfNull(instance);
+ instance.ProviderIds?.Remove(name);
+ }
- instance.ProviderIds?.Remove(provider.ToString());
- }
+ ///
+ /// Removes a provider id.
+ ///
+ /// The instance.
+ /// The provider.
+ public static void RemoveProviderId(this IHasProviderIds instance, MetadataProvider provider)
+ {
+ ArgumentNullException.ThrowIfNull(instance);
+
+ instance.ProviderIds?.Remove(provider.ToString());
}
}
diff --git a/MediaBrowser.Model/Querying/QueryResult.cs b/MediaBrowser.Model/Querying/QueryResult.cs
index ea843f34cd..dd0d4fbfcf 100644
--- a/MediaBrowser.Model/Querying/QueryResult.cs
+++ b/MediaBrowser.Model/Querying/QueryResult.cs
@@ -1,47 +1,60 @@
-#nullable disable
-#pragma warning disable CS1591
-
using System;
using System.Collections.Generic;
-namespace MediaBrowser.Model.Querying
+namespace MediaBrowser.Model.Querying;
+
+///
+/// Query result container.
+///
+/// The type of item contained in the query result.
+public class QueryResult
{
- public class QueryResult
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public QueryResult()
{
- public QueryResult()
- {
- Items = Array.Empty();
- }
+ Items = Array.Empty();
+ }
- public QueryResult(IReadOnlyList items)
- {
- Items = items;
- TotalRecordCount = items.Count;
- }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The list of items.
+ public QueryResult(IReadOnlyList items)
+ {
+ Items = items;
+ TotalRecordCount = items.Count;
+ }
- public QueryResult(int? startIndex, int? totalRecordCount, IReadOnlyList items)
- {
- StartIndex = startIndex ?? 0;
- TotalRecordCount = totalRecordCount ?? items.Count;
- Items = items;
- }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The start index that was used to build the item list.
+ /// The total count of items.
+ /// The list of items.
+ public QueryResult(int? startIndex, int? totalRecordCount, IReadOnlyList items)
+ {
+ StartIndex = startIndex ?? 0;
+ TotalRecordCount = totalRecordCount ?? items.Count;
+ Items = items;
+ }
- ///
- /// Gets or sets the items.
- ///
- /// The items.
- public IReadOnlyList Items { get; set; }
+ ///
+ /// Gets or sets the items.
+ ///
+ /// The items.
+ public IReadOnlyList Items { get; set; }
- ///
- /// Gets or sets the total number of records available.
- ///
- /// The total record count.
- public int TotalRecordCount { get; set; }
+ ///
+ /// Gets or sets the total number of records available.
+ ///
+ /// The total record count.
+ public int TotalRecordCount { get; set; }
- ///
- /// Gets or sets the index of the first record in Items.
- ///
- /// First record index.
- public int StartIndex { get; set; }
- }
+ ///
+ /// Gets or sets the index of the first record in Items.
+ ///
+ /// First record index.
+ public int StartIndex { get; set; }
}
diff --git a/MediaBrowser.Model/Session/UserDataChangeInfo.cs b/MediaBrowser.Model/Session/UserDataChangeInfo.cs
index 0fd24edccd..ccd768da51 100644
--- a/MediaBrowser.Model/Session/UserDataChangeInfo.cs
+++ b/MediaBrowser.Model/Session/UserDataChangeInfo.cs
@@ -1,4 +1,4 @@
-#nullable disable
+using System;
using MediaBrowser.Model.Dto;
namespace MediaBrowser.Model.Session
@@ -12,12 +12,12 @@ namespace MediaBrowser.Model.Session
/// Gets or sets the user id.
///
/// The user id.
- public string UserId { get; set; }
+ public Guid UserId { get; set; }
///
/// Gets or sets the user data list.
///
/// The user data list.
- public UserItemDataDto[] UserDataList { get; set; }
+ public required UserItemDataDto[] UserDataList { get; set; }
}
}
diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
index dfb6319acb..9a65852f02 100644
--- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj
+++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj
@@ -23,7 +23,7 @@
-
+
diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
index 0083d4f75f..7e0773b6d3 100644
--- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
+++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
@@ -4,6 +4,7 @@ using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using ATL;
using Jellyfin.Data.Enums;
using Jellyfin.Extensions;
using MediaBrowser.Controller.Entities;
@@ -18,7 +19,6 @@ using MediaBrowser.Model.Dto;
using MediaBrowser.Model.Entities;
using MediaBrowser.Model.MediaInfo;
using Microsoft.Extensions.Logging;
-using TagLib;
namespace MediaBrowser.Providers.MediaInfo
{
@@ -27,6 +27,7 @@ namespace MediaBrowser.Providers.MediaInfo
///
public class AudioFileProber
{
+ private const char InternalValueSeparator = '\u001F';
private readonly IMediaEncoder _mediaEncoder;
private readonly IItemRepository _itemRepo;
private readonly ILibraryManager _libraryManager;
@@ -61,6 +62,7 @@ namespace MediaBrowser.Providers.MediaInfo
_mediaSourceManager = mediaSourceManager;
_lyricResolver = lyricResolver;
_lyricManager = lyricManager;
+ ATL.Settings.DisplayValueSeparator = InternalValueSeparator;
}
///
@@ -127,7 +129,6 @@ namespace MediaBrowser.Providers.MediaInfo
audio.RunTimeTicks = mediaInfo.RunTimeTicks;
audio.Size = mediaInfo.Size;
- audio.PremiereDate = mediaInfo.PremiereDate;
// Add external lyrics first to prevent the lrc file get overwritten on first scan
var mediaStreams = new List(mediaInfo.MediaStreams);
@@ -157,60 +158,23 @@ namespace MediaBrowser.Providers.MediaInfo
/// Whether to extract embedded lyrics to lrc file.
private async Task FetchDataFromTags(Audio audio, Model.MediaInfo.MediaInfo mediaInfo, MetadataRefreshOptions options, bool tryExtractEmbeddedLyrics)
{
- Tag? tags = null;
- try
- {
- using var file = TagLib.File.Create(audio.Path);
- var tagTypes = file.TagTypesOnDisk;
+ Track track = new Track(audio.Path);
- if (tagTypes.HasFlag(TagTypes.Id3v2))
- {
- tags = file.GetTag(TagTypes.Id3v2);
- }
- else if (tagTypes.HasFlag(TagTypes.Ape))
- {
- tags = file.GetTag(TagTypes.Ape);
- }
- else if (tagTypes.HasFlag(TagTypes.FlacMetadata))
- {
- tags = file.GetTag(TagTypes.FlacMetadata);
- }
- else if (tagTypes.HasFlag(TagTypes.Apple))
- {
- tags = file.GetTag(TagTypes.Apple);
- }
- else if (tagTypes.HasFlag(TagTypes.Xiph))
- {
- tags = file.GetTag(TagTypes.Xiph);
- }
- else if (tagTypes.HasFlag(TagTypes.AudibleMetadata))
- {
- tags = file.GetTag(TagTypes.AudibleMetadata);
- }
- else if (tagTypes.HasFlag(TagTypes.Id3v1))
- {
- tags = file.GetTag(TagTypes.Id3v1);
- }
- }
- catch (Exception e)
+ // ATL will fall back to filename as title when it does not understand the metadata
+ if (track.MetadataFormats.All(mf => mf.Equals(ATL.Factory.UNKNOWN_FORMAT)))
{
- _logger.LogWarning(e, "TagLib-Sharp does not support this audio");
+ track.Title = mediaInfo.Name;
}
- tags ??= new TagLib.Id3v2.Tag();
- tags.AlbumArtists ??= mediaInfo.AlbumArtists;
- tags.Album ??= mediaInfo.Album;
- tags.Title ??= mediaInfo.Name;
- tags.Year = tags.Year == 0U ? Convert.ToUInt32(mediaInfo.ProductionYear, CultureInfo.InvariantCulture) : tags.Year;
- tags.Performers ??= mediaInfo.Artists;
- tags.Genres ??= mediaInfo.Genres;
- tags.Track = tags.Track == 0U ? Convert.ToUInt32(mediaInfo.IndexNumber, CultureInfo.InvariantCulture) : tags.Track;
- tags.Disc = tags.Disc == 0U ? Convert.ToUInt32(mediaInfo.ParentIndexNumber, CultureInfo.InvariantCulture) : tags.Disc;
+ track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album;
+ track.Year ??= mediaInfo.ProductionYear;
+ track.TrackNumber ??= mediaInfo.IndexNumber;
+ track.DiscNumber ??= mediaInfo.ParentIndexNumber;
if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
{
var people = new List();
- var albumArtists = tags.AlbumArtists;
+ var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? mediaInfo.AlbumArtists : track.AlbumArtist.Split(InternalValueSeparator);
foreach (var albumArtist in albumArtists)
{
if (!string.IsNullOrEmpty(albumArtist))
@@ -223,7 +187,7 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
- var performers = tags.Performers;
+ var performers = string.IsNullOrEmpty(track.Artist) ? mediaInfo.Artists : track.Artist.Split(InternalValueSeparator);
foreach (var performer in performers)
{
if (!string.IsNullOrEmpty(performer))
@@ -236,7 +200,7 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
- foreach (var composer in tags.Composers)
+ foreach (var composer in track.Composer.Split(InternalValueSeparator))
{
if (!string.IsNullOrEmpty(composer))
{
@@ -277,27 +241,32 @@ namespace MediaBrowser.Providers.MediaInfo
}
}
- if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(tags.Title))
+ if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(track.Title))
{
- audio.Name = tags.Title;
+ audio.Name = track.Title;
}
if (options.ReplaceAllMetadata)
{
- audio.Album = tags.Album;
- audio.IndexNumber = Convert.ToInt32(tags.Track);
- audio.ParentIndexNumber = Convert.ToInt32(tags.Disc);
+ audio.Album = track.Album;
+ audio.IndexNumber = track.TrackNumber;
+ audio.ParentIndexNumber = track.DiscNumber;
}
else
{
- audio.Album ??= tags.Album;
- audio.IndexNumber ??= Convert.ToInt32(tags.Track);
- audio.ParentIndexNumber ??= Convert.ToInt32(tags.Disc);
+ audio.Album ??= track.Album;
+ audio.IndexNumber ??= track.TrackNumber;
+ audio.ParentIndexNumber ??= track.DiscNumber;
}
- if (tags.Year != 0)
+ if (track.Date.HasValue)
{
- var year = Convert.ToInt32(tags.Year);
+ audio.PremiereDate = track.Date;
+ }
+
+ if (track.Year.HasValue)
+ {
+ var year = track.Year.Value;
audio.ProductionYear = year;
if (!audio.PremiereDate.HasValue)
@@ -308,64 +277,91 @@ namespace MediaBrowser.Providers.MediaInfo
}
catch (ArgumentOutOfRangeException ex)
{
- _logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, tags.Year);
+ _logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, track.Year);
}
}
}
if (!audio.LockedFields.Contains(MetadataField.Genres))
{
+ var genres = string.IsNullOrEmpty(track.Genre) ? mediaInfo.Genres : track.Genre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
audio.Genres = options.ReplaceAllMetadata || audio.Genres == null || audio.Genres.Length == 0
- ? tags.Genres.Distinct(StringComparer.OrdinalIgnoreCase).ToArray()
+ ? genres
: audio.Genres;
}
- if (!double.IsNaN(tags.ReplayGainTrackGain))
+ track.AdditionalFields.TryGetValue("REPLAYGAIN_TRACK_GAIN", out var trackGainTag);
+
+ if (trackGainTag is not null)
{
- audio.NormalizationGain = (float)tags.ReplayGainTrackGain;
+ if (trackGainTag.EndsWith("db", StringComparison.OrdinalIgnoreCase))
+ {
+ trackGainTag = trackGainTag[..^2].Trim();
+ }
+
+ if (float.TryParse(trackGainTag, NumberStyles.Float, CultureInfo.InvariantCulture, out var value))
+ {
+ audio.NormalizationGain = value;
+ }
}
- if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _))
- && !string.IsNullOrEmpty(tags.MusicBrainzArtistId))
+ if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _))
{
- audio.SetProviderId(MetadataProvider.MusicBrainzArtist, tags.MusicBrainzArtistId);
+ if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_ARTISTID", out var musicBrainzArtistTag)
+ || track.AdditionalFields.TryGetValue("MusicBrainz Artist Id", out musicBrainzArtistTag))
+ && !string.IsNullOrEmpty(musicBrainzArtistTag))
+ {
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, musicBrainzArtistTag);
+ }
}
- if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _))
- && !string.IsNullOrEmpty(tags.MusicBrainzReleaseArtistId))
+ if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _))
{
- audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, tags.MusicBrainzReleaseArtistId);
+ if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_ALBUMARTISTID", out var musicBrainzReleaseArtistIdTag)
+ || track.AdditionalFields.TryGetValue("MusicBrainz Album Artist Id", out musicBrainzReleaseArtistIdTag))
+ && !string.IsNullOrEmpty(musicBrainzReleaseArtistIdTag))
+ {
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, musicBrainzReleaseArtistIdTag);
+ }
}
- if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _))
- && !string.IsNullOrEmpty(tags.MusicBrainzReleaseId))
+ if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _))
{
- audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, tags.MusicBrainzReleaseId);
+ if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_ALBUMID", out var musicBrainzReleaseIdTag)
+ || track.AdditionalFields.TryGetValue("MusicBrainz Album Id", out musicBrainzReleaseIdTag))
+ && !string.IsNullOrEmpty(musicBrainzReleaseIdTag))
+ {
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, musicBrainzReleaseIdTag);
+ }
}
- if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _))
- && !string.IsNullOrEmpty(tags.MusicBrainzReleaseGroupId))
+ if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _))
{
- audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, tags.MusicBrainzReleaseGroupId);
+ if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_RELEASEGROUPID", out var musicBrainzReleaseGroupIdTag)
+ || track.AdditionalFields.TryGetValue("MusicBrainz Release Group Id", out musicBrainzReleaseGroupIdTag))
+ && !string.IsNullOrEmpty(musicBrainzReleaseGroupIdTag))
+ {
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, musicBrainzReleaseGroupIdTag);
+ }
}
if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out _))
{
- // Fallback to ffprobe as TagLib incorrectly provides recording MBID in `tags.MusicBrainzTrackId`.
- // See https://github.com/mono/taglib-sharp/issues/304
- var trackMbId = mediaInfo.GetProviderId(MetadataProvider.MusicBrainzTrack);
- if (trackMbId is not null)
+ if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_RELEASETRACKID", out var trackMbId)
+ || track.AdditionalFields.TryGetValue("MusicBrainz Release Track Id", out trackMbId))
+ && !string.IsNullOrEmpty(trackMbId))
{
- audio.SetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId);
+ audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId);
}
}
// Save extracted lyrics if they exist,
// and if the audio doesn't yet have lyrics.
- if (!string.IsNullOrWhiteSpace(tags.Lyrics)
+ var lyrics = track.Lyrics.SynchronizedLyrics.Count > 0 ? track.Lyrics.FormatSynchToLRC() : track.Lyrics.UnsynchronizedLyrics;
+ if (!string.IsNullOrWhiteSpace(lyrics)
&& tryExtractEmbeddedLyrics)
{
- await _lyricManager.SaveLyricAsync(audio, "lrc", tags.Lyrics).ConfigureAwait(false);
+ await _lyricManager.SaveLyricAsync(audio, "lrc", lyrics).ConfigureAwait(false);
}
}
diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
index c750caa1c9..de0da7f7bd 100644
--- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs
@@ -220,10 +220,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
item.HomePageUrl = result.Website;
}
- if (!string.IsNullOrWhiteSpace(result.imdbID))
- {
- item.SetProviderId(MetadataProvider.Imdb, result.imdbID);
- }
+ item.TrySetProviderId(MetadataProvider.Imdb, result.imdbID);
ParseAdditionalMetadata(itemResult, result, isEnglishRequested);
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
index dac7a74ed8..8d68e2dcfe 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs
@@ -81,11 +81,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
}
remoteResult.SetProviderId(MetadataProvider.Tmdb, movie.Id.ToString(CultureInfo.InvariantCulture));
-
- if (!string.IsNullOrWhiteSpace(movie.ImdbId))
- {
- remoteResult.SetProviderId(MetadataProvider.Imdb, movie.ImdbId);
- }
+ remoteResult.TrySetProviderId(MetadataProvider.Imdb, movie.ImdbId);
return new[] { remoteResult };
}
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
index 5c6e71fd89..98c46895d7 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonProvider.cs
@@ -56,10 +56,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
}
result.SetProviderId(MetadataProvider.Tmdb, personResult.Id.ToString(CultureInfo.InvariantCulture));
- if (!string.IsNullOrEmpty(personResult.ExternalIds.ImdbId))
- {
- result.SetProviderId(MetadataProvider.Imdb, personResult.ExternalIds.ImdbId);
- }
+ result.TrySetProviderId(MetadataProvider.Imdb, personResult.ExternalIds.ImdbId);
return new[] { result };
}
@@ -129,11 +126,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People
}
item.SetProviderId(MetadataProvider.Tmdb, person.Id.ToString(CultureInfo.InvariantCulture));
-
- if (!string.IsNullOrEmpty(person.ImdbId))
- {
- item.SetProviderId(MetadataProvider.Imdb, person.ImdbId);
- }
+ item.TrySetProviderId(MetadataProvider.Imdb, person.ImdbId);
result.HasMetadata = true;
result.Item = item;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
index 489f5e2a17..e628abde55 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs
@@ -187,20 +187,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
};
var externalIds = episodeResult.ExternalIds;
- if (!string.IsNullOrEmpty(externalIds?.TvdbId))
- {
- item.SetProviderId(MetadataProvider.Tvdb, externalIds.TvdbId);
- }
-
- if (!string.IsNullOrEmpty(externalIds?.ImdbId))
- {
- item.SetProviderId(MetadataProvider.Imdb, externalIds.ImdbId);
- }
-
- if (!string.IsNullOrEmpty(externalIds?.TvrageId))
- {
- item.SetProviderId(MetadataProvider.TvRage, externalIds.TvrageId);
- }
+ item.TrySetProviderId(MetadataProvider.Tvdb, externalIds?.TvdbId);
+ item.TrySetProviderId(MetadataProvider.Imdb, externalIds?.ImdbId);
+ item.TrySetProviderId(MetadataProvider.TvRage, externalIds?.TvrageId);
if (episodeResult.Videos?.Results is not null)
{
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
index 10efb68b94..3f208b5993 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs
@@ -73,10 +73,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
result.Item.Name = seasonResult.Name;
}
- if (!string.IsNullOrEmpty(seasonResult.ExternalIds?.TvdbId))
- {
- result.Item.SetProviderId(MetadataProvider.Tvdb, seasonResult.ExternalIds.TvdbId);
- }
+ result.Item.TrySetProviderId(MetadataProvider.Tvdb, seasonResult.ExternalIds.TvdbId);
// TODO why was this disabled?
var credits = seasonResult.Credits;
diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
index d8476bd47d..e4062740fe 100644
--- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
+++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs
@@ -135,15 +135,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
remoteResult.SetProviderId(MetadataProvider.Tmdb, series.Id.ToString(CultureInfo.InvariantCulture));
if (series.ExternalIds is not null)
{
- if (!string.IsNullOrEmpty(series.ExternalIds.ImdbId))
- {
- remoteResult.SetProviderId(MetadataProvider.Imdb, series.ExternalIds.ImdbId);
- }
+ remoteResult.TrySetProviderId(MetadataProvider.Imdb, series.ExternalIds.ImdbId);
- if (!string.IsNullOrEmpty(series.ExternalIds.TvdbId))
- {
- remoteResult.SetProviderId(MetadataProvider.Tvdb, series.ExternalIds.TvdbId);
- }
+ remoteResult.TrySetProviderId(MetadataProvider.Tvdb, series.ExternalIds.TvdbId);
}
remoteResult.PremiereDate = series.FirstAirDate?.ToUniversalTime();
@@ -289,20 +283,9 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
var ids = seriesResult.ExternalIds;
if (ids is not null)
{
- if (!string.IsNullOrWhiteSpace(ids.ImdbId))
- {
- series.SetProviderId(MetadataProvider.Imdb, ids.ImdbId);
- }
-
- if (!string.IsNullOrEmpty(ids.TvrageId))
- {
- series.SetProviderId(MetadataProvider.TvRage, ids.TvrageId);
- }
-
- if (!string.IsNullOrEmpty(ids.TvdbId))
- {
- series.SetProviderId(MetadataProvider.Tvdb, ids.TvdbId);
- }
+ series.TrySetProviderId(MetadataProvider.Imdb, ids.ImdbId);
+ series.TrySetProviderId(MetadataProvider.TvRage, ids.TvrageId);
+ series.TrySetProviderId(MetadataProvider.Tvdb, ids.TvdbId);
}
var contentRatings = seriesResult.ContentRatings.Results ?? new List();
diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
index d049c5a8ef..f2681500b0 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs
@@ -572,10 +572,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
var provider = reader.GetAttribute("type");
var providerId = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(provider) && !string.IsNullOrWhiteSpace(providerId))
- {
- item.SetProviderId(provider, providerId);
- }
+ item.TrySetProviderId(provider, providerId);
break;
case "thumb":
@@ -604,10 +601,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
if (_validProviderIds.TryGetValue(readerName, out string? providerIdValue))
{
var id = reader.ReadElementContentAsString();
- if (!string.IsNullOrWhiteSpace(providerIdValue) && !string.IsNullOrWhiteSpace(id))
- {
- item.SetProviderId(providerIdValue, id);
- }
+ item.TrySetProviderId(providerIdValue, id);
}
else
{
diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
index 044efb51e9..2a1a14834b 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs
@@ -59,80 +59,50 @@ namespace MediaBrowser.XbmcMetadata.Parsers
try
{
// Extract episode details from the first episodedetails block
- using (var stringReader = new StringReader(xml))
- using (var reader = XmlReader.Create(stringReader, settings))
- {
- reader.MoveToContent();
- reader.Read();
-
- // Loop through each element
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- if (reader.NodeType == XmlNodeType.Element)
- {
- FetchDataFromXmlNode(reader, item);
- }
- else
- {
- reader.Read();
- }
- }
- }
+ ReadEpisodeDetailsFromXml(item, xml, settings, cancellationToken);
// Extract the last episode number from nfo
- // Retrieves all title and plot tags from the rest of the nfo and concatenates them with the first episode
+ // Retrieves all additional episodedetails blocks from the rest of the nfo and concatenates the name, originalTitle and overview tags with the first episode
// This is needed because XBMC metadata uses multiple episodedetails blocks instead of episodenumberend tag
var name = new StringBuilder(item.Item.Name);
+ var originalTitle = new StringBuilder(item.Item.OriginalTitle);
var overview = new StringBuilder(item.Item.Overview);
while ((index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase)) != -1)
{
xml = xmlFile.Substring(0, index + srch.Length);
xmlFile = xmlFile.Substring(index + srch.Length);
- using (var stringReader = new StringReader(xml))
- using (var reader = XmlReader.Create(stringReader, settings))
+ var additionalEpisode = new MetadataResult()
+ {
+ Item = new Episode()
+ };
+
+ // Extract episode details from additional episodedetails block
+ ReadEpisodeDetailsFromXml(additionalEpisode, xml, settings, cancellationToken);
+
+ if (!string.IsNullOrEmpty(additionalEpisode.Item.Name))
+ {
+ name.Append(" / ").Append(additionalEpisode.Item.Name);
+ }
+
+ if (!string.IsNullOrEmpty(additionalEpisode.Item.Overview))
+ {
+ overview.Append(" / ").Append(additionalEpisode.Item.Overview);
+ }
+
+ if (!string.IsNullOrEmpty(additionalEpisode.Item.OriginalTitle))
+ {
+ originalTitle.Append(" / ").Append(additionalEpisode.Item.OriginalTitle);
+ }
+
+ if (additionalEpisode.Item.IndexNumber != null)
{
- reader.MoveToContent();
-
- while (!reader.EOF && reader.ReadState == ReadState.Interactive)
- {
- cancellationToken.ThrowIfCancellationRequested();
-
- if (reader.NodeType == XmlNodeType.Element)
- {
- switch (reader.Name)
- {
- case "name":
- case "title":
- case "localtitle":
- name.Append(" / ").Append(reader.ReadElementContentAsString());
- break;
- case "episode":
- {
- if (int.TryParse(reader.ReadElementContentAsString(), out var num))
- {
- item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num);
- }
-
- break;
- }
-
- case "biography":
- case "plot":
- case "review":
- overview.Append(" / ").Append(reader.ReadElementContentAsString());
- break;
- }
- }
-
- reader.Read();
- }
+ item.Item.IndexNumberEnd = Math.Max((int)additionalEpisode.Item.IndexNumber, item.Item.IndexNumberEnd ?? (int)additionalEpisode.Item.IndexNumber);
}
}
item.Item.Name = name.ToString();
+ item.Item.OriginalTitle = originalTitle.ToString();
item.Item.Overview = overview.ToString();
}
catch (XmlException)
@@ -200,5 +170,33 @@ namespace MediaBrowser.XbmcMetadata.Parsers
break;
}
}
+
+ ///
+ /// Reads the episode details from the given xml and saves the result in the provided result item.
+ ///
+ private void ReadEpisodeDetailsFromXml(MetadataResult item, string xml, XmlReaderSettings settings, CancellationToken cancellationToken)
+ {
+ using (var stringReader = new StringReader(xml))
+ using (var reader = XmlReader.Create(stringReader, settings))
+ {
+ reader.MoveToContent();
+ reader.Read();
+
+ // Loop through each element
+ while (!reader.EOF && reader.ReadState == ReadState.Interactive)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ FetchDataFromXmlNode(reader, item);
+ }
+ else
+ {
+ reader.Read();
+ }
+ }
+ }
+ }
}
}
diff --git a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
index af867cd59f..2d65188b63 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
@@ -65,15 +65,8 @@ namespace MediaBrowser.XbmcMetadata.Parsers
tmdbId = contentId;
}
- if (!string.IsNullOrWhiteSpace(imdbId))
- {
- item.SetProviderId(MetadataProvider.Imdb, imdbId);
- }
-
- if (!string.IsNullOrWhiteSpace(tmdbId))
- {
- item.SetProviderId(MetadataProvider.Tmdb, tmdbId);
- }
+ item.TrySetProviderId(MetadataProvider.Imdb, imdbId);
+ item.TrySetProviderId(MetadataProvider.Tmdb, tmdbId);
break;
}
@@ -83,10 +76,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers
var movie = item as Movie;
var tmdbcolid = reader.GetAttribute("tmdbcolid");
- if (!string.IsNullOrWhiteSpace(tmdbcolid) && movie is not null)
- {
- movie.SetProviderId(MetadataProvider.TmdbCollection, tmdbcolid);
- }
+ movie?.TrySetProviderId(MetadataProvider.TmdbCollection, tmdbcolid);
var val = reader.ReadInnerXml();
diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
index d99e11bcd9..59abef919e 100644
--- a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
+++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs
@@ -48,29 +48,16 @@ namespace MediaBrowser.XbmcMetadata.Parsers
{
case "id":
{
- string? imdbId = reader.GetAttribute("IMDB");
- string? tmdbId = reader.GetAttribute("TMDB");
- string? tvdbId = reader.GetAttribute("TVDB");
+ item.TrySetProviderId(MetadataProvider.Imdb, reader.GetAttribute("IMDB"));
+ item.TrySetProviderId(MetadataProvider.Tmdb, reader.GetAttribute("TMDB"));
+ string? tvdbId = reader.GetAttribute("TVDB");
if (string.IsNullOrWhiteSpace(tvdbId))
{
tvdbId = reader.ReadElementContentAsString();
}
- if (!string.IsNullOrWhiteSpace(imdbId))
- {
- item.SetProviderId(MetadataProvider.Imdb, imdbId);
- }
-
- if (!string.IsNullOrWhiteSpace(tmdbId))
- {
- item.SetProviderId(MetadataProvider.Tmdb, tmdbId);
- }
-
- if (!string.IsNullOrWhiteSpace(tvdbId))
- {
- item.SetProviderId(MetadataProvider.Tvdb, tvdbId);
- }
+ item.TrySetProviderId(MetadataProvider.Tvdb, tvdbId);
break;
}
diff --git a/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs
index 9954424a4c..85f327c934 100644
--- a/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs
+++ b/MediaBrowser.XbmcMetadata/Providers/BaseVideoNfoProvider.cs
@@ -54,11 +54,6 @@ namespace MediaBrowser.XbmcMetadata.Providers
result.People = tmpItem.People;
result.Images = tmpItem.Images;
result.RemoteImages = tmpItem.RemoteImages;
-
- if (tmpItem.UserDataList is not null)
- {
- result.UserDataList = tmpItem.UserDataList;
- }
}
///
diff --git a/jellyfin.ruleset b/jellyfin.ruleset
index db116f46c8..67ffd9a37b 100644
--- a/jellyfin.ruleset
+++ b/jellyfin.ruleset
@@ -105,6 +105,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Jellyfin.LiveTv/Guide/GuideManager.cs b/src/Jellyfin.LiveTv/Guide/GuideManager.cs
index 093970c38b..f657422a04 100644
--- a/src/Jellyfin.LiveTv/Guide/GuideManager.cs
+++ b/src/Jellyfin.LiveTv/Guide/GuideManager.cs
@@ -479,10 +479,7 @@ public class GuideManager : IGuideManager
DateModified = DateTime.UtcNow
};
- if (!string.IsNullOrEmpty(info.Etag))
- {
- item.SetProviderId(EtagKey, info.Etag);
- }
+ item.TrySetProviderId(EtagKey, info.Etag);
}
if (!string.Equals(info.ShowId, item.ShowId, StringComparison.OrdinalIgnoreCase))
diff --git a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
index 6d88dbb8ef..31ddd427cc 100644
--- a/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
+++ b/tests/Jellyfin.Model.Tests/Dlna/StreamBuilderTests.cs
@@ -51,8 +51,8 @@ namespace Jellyfin.Model.Tests
[InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
- [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
- [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
+ [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450
// AndroidPixel
[InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("AndroidPixel", "mp4-h264-ac3-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
@@ -205,8 +205,8 @@ namespace Jellyfin.Model.Tests
[InlineData("SafariNext", "mp4-h264-ac3-aacExt-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("SafariNext", "mp4-h264-ac3-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("SafariNext", "mp4-hevc-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
- [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
- [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported, "Remux", "HLS.mp4")] // #6450
+ [InlineData("SafariNext", "mp4-hevc-ac3-aac-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450
+ [InlineData("SafariNext", "mp4-hevc-ac3-aacExt-srt-15200k", PlayMethod.Transcode, TranscodeReason.VideoCodecNotSupported | TranscodeReason.AudioChannelsNotSupported, "DirectStream", "HLS.mp4")] // #6450
// AndroidPixel
[InlineData("AndroidPixel", "mp4-h264-aac-srt-2600k", PlayMethod.DirectPlay)] // #6450
[InlineData("AndroidPixel", "mp4-h264-ac3-aacDef-srt-2600k", PlayMethod.DirectPlay)] // #6450
@@ -432,7 +432,14 @@ namespace Jellyfin.Model.Tests
if (targetAudioStream?.IsExternal == false)
{
// Check expected audio codecs (1)
- Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs);
+ if ((why & TranscodeReason.AudioChannelsNotSupported) == 0)
+ {
+ Assert.DoesNotContain(targetAudioStream.Codec, streamInfo.AudioCodecs);
+ }
+ else
+ {
+ Assert.Equal(targetAudioStream.Channels, streamInfo.TranscodingMaxAudioChannels);
+ }
}
}
else if (transcodeMode.Equals("Remux", StringComparison.Ordinal))
diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs
deleted file mode 100644
index 6773bbeb19..0000000000
--- a/tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs
+++ /dev/null
@@ -1,35 +0,0 @@
-using Emby.Naming.TV;
-using Xunit;
-
-namespace Jellyfin.Naming.Tests.TV
-{
- public class SeasonFolderTests
- {
- [Theory]
- [InlineData("/Drive/Season 1", 1, true)]
- [InlineData("/Drive/Season 2", 2, true)]
- [InlineData("/Drive/Season 02", 2, true)]
- [InlineData("/Drive/Seinfeld/S02", 2, true)]
- [InlineData("/Drive/Seinfeld/2", 2, true)]
- [InlineData("/Drive/Season 2009", 2009, true)]
- [InlineData("/Drive/Season1", 1, true)]
- [InlineData("The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", 4, true)]
- [InlineData("/Drive/Season 7 (2016)", 7, false)]
- [InlineData("/Drive/Staffel 7 (2016)", 7, false)]
- [InlineData("/Drive/Stagione 7 (2016)", 7, false)]
- [InlineData("/Drive/Season (8)", null, false)]
- [InlineData("/Drive/3.Staffel", 3, false)]
- [InlineData("/Drive/s06e05", null, false)]
- [InlineData("/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", null, false)]
- [InlineData("/Drive/extras", 0, true)]
- [InlineData("/Drive/specials", 0, true)]
- public void GetSeasonNumberFromPathTest(string path, int? seasonNumber, bool isSeasonDirectory)
- {
- var result = SeasonPathParser.Parse(path, true, true);
-
- Assert.Equal(result.SeasonNumber is not null, result.Success);
- Assert.Equal(result.SeasonNumber, seasonNumber);
- Assert.Equal(isSeasonDirectory, result.IsSeasonFolder);
- }
- }
-}
diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs
new file mode 100644
index 0000000000..3a042df683
--- /dev/null
+++ b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs
@@ -0,0 +1,37 @@
+using Emby.Naming.TV;
+using Xunit;
+
+namespace Jellyfin.Naming.Tests.TV;
+
+public class SeasonPathParserTests
+{
+ [Theory]
+ [InlineData("/Drive/Season 1", 1, true)]
+ [InlineData("/Drive/s1", 1, true)]
+ [InlineData("/Drive/S1", 1, true)]
+ [InlineData("/Drive/Season 2", 2, true)]
+ [InlineData("/Drive/Season 02", 2, true)]
+ [InlineData("/Drive/Seinfeld/S02", 2, true)]
+ [InlineData("/Drive/Seinfeld/2", 2, true)]
+ [InlineData("/Drive/Seinfeld - S02", 2, true)]
+ [InlineData("/Drive/Season 2009", 2009, true)]
+ [InlineData("/Drive/Season1", 1, true)]
+ [InlineData("The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", 4, true)]
+ [InlineData("/Drive/Season 7 (2016)", 7, false)]
+ [InlineData("/Drive/Staffel 7 (2016)", 7, false)]
+ [InlineData("/Drive/Stagione 7 (2016)", 7, false)]
+ [InlineData("/Drive/Season (8)", null, false)]
+ [InlineData("/Drive/3.Staffel", 3, false)]
+ [InlineData("/Drive/s06e05", null, false)]
+ [InlineData("/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", null, false)]
+ [InlineData("/Drive/extras", 0, true)]
+ [InlineData("/Drive/specials", 0, true)]
+ public void GetSeasonNumberFromPathTest(string path, int? seasonNumber, bool isSeasonDirectory)
+ {
+ var result = SeasonPathParser.Parse(path, true, true);
+
+ Assert.Equal(result.SeasonNumber is not null, result.Success);
+ Assert.Equal(result.SeasonNumber, seasonNumber);
+ Assert.Equal(isSeasonDirectory, result.IsSeasonFolder);
+ }
+}
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
index c0d06116b5..3721d1f7ac 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs
@@ -123,6 +123,30 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
Assert.Equal(2004, item.ProductionYear);
}
+ [Fact]
+ public void Fetch_Valid_MultiEpisode_With_Missing_Tags_Success()
+ {
+ var result = new MetadataResult()
+ {
+ Item = new Episode()
+ };
+
+ _parser.Fetch(result, "Test Data/Stargate Atlantis S01E01-E04.nfo", CancellationToken.None);
+
+ var item = result.Item;
+ // provided for episode 1, 3 and 4
+ Assert.Equal("Rising / Hide and Seek / Thirty-Eight Minutes", item.Name);
+ // provided for all episodes
+ Assert.Equal("Rising (1) / Rising (2) / Hide and Seek / Thirty-Eight Minutes", item.OriginalTitle);
+ Assert.Equal(1, item.IndexNumber);
+ Assert.Equal(4, item.IndexNumberEnd);
+ Assert.Equal(1, item.ParentIndexNumber);
+ // only provided for episode 1
+ Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy.", item.Overview);
+ Assert.Equal(new DateTime(2004, 7, 16), item.PremiereDate);
+ Assert.Equal(2004, item.ProductionYear);
+ }
+
[Fact]
public void Parse_GivenFileWithThumbWithoutAspect_Success()
{
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
index 0a153b9cc1..5bc4abd06d 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs
@@ -53,7 +53,10 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers
var userData = new Mock();
userData.Setup(x => x.GetUserData(_testUser, It.IsAny()))
- .Returns(new UserItemData());
+ .Returns(new UserItemData()
+ {
+ Key = "Something"
+ });
var directoryService = new Mock();
_localImageFileMetadata = new FileSystemMetadata()
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Rising.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Rising.nfo
index 56250c09a8..e95f5002a1 100644
--- a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Rising.nfo
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Rising.nfo
@@ -7,6 +7,18 @@
https://artworks.thetvdb.com/banners/episodes/70851/25333.jpg
false
8.0
+
+ Joe Flanigan
+ John Sheppard
+ 0
+ https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg
+
+
+ David Hewlett
+ Rodney McKay
+ 1
+ https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg
+
Rising (2)
@@ -17,4 +29,16 @@
https://artworks.thetvdb.com/banners/episodes/70851/25334.jpg
false
7.9
+
+ Joe Flanigan
+ John Sheppard
+ 0
+ https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg
+
+
+ David Hewlett
+ Rodney McKay
+ 1
+ https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg
+
diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Stargate Atlantis S01E01-E04.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Stargate Atlantis S01E01-E04.nfo
new file mode 100644
index 0000000000..7dee0110c4
--- /dev/null
+++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Stargate Atlantis S01E01-E04.nfo
@@ -0,0 +1,89 @@
+
+ Rising
+ Rising (1)
+ 1
+ 1
+ 2004-07-16
+ A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy.
+ https://artworks.thetvdb.com/banners/episodes/70851/25333.jpg
+ false
+ 8.0
+
+ Joe Flanigan
+ John Sheppard
+ 0
+ https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg
+
+
+ David Hewlett
+ Rodney McKay
+ 1
+ https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg
+
+
+
+ Rising (2)
+ 1
+ 2
+ 2004-07-16
+ https://artworks.thetvdb.com/banners/episodes/70851/25334.jpg
+ false
+ 7.9
+
+ Joe Flanigan
+ John Sheppard
+ 0
+ https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg
+
+
+ David Hewlett
+ Rodney McKay
+ 1
+ https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg
+
+
+
+ Hide and Seek
+ Hide and Seek
+ 1
+ 3
+ 2004-07-23
+ https://artworks.thetvdb.com/banners/episodes/70851/25335.jpg
+ false
+ 7.5
+
+ Joe Flanigan
+ John Sheppard
+ 0
+ https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg
+
+
+ David Hewlett
+ Rodney McKay
+ 1
+ https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg
+
+
+
+ Thirty-Eight Minutes
+ Thirty-Eight Minutes
+ 1
+ 4
+ 2004-07-23
+ https://artworks.thetvdb.com/banners/episodes/70851/25336.jpg
+ false
+ 7.5
+
+ Joe Flanigan
+ John Sheppard
+ 0
+ https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg
+
+
+ David Hewlett
+ Rodney McKay
+ 1
+ https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg
+
+
+