From ecd2dab0a2550c80ae054576d9c05ddb24d5e47b Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Wed, 17 Jul 2024 15:48:21 +0200 Subject: [PATCH 01/41] Add TrySetProviderId extension --- .../Resolvers/Movies/BoxSetResolver.cs | 6 +- .../Library/Resolvers/Movies/MovieResolver.cs | 12 +- .../Library/Resolvers/TV/SeriesResolver.cs | 35 +- .../Parsers/BaseItemXmlParser.cs | 10 +- .../Probing/ProbeResultNormalizer.cs | 25 +- .../Entities/ProviderIdsExtensions.cs | 333 ++++++++++-------- .../MediaInfo/AudioFileProber.cs | 27 +- .../Plugins/Omdb/OmdbProvider.cs | 5 +- .../Plugins/Tmdb/Movies/TmdbMovieProvider.cs | 6 +- .../Plugins/Tmdb/People/TmdbPersonProvider.cs | 11 +- .../Plugins/Tmdb/TV/TmdbEpisodeProvider.cs | 17 +- .../Plugins/Tmdb/TV/TmdbSeasonProvider.cs | 5 +- .../Plugins/Tmdb/TV/TmdbSeriesProvider.cs | 27 +- .../Parsers/BaseNfoParser.cs | 10 +- .../Parsers/MovieNfoParser.cs | 16 +- .../Parsers/SeriesNfoParser.cs | 21 +- src/Jellyfin.LiveTv/Guide/GuideManager.cs | 5 +- 17 files changed, 235 insertions(+), 336 deletions(-) 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/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 f5185398cd..3aafb733da 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -1319,38 +1319,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/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. /// - public static class ProviderIdsExtensions + 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. + /// + /// 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); + ArgumentNullException.ThrowIfNull(instance); - /// - /// 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) + if (instance.ProviderIds is null) { - ArgumentNullException.ThrowIfNull(instance); - - return instance.TryGetProviderId(name, out _); + 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; + } + else + { + instance.ProviderIds[name] = value; } - /// - /// Gets a provider id. - /// - /// The instance. - /// The name. - /// System.String. - public static string? GetProviderId(this IHasProviderIds instance, string name) + 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)) { - instance.TryGetProviderId(name, out string? id); - return id; + throw new ArgumentException("Provider id name cannot contain '='", nameof(name)); } - /// - /// Gets a provider id. - /// - /// The instance. - /// The provider. - /// System.String. - public static string? GetProviderId(this IHasProviderIds instance, MetadataProvider provider) + // 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.GetProviderId(provider.ToString()); + instance.ProviderIds[enumValue] = 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) + else { - 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; - } - } - - /// - /// 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); - } - - /// - /// Removes a provider id. - /// - /// The instance. - /// The name. - public static void RemoveProviderId(this IHasProviderIds instance, string name) - { - ArgumentNullException.ThrowIfNull(instance); - ArgumentException.ThrowIfNullOrEmpty(name); - - instance.ProviderIds?.Remove(name); - } - - /// - /// 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()); + instance.ProviderIds[name] = value; } } + + /// + /// 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); + + /// + /// Removes a provider id. + /// + /// The instance. + /// The name. + public static void RemoveProviderId(this IHasProviderIds instance, string name) + { + ArgumentNullException.ThrowIfNull(instance); + ArgumentException.ThrowIfNullOrEmpty(name); + + instance.ProviderIds?.Remove(name); + } + + /// + /// 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.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 0083d4f75f..fbafd55187 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -325,39 +325,32 @@ namespace MediaBrowser.Providers.MediaInfo audio.NormalizationGain = (float)tags.ReplayGainTrackGain; } - if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _)) - && !string.IsNullOrEmpty(tags.MusicBrainzArtistId)) + if (options.ReplaceAllMetadata || !audio.HasProviderId(MetadataProvider.MusicBrainzArtist)) { - audio.SetProviderId(MetadataProvider.MusicBrainzArtist, tags.MusicBrainzArtistId); + audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, tags.MusicBrainzArtistId); } - if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _)) - && !string.IsNullOrEmpty(tags.MusicBrainzReleaseArtistId)) + if (options.ReplaceAllMetadata || !audio.HasProviderId(MetadataProvider.MusicBrainzAlbumArtist)) { - audio.SetProviderId(MetadataProvider.MusicBrainzAlbumArtist, tags.MusicBrainzReleaseArtistId); + audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, tags.MusicBrainzReleaseArtistId); } - if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _)) - && !string.IsNullOrEmpty(tags.MusicBrainzReleaseId)) + if (options.ReplaceAllMetadata || !audio.HasProviderId(MetadataProvider.MusicBrainzAlbum)) { - audio.SetProviderId(MetadataProvider.MusicBrainzAlbum, tags.MusicBrainzReleaseId); + audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, tags.MusicBrainzReleaseId); } - if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _)) - && !string.IsNullOrEmpty(tags.MusicBrainzReleaseGroupId)) + if (options.ReplaceAllMetadata || !audio.HasProviderId(MetadataProvider.MusicBrainzReleaseGroup)) { - audio.SetProviderId(MetadataProvider.MusicBrainzReleaseGroup, tags.MusicBrainzReleaseGroupId); + audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, tags.MusicBrainzReleaseGroupId); } - if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out _)) + if (options.ReplaceAllMetadata || !audio.HasProviderId(MetadataProvider.MusicBrainzTrack)) { // 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) - { - audio.SetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId); - } + audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId); } // Save extracted lyrics if they exist, 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/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/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)) From 26fcb78ae39ffe08885c2f3c8bf97c74ab61602e Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sat, 20 Jul 2024 13:31:04 +0200 Subject: [PATCH 02/41] Don't buffer content in GetStaticRemoteStreamResult --- Jellyfin.Api/Helpers/FileStreamResponseHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"; From 975ad25162b9526df479d958163279d92493958e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 20:13:31 +0000 Subject: [PATCH 03/41] Update dependency Svg.Skia to v2 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 825301bfcb..5218b2b1c7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -72,7 +72,7 @@ - + From 6fd79fb01597b5132ca5b3bd9c06c92d527ee0cc Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Tue, 23 Jul 2024 11:34:41 -0600 Subject: [PATCH 04/41] Fix up getting livetv programs api docs --- Jellyfin.Api/Controllers/LiveTvController.cs | 10 ++--- .../Models/LiveTvDtos/GetProgramsDto.cs | 39 +++++-------------- 2 files changed, 14 insertions(+), 35 deletions(-) diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 2b26c01f88..a15ff0f529 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,15 +666,15 @@ 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()) @@ -690,7 +690,7 @@ public class LiveTvController : BaseJellyfinApiController 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/Models/LiveTvDtos/GetProgramsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs index 8482b1cf1c..633729c119 100644 --- a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs +++ b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs @@ -17,7 +17,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,110 +26,95 @@ 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; } @@ -140,39 +125,33 @@ public class GetProgramsDto /// /// 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; } /// - /// 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; } } From a968e31179d134ecf5ceffa17f7ae44a684204a7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jul 2024 00:37:36 +0000 Subject: [PATCH 05/41] Update dependency libse to v4.0.7 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 5218b2b1c7..11795728c6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -22,7 +22,7 @@ - + From 8bc1419699f8d235db137aa129c2a886885ed324 Mon Sep 17 00:00:00 2001 From: Tobias <76774751+tobias-varden@users.noreply.github.com> Date: Wed, 24 Jul 2024 13:10:12 +0200 Subject: [PATCH 06/41] Fix comment in PhotoProvider (#12323) Co-authored-by: CS --- Emby.Photos/PhotoProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"]; /// From 7610947248767d551c331cd31a71c9824ba57048 Mon Sep 17 00:00:00 2001 From: Tim Gels <43609220+TimGels@users.noreply.github.com> Date: Wed, 24 Jul 2024 13:33:09 +0200 Subject: [PATCH 07/41] Update issue report.yml to use 10.9.8 version (#12332) --- .github/ISSUE_TEMPLATE/issue report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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* From a4a9a181f5d7f5e93bf8e27d3822894bfa30ab54 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 24 Jul 2024 07:19:12 -0600 Subject: [PATCH 08/41] Update dependency AsyncKeyedLock to v7 (#12316) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 5218b2b1c7..92272ca090 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -4,7 +4,7 @@ - + From 001cad22db4524bb4ed85d00705ac1112808d7fd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 25 Jul 2024 10:35:32 +0000 Subject: [PATCH 09/41] Update github/codeql-action action to v3.25.14 --- .github/workflows/ci-codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index c6ea1d7ca9..339f2e3d79 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@5cf07d8b700b67e235fbb65cbc84f69c0cf10464 # v3.25.14 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12 + uses: github/codeql-action/autobuild@5cf07d8b700b67e235fbb65cbc84f69c0cf10464 # v3.25.14 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@4fa2a7953630fd2f3fb380f21be14ede0169dd4f # v3.25.12 + uses: github/codeql-action/analyze@5cf07d8b700b67e235fbb65cbc84f69c0cf10464 # v3.25.14 From 4f746c40d7ffd0d4be46531a057bf6daed121bd8 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Thu, 25 Jul 2024 11:18:25 -0600 Subject: [PATCH 10/41] suggestions from review --- Jellyfin.Api/Controllers/LiveTvController.cs | 4 ++-- Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index a15ff0f529..0cf36f57ec 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -677,11 +677,11 @@ public class LiveTvController : BaseJellyfinApiController 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; diff --git a/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs b/Jellyfin.Api/Models/LiveTvDtos/GetProgramsDto.cs index 633729c119..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; @@ -121,6 +122,7 @@ public class GetProgramsDto /// /// Gets or sets a value indicating whether retrieve total record count. /// + [DefaultValue(true)] public bool EnableTotalRecordCount { get; set; } = true; /// @@ -147,7 +149,7 @@ public class GetProgramsDto /// /// Gets or sets filter by library series id. /// - public Guid LibrarySeriesId { get; set; } + public Guid? LibrarySeriesId { get; set; } /// /// Gets or sets specify additional fields of information to return in the output. From 3de51cd9f8c4fbb7025ed1a1946a9540c8d9743a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 26 Jul 2024 17:07:18 +0000 Subject: [PATCH 11/41] Update github/codeql-action action to v3.25.15 --- .github/workflows/ci-codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 339f2e3d79..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@5cf07d8b700b67e235fbb65cbc84f69c0cf10464 # v3.25.14 + uses: github/codeql-action/init@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@5cf07d8b700b67e235fbb65cbc84f69c0cf10464 # v3.25.14 + uses: github/codeql-action/autobuild@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@5cf07d8b700b67e235fbb65cbc84f69c0cf10464 # v3.25.14 + uses: github/codeql-action/analyze@afb54ba388a7dca6ecae48f608c4ff05ff4cc77a # v3.25.15 From 162ea38a95560f19e0a2df8a54c1ac117d6d91bc Mon Sep 17 00:00:00 2001 From: gnattu Date: Mon, 29 Jul 2024 06:11:59 +0800 Subject: [PATCH 12/41] Check MaxAudioChannels for directAudioStream candidates (#12319) * Check MaxAudioChannels for directAudioStream candidates The current stream builder logic does not check the channel limit when determining if the audio stream can be directly used, and this can cause some undesired effects: - A high channel count surround sound stream might be picked even if a stereo one exists when the user requires stereo audio. - The user's preferred audio codec might not be respected during the downmix because the requested codec is now forced to be the same as the original source. Signed-off-by: gnattu * Fix unit test Signed-off-by: gnattu * Set correct transcode reason and target channels for unit test Signed-off-by: gnattu * Match old stream selection behavior Signed-off-by: gnattu * Fix reason matching Signed-off-by: gnattu --------- Signed-off-by: gnattu --- MediaBrowser.Model/Dlna/StreamBuilder.cs | 15 +++++++++++++-- .../Dlna/StreamBuilderTests.cs | 17 ++++++++++++----- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index d37528ede8..4815dcc044 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); 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)) From 172feab084c062355677f58c2da3f35f348b0d04 Mon Sep 17 00:00:00 2001 From: gnattu Date: Thu, 30 May 2024 16:09:50 +0800 Subject: [PATCH 13/41] Migrate to z440.atl instead of TagLib-Sharp The ATL lib provides a lot of advantages to the TagLib we are currently using. Notably: - auto-detect the format of the audio data, even if the file extension has the wrong label, and provides unified API for different file types. - supports more audio formats than TagLib - supports lyrics natively - supports playlists and cuesheets - srovides relatively simple and controllable way for non-standard fields, enable us to implement compatibility features instead of waiting for lib updates - is actually maintained Signed-off-by: gnattu --- Directory.Packages.props | 1 + .../MediaBrowser.Providers.csproj | 2 +- .../MediaInfo/AudioFileProber.cs | 191 ++++++++++-------- 3 files changed, 111 insertions(+), 83 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 5242126a34..b2848c538c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -81,6 +81,7 @@ + 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 fbafd55187..1e7ec36e5d 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 { @@ -127,7 +127,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 +156,19 @@ 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; + ATL.Settings.DisplayValueSeparator = '\u001F'; + 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) - { - _logger.LogWarning(e, "TagLib-Sharp does not support this audio"); - } - - 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.Title = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title; + 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('\u001F'); foreach (var albumArtist in albumArtists) { if (!string.IsNullOrEmpty(albumArtist)) @@ -223,7 +181,7 @@ namespace MediaBrowser.Providers.MediaInfo } } - var performers = tags.Performers; + var performers = string.IsNullOrEmpty(track.Artist) ? mediaInfo.Artists : track.Artist.Split('\u001F'); foreach (var performer in performers) { if (!string.IsNullOrEmpty(performer)) @@ -236,7 +194,7 @@ namespace MediaBrowser.Providers.MediaInfo } } - foreach (var composer in tags.Composers) + foreach (var composer in track.Composer.Split('\u001F')) { if (!string.IsNullOrEmpty(composer)) { @@ -277,27 +235,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,57 +271,121 @@ 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('\u001F').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); + float trackGain = float.NaN; + + if (trackGainTag is not null) { - audio.NormalizationGain = (float)tags.ReplayGainTrackGain; + if (trackGainTag.ToLower(CultureInfo.InvariantCulture).EndsWith("db", StringComparison.OrdinalIgnoreCase)) + { + trackGainTag = trackGainTag[..^2].Trim(); + } + + if (float.TryParse(trackGainTag, NumberStyles.Float, CultureInfo.InvariantCulture, out var value)) + { + trackGain = value; + } } - if (options.ReplaceAllMetadata || !audio.HasProviderId(MetadataProvider.MusicBrainzArtist)) + if (!float.IsNaN(trackGain)) { - audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, tags.MusicBrainzArtistId); + audio.NormalizationGain = trackGain; } - if (options.ReplaceAllMetadata || !audio.HasProviderId(MetadataProvider.MusicBrainzAlbumArtist)) + if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _)) + && !string.IsNullOrEmpty(tags.MusicBrainzArtistId)) { - audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, tags.MusicBrainzReleaseArtistId); + track.AdditionalFields.TryGetValue("MUSICBRAINZ_ARTISTID", out var musicBrainzArtistTag); + if (musicBrainzArtistTag is null) + { + track.AdditionalFields.TryGetValue("MusicBrainz Artist Id", out musicBrainzArtistTag); + } + + if (musicBrainzArtistTag is not null) + { + audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, musicBrainzArtistTag); + } } - if (options.ReplaceAllMetadata || !audio.HasProviderId(MetadataProvider.MusicBrainzAlbum)) + if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _)) + && !string.IsNullOrEmpty(tags.MusicBrainzReleaseArtistId)) { - audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, tags.MusicBrainzReleaseId); + track.AdditionalFields.TryGetValue("MUSICBRAINZ_ALBUMARTISTID", out var musicBrainzReleaseArtistIdTag); + if (musicBrainzReleaseArtistIdTag is null) + { + track.AdditionalFields.TryGetValue("MusicBrainz Album Artist Id", out musicBrainzReleaseArtistIdTag); + } + + if (musicBrainzReleaseArtistIdTag is not null) + { + audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, musicBrainzReleaseArtistIdTag); + } } - if (options.ReplaceAllMetadata || !audio.HasProviderId(MetadataProvider.MusicBrainzReleaseGroup)) + if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _)) + && !string.IsNullOrEmpty(tags.MusicBrainzReleaseId)) { - audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, tags.MusicBrainzReleaseGroupId); + track.AdditionalFields.TryGetValue("MUSICBRAINZ_ALBUMID", out var musicBrainzReleaseIdTag); + if (musicBrainzReleaseIdTag is null) + { + track.AdditionalFields.TryGetValue("MusicBrainz Album Id", out musicBrainzReleaseIdTag); + } + + if (musicBrainzReleaseIdTag is not null) + { + audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, musicBrainzReleaseIdTag); + } } - if (options.ReplaceAllMetadata || !audio.HasProviderId(MetadataProvider.MusicBrainzTrack)) + if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _)) + && !string.IsNullOrEmpty(tags.MusicBrainzReleaseGroupId)) { - // 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); - audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId); + track.AdditionalFields.TryGetValue("MUSICBRAINZ_RELEASEGROUPID", out var musicBrainzReleaseGroupIdTag); + if (musicBrainzReleaseGroupIdTag is null) + { + track.AdditionalFields.TryGetValue("MusicBrainz Release Group Id", out musicBrainzReleaseGroupIdTag); + } + + if (musicBrainzReleaseGroupIdTag is not null) + { + audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, musicBrainzReleaseGroupIdTag); + } + } + + if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out _)) + { + track.AdditionalFields.TryGetValue("MUSICBRAINZ_RELEASETRACKID", out var trackMbId); + if (trackMbId is null) + { + track.AdditionalFields.TryGetValue("MusicBrainz Release Track Id", out trackMbId); + } + + if (trackMbId is not null) + { + 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); } } From ac9322370b441d26fa040e91d1774df6628fce88 Mon Sep 17 00:00:00 2001 From: gnattu Date: Thu, 30 May 2024 18:59:26 +0800 Subject: [PATCH 14/41] Check if the metadata is supported for title fallback Signed-off-by: gnattu --- MediaBrowser.Providers/MediaInfo/AudioFileProber.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 1e7ec36e5d..7a1d04b3a9 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -159,8 +159,13 @@ namespace MediaBrowser.Providers.MediaInfo ATL.Settings.DisplayValueSeparator = '\u001F'; Track track = new Track(audio.Path); + // 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))) + { + track.Title = mediaInfo.Name; + } + track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album; - track.Title = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title; track.Year ??= mediaInfo.ProductionYear; track.TrackNumber ??= mediaInfo.IndexNumber; track.DiscNumber ??= mediaInfo.ParentIndexNumber; From 507f89b8ed23449006c66a2338fe767d178ee13c Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 2 Jun 2024 15:18:32 +0800 Subject: [PATCH 15/41] Bump to v5.25.0 Signed-off-by: gnattu --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index b2848c538c..f9887dc798 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -81,7 +81,7 @@ - + From 939e02cceef9ccbd2b362a39ab10f026d452eb1b Mon Sep 17 00:00:00 2001 From: gnattu Date: Wed, 17 Jul 2024 03:29:16 +0800 Subject: [PATCH 16/41] Apply suggestions from code review Co-authored-by: Cody Robibero --- .../MediaInfo/AudioFileProber.cs | 50 +++++++------------ 1 file changed, 17 insertions(+), 33 deletions(-) diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 7a1d04b3a9..250717205c 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -294,7 +294,7 @@ namespace MediaBrowser.Providers.MediaInfo if (trackGainTag is not null) { - if (trackGainTag.ToLower(CultureInfo.InvariantCulture).EndsWith("db", StringComparison.OrdinalIgnoreCase)) + if (trackGainTag.EndsWith("db", StringComparison.OrdinalIgnoreCase)) { trackGainTag = trackGainTag[..^2].Trim(); } @@ -313,13 +313,9 @@ namespace MediaBrowser.Providers.MediaInfo if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _)) && !string.IsNullOrEmpty(tags.MusicBrainzArtistId)) { - track.AdditionalFields.TryGetValue("MUSICBRAINZ_ARTISTID", out var musicBrainzArtistTag); - if (musicBrainzArtistTag is null) - { - track.AdditionalFields.TryGetValue("MusicBrainz Artist Id", out musicBrainzArtistTag); - } - - if (musicBrainzArtistTag is not null) + 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); } @@ -328,13 +324,9 @@ namespace MediaBrowser.Providers.MediaInfo if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _)) && !string.IsNullOrEmpty(tags.MusicBrainzReleaseArtistId)) { - track.AdditionalFields.TryGetValue("MUSICBRAINZ_ALBUMARTISTID", out var musicBrainzReleaseArtistIdTag); - if (musicBrainzReleaseArtistIdTag is null) - { - track.AdditionalFields.TryGetValue("MusicBrainz Album Artist Id", out musicBrainzReleaseArtistIdTag); - } - - if (musicBrainzReleaseArtistIdTag is not null) + 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); } @@ -343,13 +335,9 @@ namespace MediaBrowser.Providers.MediaInfo if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _)) && !string.IsNullOrEmpty(tags.MusicBrainzReleaseId)) { - track.AdditionalFields.TryGetValue("MUSICBRAINZ_ALBUMID", out var musicBrainzReleaseIdTag); - if (musicBrainzReleaseIdTag is null) - { - track.AdditionalFields.TryGetValue("MusicBrainz Album Id", out musicBrainzReleaseIdTag); - } - - if (musicBrainzReleaseIdTag is not null) + 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); } @@ -358,13 +346,9 @@ namespace MediaBrowser.Providers.MediaInfo if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _)) && !string.IsNullOrEmpty(tags.MusicBrainzReleaseGroupId)) { - track.AdditionalFields.TryGetValue("MUSICBRAINZ_RELEASEGROUPID", out var musicBrainzReleaseGroupIdTag); - if (musicBrainzReleaseGroupIdTag is null) - { - track.AdditionalFields.TryGetValue("MusicBrainz Release Group Id", out musicBrainzReleaseGroupIdTag); - } - - if (musicBrainzReleaseGroupIdTag is not null) + 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); } @@ -372,12 +356,12 @@ namespace MediaBrowser.Providers.MediaInfo if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out _)) { - track.AdditionalFields.TryGetValue("MUSICBRAINZ_RELEASETRACKID", out var trackMbId); - if (trackMbId is null) + if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_RELEASETRACKID", out var trackMbId) + || track.AdditionalFields.TryGetValue("MusicBrainz Release Track Id", out trackMbId)) + && !string.IsNullOrEmpty(trackMbId)) { - track.AdditionalFields.TryGetValue("MusicBrainz Release Track Id", out trackMbId); + audio.SetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId); } - if (trackMbId is not null) { audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId); From 1d658a5a4de0332987c468dc9eb13fe90a08b469 Mon Sep 17 00:00:00 2001 From: gnattu Date: Wed, 17 Jul 2024 03:31:03 +0800 Subject: [PATCH 17/41] Remove redundant check Signed-off-by: gnattu --- MediaBrowser.Providers/MediaInfo/AudioFileProber.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 250717205c..40ff5551d4 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -359,10 +359,6 @@ namespace MediaBrowser.Providers.MediaInfo 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); - } - if (trackMbId is not null) { audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId); } From 56a98a3bb0cc6efcc0d533164aeb8879045d646f Mon Sep 17 00:00:00 2001 From: gnattu Date: Wed, 17 Jul 2024 03:40:07 +0800 Subject: [PATCH 18/41] Make internal value separator a constant Signed-off-by: gnattu --- MediaBrowser.Providers/MediaInfo/AudioFileProber.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 40ff5551d4..13e5310dee 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -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; } /// @@ -156,7 +158,7 @@ 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) { - ATL.Settings.DisplayValueSeparator = '\u001F'; + var test = ATL.Settings.DisplayValueSeparator; Track track = new Track(audio.Path); // ATL will fall back to filename as title when it does not understand the metadata @@ -173,7 +175,7 @@ namespace MediaBrowser.Providers.MediaInfo if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast)) { var people = new List(); - var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? mediaInfo.AlbumArtists : track.AlbumArtist.Split('\u001F'); + var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? mediaInfo.AlbumArtists : track.AlbumArtist.Split(InternalValueSeparator); foreach (var albumArtist in albumArtists) { if (!string.IsNullOrEmpty(albumArtist)) @@ -186,7 +188,7 @@ namespace MediaBrowser.Providers.MediaInfo } } - var performers = string.IsNullOrEmpty(track.Artist) ? mediaInfo.Artists : track.Artist.Split('\u001F'); + var performers = string.IsNullOrEmpty(track.Artist) ? mediaInfo.Artists : track.Artist.Split(InternalValueSeparator); foreach (var performer in performers) { if (!string.IsNullOrEmpty(performer)) @@ -199,7 +201,7 @@ namespace MediaBrowser.Providers.MediaInfo } } - foreach (var composer in track.Composer.Split('\u001F')) + foreach (var composer in track.Composer.Split(InternalValueSeparator)) { if (!string.IsNullOrEmpty(composer)) { @@ -283,7 +285,7 @@ namespace MediaBrowser.Providers.MediaInfo if (!audio.LockedFields.Contains(MetadataField.Genres)) { - var genres = string.IsNullOrEmpty(track.Genre) ? mediaInfo.Genres : track.Genre.Split('\u001F').Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + 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 ? genres : audio.Genres; From 5c5b326b1aa794e3956cc81c08dd380ac9fd9ae0 Mon Sep 17 00:00:00 2001 From: gnattu Date: Wed, 17 Jul 2024 20:33:49 +0800 Subject: [PATCH 19/41] Remove test var Co-authored-by: Cody Robibero --- MediaBrowser.Providers/MediaInfo/AudioFileProber.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 13e5310dee..5270b1c3ef 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -158,7 +158,6 @@ 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) { - var test = ATL.Settings.DisplayValueSeparator; Track track = new Track(audio.Path); // ATL will fall back to filename as title when it does not understand the metadata From 79c4469ac7ac936ded42e0d855df10a0a7d609e2 Mon Sep 17 00:00:00 2001 From: gnattu Date: Thu, 18 Jul 2024 01:59:16 +0800 Subject: [PATCH 20/41] Remove redundant NaN check Signed-off-by: gnattu --- .../MediaInfo/AudioFileProber.cs | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 5270b1c3ef..7e0773b6d3 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -291,7 +291,6 @@ namespace MediaBrowser.Providers.MediaInfo } track.AdditionalFields.TryGetValue("REPLAYGAIN_TRACK_GAIN", out var trackGainTag); - float trackGain = float.NaN; if (trackGainTag is not null) { @@ -302,17 +301,11 @@ namespace MediaBrowser.Providers.MediaInfo if (float.TryParse(trackGainTag, NumberStyles.Float, CultureInfo.InvariantCulture, out var value)) { - trackGain = value; + audio.NormalizationGain = value; } } - if (!float.IsNaN(trackGain)) - { - audio.NormalizationGain = trackGain; - } - - if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _)) - && !string.IsNullOrEmpty(tags.MusicBrainzArtistId)) + if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _)) { if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_ARTISTID", out var musicBrainzArtistTag) || track.AdditionalFields.TryGetValue("MusicBrainz Artist Id", out musicBrainzArtistTag)) @@ -322,8 +315,7 @@ namespace MediaBrowser.Providers.MediaInfo } } - if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _)) - && !string.IsNullOrEmpty(tags.MusicBrainzReleaseArtistId)) + if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _)) { if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_ALBUMARTISTID", out var musicBrainzReleaseArtistIdTag) || track.AdditionalFields.TryGetValue("MusicBrainz Album Artist Id", out musicBrainzReleaseArtistIdTag)) @@ -333,8 +325,7 @@ namespace MediaBrowser.Providers.MediaInfo } } - if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _)) - && !string.IsNullOrEmpty(tags.MusicBrainzReleaseId)) + if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _)) { if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_ALBUMID", out var musicBrainzReleaseIdTag) || track.AdditionalFields.TryGetValue("MusicBrainz Album Id", out musicBrainzReleaseIdTag)) @@ -344,8 +335,7 @@ namespace MediaBrowser.Providers.MediaInfo } } - if ((options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _)) - && !string.IsNullOrEmpty(tags.MusicBrainzReleaseGroupId)) + if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _)) { if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_RELEASEGROUPID", out var musicBrainzReleaseGroupIdTag) || track.AdditionalFields.TryGetValue("MusicBrainz Release Group Id", out musicBrainzReleaseGroupIdTag)) From 0132ad05abf4a158a232d9960bc2c51c4e7842e9 Mon Sep 17 00:00:00 2001 From: gnattu Date: Mon, 29 Jul 2024 07:44:13 +0800 Subject: [PATCH 21/41] Display DOVI title in DisplayTitle when available Signed-off-by: gnattu --- MediaBrowser.Model/Entities/MediaStream.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Model/Entities/MediaStream.cs b/MediaBrowser.Model/Entities/MediaStream.cs index dcb3febbd2..20e011745c 100644 --- a/MediaBrowser.Model/Entities/MediaStream.cs +++ b/MediaBrowser.Model/Entities/MediaStream.cs @@ -196,7 +196,7 @@ namespace MediaBrowser.Model.Entities || dvProfile == 8 || dvProfile == 9)) { - var title = "DV Profile " + dvProfile; + var title = "Dolby Vision Profile " + dvProfile; if (dvBlCompatId > 0) { @@ -208,6 +208,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 }; } @@ -330,7 +331,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()); } From 5f122e7f7903c7460a842d92bac3c887e76f003b Mon Sep 17 00:00:00 2001 From: aky Date: Sun, 28 Jul 2024 05:58:26 +0000 Subject: [PATCH 22/41] Translated using Weblate (Korean) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/ko/ --- Emby.Server.Implementations/Localization/Core/ko.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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": "더 이상 존재하지 않는 컬렉션 및 재생 목록에서 항목을 제거합니다." } From ff72acd1942aa2cde4a595061a54b04f778d4620 Mon Sep 17 00:00:00 2001 From: Gokki Date: Sun, 28 Jul 2024 14:41:52 +0000 Subject: [PATCH 23/41] Translated using Weblate (Filipino) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/fil/ --- Emby.Server.Implementations/Localization/Core/fil.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", From 48b5602144d752b183d260ec255465a436703904 Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Mon, 29 Jul 2024 07:38:15 -0600 Subject: [PATCH 24/41] Enable nullability for QueryResult --- MediaBrowser.Model/Querying/QueryResult.cs | 93 ++++++++++++---------- 1 file changed, 53 insertions(+), 40 deletions(-) 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(); - } - - 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; - } - - /// - /// 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 index of the first record in Items. - /// - /// First record index. - public int StartIndex { get; set; } + Items = Array.Empty(); } + + /// + /// Initializes a new instance of the class. + /// + /// The list of items. + public QueryResult(IReadOnlyList items) + { + Items = items; + TotalRecordCount = items.Count; + } + + /// + /// 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 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; } } From d3f0346f0498538ae3c0f34e4d17a6e1d044b4f8 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Sat, 20 Apr 2024 16:43:25 +0200 Subject: [PATCH 25/41] Enable nullable for UserItemData MetadataResult.GetOrAddUserData doesn't ever get used and is probably broken since the migration to .NET Core as it still expects a Guid for userId --- .../Data/SqliteUserDataRepository.cs | 8 ++--- .../EntryPoints/UserDataChangeNotifier.cs | 5 ++- .../Library/MediaSourceManager.cs | 7 +++-- .../Entities/UserItemData.cs | 10 +----- .../Providers/MetadataResult.cs | 31 ------------------- MediaBrowser.Model/Dto/UserItemDataDto.cs | 5 ++- .../Session/UserDataChangeInfo.cs | 6 ++-- .../Providers/BaseVideoNfoProvider.cs | 5 --- .../Parsers/MovieNfoParserTests.cs | 5 ++- 9 files changed, 20 insertions(+), 62 deletions(-) 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/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/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/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.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/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.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/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() From 2b3ebb0751e752eeb3b1d5baadf3086a09b867d6 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Thu, 25 Apr 2024 12:42:10 +0200 Subject: [PATCH 26/41] Enable nullable for DtoService and DtoOptions --- Emby.Server.Implementations/Dto/DtoService.cs | 62 ++++++++++--------- Jellyfin.Api/Extensions/DtoExtensions.cs | 2 - MediaBrowser.Controller/Dto/DtoOptions.cs | 2 - 3 files changed, 34 insertions(+), 32 deletions(-) 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/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/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; From 63b90ab45ca8c4d85e1930f241ce24a9bff531eb Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Mon, 29 Jul 2024 21:57:11 +0200 Subject: [PATCH 27/41] Fix build --- Jellyfin.Api/Controllers/LiveTvController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index 0cf36f57ec..6c3d011036 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -688,7 +688,7 @@ public class LiveTvController : BaseJellyfinApiController } } - 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 ?? []); return await _liveTvManager.GetPrograms(query, dtoOptions, CancellationToken.None).ConfigureAwait(false); From 3984b828e8088c4d14918310a13623f4a7fe1944 Mon Sep 17 00:00:00 2001 From: vyrmin Date: Mon, 29 Jul 2024 13:46:48 +0000 Subject: [PATCH 28/41] Translated using Weblate (Croatian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/hr/ --- Emby.Server.Implementations/Localization/Core/hr.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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" } From 0a1a109b2e9503213debdc8445910cb4c93ae382 Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 30 Jul 2024 23:50:22 +0800 Subject: [PATCH 29/41] Add RFC7845 downmix algorithm (#12300) --- .../Controllers/DynamicHlsController.cs | 2 +- .../MediaEncoding/DownMixAlgorithmsHelper.cs | 65 +++++++++++++++++++ .../MediaEncoding/EncodingHelper.cs | 30 ++++----- .../Entities/DownMixStereoAlgorithms.cs | 7 +- 4 files changed, 83 insertions(+), 21 deletions(-) create mode 100644 MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs 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/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs b/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs new file mode 100644 index 0000000000..b90f9a4793 --- /dev/null +++ b/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs @@ -0,0 +1,65 @@ +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" }, + { (DownMixStereoAlgorithms.NightmodeDialogue, "5.1"), "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" }, + }; + + /// + /// 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 42b09a29e7..2c3d44bf83 100644 --- a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs @@ -2666,29 +2666,18 @@ 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) { + var hasDownMixFilter = DownMixAlgorithmsHelper.AlgorithmFilterStrings.TryGetValue((encodingOptions.DownMixStereoAlgorithm, DownMixAlgorithmsHelper.InferChannelLayout(state.AudioStream)), out var downMixFilterString); + if (hasDownMixFilter) + { + filters.Add(downMixFilterString); + } + if (!encodingOptions.DownMixAudioBoost.Equals(1)) { filters.Add("volume=" + encodingOptions.DownMixAudioBoost.ToString(CultureInfo.InvariantCulture)); } - - switch (encodingOptions.DownMixStereoAlgorithm) - { - 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; - } } var isCopyingTimestamps = state.CopyTimestamps || state.TranscodingType != TranscodingJobType.Progressive; @@ -7008,7 +6997,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.Model/Entities/DownMixStereoAlgorithms.cs b/MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs index 385cd6a34e..be43ef32d3 100644 --- a/MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs +++ b/MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs @@ -19,5 +19,10 @@ public enum DownMixStereoAlgorithms /// /// Nightmode Dialogue algorithm. /// - NightmodeDialogue = 2 + NightmodeDialogue = 2, + + /// + /// RFC7845 Section 5.1.1.5 defined algorithm. + /// + Rfc7845 = 3 } From d4eeafe53ff8ae2f287f5dca49a873cd71f4c3da Mon Sep 17 00:00:00 2001 From: TheMelmacian <76712303+TheMelmacian@users.noreply.github.com> Date: Tue, 30 Jul 2024 17:51:08 +0200 Subject: [PATCH 30/41] Fix: parsing of xbmc style multi episode nfo files (#12268) --- CONTRIBUTORS.md | 1 + .../Parsers/EpisodeNfoParser.cs | 106 +++++++++--------- .../Parsers/EpisodeNfoProviderTests.cs | 24 ++++ .../Test Data/Rising.nfo | 24 ++++ .../Stargate Atlantis S01E01-E04.nfo | 89 +++++++++++++++ 5 files changed, 190 insertions(+), 54 deletions(-) create mode 100644 tests/Jellyfin.XbmcMetadata.Tests/Test Data/Stargate Atlantis S01E01-E04.nfo 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/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() { - reader.MoveToContent(); + Item = new Episode() + }; - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - cancellationToken.ThrowIfCancellationRequested(); + // Extract episode details from additional episodedetails block + ReadEpisodeDetailsFromXml(additionalEpisode, xml, settings, cancellationToken); - 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); - } + if (!string.IsNullOrEmpty(additionalEpisode.Item.Name)) + { + name.Append(" / ").Append(additionalEpisode.Item.Name); + } - break; - } + if (!string.IsNullOrEmpty(additionalEpisode.Item.Overview)) + { + overview.Append(" / ").Append(additionalEpisode.Item.Overview); + } - case "biography": - case "plot": - case "review": - overview.Append(" / ").Append(reader.ReadElementContentAsString()); - break; - } - } + if (!string.IsNullOrEmpty(additionalEpisode.Item.OriginalTitle)) + { + originalTitle.Append(" / ").Append(additionalEpisode.Item.OriginalTitle); + } - reader.Read(); - } + if (additionalEpisode.Item.IndexNumber != null) + { + 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/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); + // <originaltitle> 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); + // <plot> 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/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 @@ <thumb>https://artworks.thetvdb.com/banners/episodes/70851/25333.jpg</thumb> <watched>false</watched> <rating>8.0</rating> + <actor> + <name>Joe Flanigan</name> + <role>John Sheppard</role> + <order>0</order> + <thumb>https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg</thumb> + </actor> + <actor> + <name>David Hewlett</name> + <role>Rodney McKay</role> + <order>1</order> + <thumb>https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg</thumb> + </actor> </episodedetails> <episodedetails> <title>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 + + + From daf8d649f2bcadddabb2293fdadee9df436e7bf1 Mon Sep 17 00:00:00 2001 From: gnattu Date: Mon, 29 Jul 2024 06:19:04 +0800 Subject: [PATCH 31/41] Add AC4 downmix Signed-off-by: gnattu --- .../MediaEncoding/DownMixAlgorithmsHelper.cs | 5 +++++ .../Entities/DownMixStereoAlgorithms.cs | 13 ++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs b/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs index b90f9a4793..5d253184a7 100644 --- a/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs @@ -15,13 +15,18 @@ public static class DownMixAlgorithmsHelper 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" }, + { (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" }, + { (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.1"), "pan=stereo|c0=c0+0.707*c2+0.707*c4|c1=c1+0.707*c2+0.707*c5"}, + { (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"}, }; /// diff --git a/MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs b/MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs index be43ef32d3..dcb2c802d9 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,16 +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, /// /// RFC7845 Section 5.1.1.5 defined algorithm. /// - Rfc7845 = 3 + Rfc7845 = 3, + + /// + /// AC-4 standard algorithm with its default gain values. + /// Defined in ETSI TS 103 190 Section 6.2.17 + /// + Ac4 = 4 } From 31dccaca0f8d3d924756286af54c70101155cffb Mon Sep 17 00:00:00 2001 From: gnattu Date: Mon, 29 Jul 2024 10:04:44 +0800 Subject: [PATCH 32/41] Add 5.0 and 7.0 support to ac4 downmix Signed-off-by: gnattu --- .../MediaEncoding/DownMixAlgorithmsHelper.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs b/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs index 5d253184a7..7583961e96 100644 --- a/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs @@ -25,7 +25,9 @@ public static class DownMixAlgorithmsHelper { (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"}, }; From 8f28c3a0fbdd285288de76b5213b1462005eb7dc Mon Sep 17 00:00:00 2001 From: gnattu Date: Mon, 29 Jul 2024 10:22:51 +0800 Subject: [PATCH 33/41] fix doc Signed-off-by: gnattu --- MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs b/MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs index dcb2c802d9..0c03d430dc 100644 --- a/MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs +++ b/MediaBrowser.Model/Entities/DownMixStereoAlgorithms.cs @@ -29,7 +29,7 @@ public enum DownMixStereoAlgorithms /// /// AC-4 standard algorithm with its default gain values. - /// Defined in ETSI TS 103 190 Section 6.2.17 + /// Defined in ETSI TS 103 190 Section 6.2.17. /// Ac4 = 4 } From c171b6def22b32740654d7830f4ef6440854c404 Mon Sep 17 00:00:00 2001 From: gnattu Date: Mon, 29 Jul 2024 10:28:22 +0800 Subject: [PATCH 34/41] fix space Signed-off-by: gnattu --- .../MediaEncoding/DownMixAlgorithmsHelper.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs b/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs index 7583961e96..64a0f59920 100644 --- a/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs @@ -24,11 +24,11 @@ public static class DownMixAlgorithmsHelper { (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"}, + { (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" }, }; /// From bb12d8240f7361c114c56f38d63c9cd64b510d6c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 31 Jul 2024 23:05:30 +0000 Subject: [PATCH 35/41] Update dependency Serilog.AspNetCore to v8.0.2 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index f9887dc798..683f2eee32 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -58,7 +58,7 @@ - + From 3788ccd447ffaa4638f3fe5d235c5bb92b809692 Mon Sep 17 00:00:00 2001 From: gnattu Date: Thu, 1 Aug 2024 07:45:16 +0800 Subject: [PATCH 36/41] Add comments for Dave750/NightmodeDialogue 7.1 downmix Signed-off-by: gnattu --- .../MediaEncoding/DownMixAlgorithmsHelper.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs b/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs index 64a0f59920..749f872710 100644 --- a/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs +++ b/MediaBrowser.Controller/MediaEncoding/DownMixAlgorithmsHelper.cs @@ -15,8 +15,10 @@ public static class DownMixAlgorithmsHelper 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" }, From 0a0de6708e0b900ca616b108dfdf94894a56a6e0 Mon Sep 17 00:00:00 2001 From: Bond_009 Date: Thu, 1 Aug 2024 17:17:10 +0200 Subject: [PATCH 37/41] Enable more analyser rules as errors Also deduplicates a bit of code inside of SeasonPathParser and adds some more tests --- Emby.Naming/TV/SeasonPathParser.cs | 15 +++----- MediaBrowser.Model/Dlna/StreamBuilder.cs | 2 +- jellyfin.ruleset | 22 +++++++++++ .../TV/SeasonFolderTests.cs | 35 ------------------ .../TV/SeasonPathParserTests.cs | 37 +++++++++++++++++++ 5 files changed, 66 insertions(+), 45 deletions(-) delete mode 100644 tests/Jellyfin.Naming.Tests/TV/SeasonFolderTests.cs create mode 100644 tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs 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/MediaBrowser.Model/Dlna/StreamBuilder.cs b/MediaBrowser.Model/Dlna/StreamBuilder.cs index 4815dcc044..d2715e2ac7 100644 --- a/MediaBrowser.Model/Dlna/StreamBuilder.cs +++ b/MediaBrowser.Model/Dlna/StreamBuilder.cs @@ -1304,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/jellyfin.ruleset b/jellyfin.ruleset index db116f46c8..67ffd9a37b 100644 --- a/jellyfin.ruleset +++ b/jellyfin.ruleset @@ -105,6 +105,28 @@ + + + + + + + + + + + + + + + + + + + + + + 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); + } +} From 894933848ab6daf4176419dc31d914f87a8a071f Mon Sep 17 00:00:00 2001 From: Andrejs Date: Fri, 2 Aug 2024 07:39:38 +0000 Subject: [PATCH 38/41] Translated using Weblate (Latvian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/lv/ --- Emby.Server.Implementations/Localization/Core/lv.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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" } From 95f41b6a73aed7928d652b33930c1db61a9b052b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 2 Aug 2024 15:52:17 +0000 Subject: [PATCH 39/41] Update actions/upload-artifact action to v4.3.5 --- .github/workflows/ci-openapi.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From bbf3a2138b77ac5d818d396afe571a41fcb1fe43 Mon Sep 17 00:00:00 2001 From: Franco Castillo Date: Sat, 3 Aug 2024 21:04:49 +0000 Subject: [PATCH 40/41] Translated using Weblate (Spanish (Argentina)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/es_AR/ --- Emby.Server.Implementations/Localization/Core/es-AR.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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." } From 7ea91dfcc4030892fff164d49969f6e85c8493fe Mon Sep 17 00:00:00 2001 From: gnattu Date: Mon, 5 Aug 2024 10:37:40 +0800 Subject: [PATCH 41/41] Update VideoToolbox pipeline for jellyfin-ffmpeg7 (#12380) --- .../MediaEncoding/EncodingHelper.cs | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs b/MediaBrowser.Controller/MediaEncoding/EncodingHelper.cs index 2c3d44bf83..bfeabebde8 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 static readonly Regex _validationRegex = new(ValidationRegex, RegexOptions.Compiled); @@ -5166,12 +5167,14 @@ namespace MediaBrowser.Controller.MediaEncoding var threeDFormat = state.MediaSource.Video3DFormat; var isVtEncoder = vidEncoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase); + var isVtDecoder = vidDecoder.Contains("videotoolbox", StringComparison.OrdinalIgnoreCase); var doDeintH264 = state.DeInterlace("h264", true) || state.DeInterlace("avc", true); var doDeintHevc = state.DeInterlace("h265", true) || state.DeInterlace("hevc", true); var doDeintH2645 = doDeintH264 || doDeintHevc; var doVtTonemap = IsVideoToolboxTonemapAvailable(state, options); var doMetalTonemap = !doVtTonemap && IsHwTonemapAvailable(state, options); + var usingHwSurface = isVtDecoder && (_mediaEncoder.EncoderVersion >= _minFFmpegWorkingVtHwSurface); var scaleFormat = string.Empty; // Use P010 for Metal tone mapping, otherwise force an 8bit output. @@ -5259,23 +5262,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); @@ -6283,22 +6288,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); } } @@ -6307,12 +6310,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); } }