From 6dc61a430ba3a8480399309f277e5debfd6403ba Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Mon, 15 May 2023 00:38:27 -0500 Subject: [PATCH 01/56] Sort embedded collections in Nfo files Because the Nfo files emit the collections as they are in-memory, the files are not stable in format, genres, tags, albums, people, etc. are emitted in random orders. Add ordering of the collections when emitting the Nfo files so the file remains stable (unchanged) when underlying media information doesn't change. In the process of this, it became clear that most of the providers and probes don't trim the strings like people's names, genre names, etc. so did a pass of Trim cleanup too. Specific ordering: (alphabetical/numeric ascending after trimming blanks and defaulting to zero for missing numbers) BaseItem: Directors, Writers, Trailers (by Url), Production Locations, Genres, Studios, Tags, Custom Provider Data (by key), Linked Children (by Path>LibraryItemId), Backdrop Images (by path), Actors (by SortOrder>Name) AlbumNfo: Artists, Album Artists, Tracks (by ParentIndexNumber>IndexNumber>Name) ArtistNfo: Albums (by Production Year>SortName>Name) MovieNfo: Artists Fix Debug build lint Fix CI debug build lint issue. Fix review issues Fixed debug-build lint issues. Emits the `disc` number to NFO for tracks with a non-zero ParentIndexNumber and only emit `position` if non-zero. Removed the exception filtering I put in for testing. Don't emit actors for MusicAlbums or MusicArtists Swap from String.Trimmed() to ?.Trim() Addressing PR feedback Can't use ReadOnlySpan in an async method Removed now-unused namespace --- MediaBrowser.Controller/Entities/BaseItem.cs | 6 ++-- .../Entities/PeopleHelper.cs | 2 ++ .../Sorting/SortExtensions.cs | 5 +++ .../Parsers/BaseItemXmlParser.cs | 27 ++++++++------ .../Probing/ProbeResultNormalizer.cs | 34 ++++++++++-------- .../MediaInfo/AudioFileProber.cs | 21 ++++++----- .../MediaInfo/FFProbeVideoInfo.cs | 7 ++-- .../Music/AlbumMetadataService.cs | 4 +-- .../Plugins/Omdb/OmdbProvider.cs | 4 +-- .../Plugins/Tmdb/Movies/TmdbMovieProvider.cs | 7 ++-- .../Plugins/Tmdb/TV/TmdbEpisodeProvider.cs | 6 ++-- .../Plugins/Tmdb/TV/TmdbSeasonProvider.cs | 9 ++--- .../Plugins/Tmdb/TV/TmdbSeriesProvider.cs | 4 +-- .../Savers/AlbumNfoSaver.cs | 17 ++++++--- .../Savers/ArtistNfoSaver.cs | 7 +++- .../Savers/BaseNfoSaver.cs | 35 ++++++++++++------- .../Savers/MovieNfoSaver.cs | 3 +- 17 files changed, 123 insertions(+), 75 deletions(-) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 414488853f..8201ae318b 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -22,6 +22,7 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; @@ -915,7 +916,7 @@ namespace MediaBrowser.Controller.Entities // Remove from middle if surrounded by spaces sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal); - // Remove from end if followed by a space + // Remove from end if preceeded by a space if (sortable.EndsWith(" " + search, StringComparison.Ordinal)) { sortable = sortable.Remove(sortable.Length - (search.Length + 1)); @@ -1769,7 +1770,6 @@ namespace MediaBrowser.Controller.Entities public void AddStudio(string name) { ArgumentException.ThrowIfNullOrEmpty(name); - var current = Studios; if (!current.Contains(name, StringComparison.OrdinalIgnoreCase)) @@ -1788,7 +1788,7 @@ namespace MediaBrowser.Controller.Entities public void SetStudios(IEnumerable names) { - Studios = names.Distinct().ToArray(); + Studios = names.Trimmed().Distinct().ToArray(); } /// diff --git a/MediaBrowser.Controller/Entities/PeopleHelper.cs b/MediaBrowser.Controller/Entities/PeopleHelper.cs index 5292bd7727..d818604365 100644 --- a/MediaBrowser.Controller/Entities/PeopleHelper.cs +++ b/MediaBrowser.Controller/Entities/PeopleHelper.cs @@ -15,6 +15,8 @@ namespace MediaBrowser.Controller.Entities ArgumentNullException.ThrowIfNull(person); ArgumentException.ThrowIfNullOrEmpty(person.Name); + person.Name = person.Name.Trim(); + // Normalize if (string.Equals(person.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase)) { diff --git a/MediaBrowser.Controller/Sorting/SortExtensions.cs b/MediaBrowser.Controller/Sorting/SortExtensions.cs index f9c0d39ddd..db934e0f47 100644 --- a/MediaBrowser.Controller/Sorting/SortExtensions.cs +++ b/MediaBrowser.Controller/Sorting/SortExtensions.cs @@ -30,5 +30,10 @@ namespace MediaBrowser.Controller.Sorting { return list.ThenByDescending(getName, _comparer); } + + public static IEnumerable Trimmed(this IEnumerable values) + { + return values.Select(i => (i ?? string.Empty).Trim()); + } } } diff --git a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs index e4ac59b676..119effe791 100644 --- a/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs +++ b/MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs @@ -234,8 +234,8 @@ namespace MediaBrowser.LocalMetadata.Parsers item.CustomRating = reader.ReadNormalizedString(); break; case "RunningTime": - var runtimeText = reader.ReadElementContentAsString(); - if (!string.IsNullOrWhiteSpace(runtimeText)) + var runtimeText = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(runtimeText)) { if (int.TryParse(runtimeText.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime)) { @@ -253,7 +253,7 @@ namespace MediaBrowser.LocalMetadata.Parsers break; case "LockData": - item.IsLocked = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase); + item.IsLocked = string.Equals(reader.ReadNormalizedString(), "true", StringComparison.OrdinalIgnoreCase); break; case "Network": foreach (var name in reader.GetStringArray()) @@ -331,9 +331,9 @@ namespace MediaBrowser.LocalMetadata.Parsers case "Rating": case "IMDBrating": { - var rating = reader.ReadElementContentAsString(); + var rating = reader.ReadNormalizedString(); - if (!string.IsNullOrWhiteSpace(rating)) + if (!string.IsNullOrEmpty(rating)) { // All external meta is saving this as '.' for decimal I believe...but just to be sure if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val)) @@ -449,7 +449,7 @@ namespace MediaBrowser.LocalMetadata.Parsers case "OwnerUserId": { - var val = reader.ReadElementContentAsString(); + var val = reader.ReadNormalizedString(); if (Guid.TryParse(val, out var guid) && !guid.Equals(Guid.Empty)) { @@ -464,7 +464,7 @@ namespace MediaBrowser.LocalMetadata.Parsers case "Format3D": { - var val = reader.ReadElementContentAsString(); + var val = reader.ReadNormalizedString(); if (item is Video video) { @@ -498,7 +498,7 @@ namespace MediaBrowser.LocalMetadata.Parsers string readerName = reader.Name; if (_validProviderIds!.TryGetValue(readerName, out string? providerIdValue)) { - var id = reader.ReadElementContentAsString(); + var id = reader.ReadNormalizedString(); item.TrySetProviderId(providerIdValue, id); } else @@ -580,7 +580,12 @@ namespace MediaBrowser.LocalMetadata.Parsers switch (reader.Name) { case "Tagline": - item.Tagline = reader.ReadNormalizedString(); + var val = reader.ReadNormalizedString(); + if (!string.IsNullOrEmpty(val)) + { + item.Tagline = val; + } + break; default: reader.Skip(); @@ -842,7 +847,7 @@ namespace MediaBrowser.LocalMetadata.Parsers userId = reader.ReadNormalizedString(); break; case "CanEdit": - canEdit = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase); + canEdit = string.Equals(reader.ReadNormalizedString(), "true", StringComparison.OrdinalIgnoreCase); break; default: reader.Skip(); @@ -856,7 +861,7 @@ namespace MediaBrowser.LocalMetadata.Parsers } // This is valid - if (!string.IsNullOrWhiteSpace(userId) && Guid.TryParse(userId, out var guid)) + if (!string.IsNullOrEmpty(userId) && Guid.TryParse(userId, out var guid)) { return new PlaylistUserPermissions(guid, canEdit); } diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index 334796f585..0dee77db81 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -10,7 +10,9 @@ using System.Text.RegularExpressions; using System.Xml; using Jellyfin.Data.Enums; using Jellyfin.Extensions; +using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; @@ -531,42 +533,44 @@ namespace MediaBrowser.MediaEncoding.Probing private void ProcessPairs(string key, List pairs, MediaInfo info) { List peoples = new List(); + var distinctPairs = pairs.Select(p => p.Value) + .Where(i => !string.IsNullOrWhiteSpace(i)) + .Trimmed() + .Distinct(StringComparer.OrdinalIgnoreCase); + if (string.Equals(key, "studio", StringComparison.OrdinalIgnoreCase)) { - info.Studios = pairs.Select(p => p.Value) - .Where(i => !string.IsNullOrWhiteSpace(i)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); + info.Studios = distinctPairs.ToArray(); } else if (string.Equals(key, "screenwriters", StringComparison.OrdinalIgnoreCase)) { - foreach (var pair in pairs) + foreach (var pair in distinctPairs) { peoples.Add(new BaseItemPerson { - Name = pair.Value, + Name = pair, Type = PersonKind.Writer }); } } else if (string.Equals(key, "producers", StringComparison.OrdinalIgnoreCase)) { - foreach (var pair in pairs) + foreach (var pair in distinctPairs) { peoples.Add(new BaseItemPerson { - Name = pair.Value, + Name = pair, Type = PersonKind.Producer }); } } else if (string.Equals(key, "directors", StringComparison.OrdinalIgnoreCase)) { - foreach (var pair in pairs) + foreach (var pair in distinctPairs) { peoples.Add(new BaseItemPerson { - Name = pair.Value, + Name = pair, Type = PersonKind.Director }); } @@ -591,10 +595,10 @@ namespace MediaBrowser.MediaEncoding.Probing switch (reader.Name) { case "key": - name = reader.ReadElementContentAsString(); + name = reader.ReadNormalizedString(); break; case "string": - value = reader.ReadElementContentAsString(); + value = reader.ReadNormalizedString(); break; default: reader.Skip(); @@ -607,8 +611,8 @@ namespace MediaBrowser.MediaEncoding.Probing } } - if (string.IsNullOrWhiteSpace(name) - || string.IsNullOrWhiteSpace(value)) + if (string.IsNullOrEmpty(name) + || string.IsNullOrEmpty(value)) { return null; } @@ -1453,7 +1457,7 @@ namespace MediaBrowser.MediaEncoding.Probing var genres = new List(info.Genres); foreach (var genre in Split(genreVal, true)) { - if (string.IsNullOrWhiteSpace(genre)) + if (string.IsNullOrEmpty(genre)) { continue; } diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 80bb1a514c..d113cabc7f 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -13,6 +13,7 @@ using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; @@ -183,11 +184,11 @@ namespace MediaBrowser.Providers.MediaInfo foreach (var albumArtist in albumArtists) { - if (!string.IsNullOrEmpty(albumArtist)) + if (!string.IsNullOrWhiteSpace(albumArtist)) { PeopleHelper.AddPerson(people, new PersonInfo { - Name = albumArtist, + Name = albumArtist.Trim(), Type = PersonKind.AlbumArtist }); } @@ -215,11 +216,11 @@ namespace MediaBrowser.Providers.MediaInfo foreach (var performer in performers) { - if (!string.IsNullOrEmpty(performer)) + if (!string.IsNullOrWhiteSpace(performer)) { PeopleHelper.AddPerson(people, new PersonInfo { - Name = performer, + Name = performer.Trim(), Type = PersonKind.Artist }); } @@ -227,11 +228,11 @@ namespace MediaBrowser.Providers.MediaInfo foreach (var composer in track.Composer.Split(InternalValueSeparator)) { - if (!string.IsNullOrEmpty(composer)) + if (!string.IsNullOrWhiteSpace(composer)) { PeopleHelper.AddPerson(people, new PersonInfo { - Name = composer, + Name = composer.Trim(), Type = PersonKind.Composer }); } @@ -273,13 +274,13 @@ namespace MediaBrowser.Providers.MediaInfo if (options.ReplaceAllMetadata) { - audio.Album = track.Album; + audio.Album = track.Album.Trim(); audio.IndexNumber = track.TrackNumber; audio.ParentIndexNumber = track.DiscNumber; } else { - audio.Album ??= track.Album; + audio.Album ??= track.Album.Trim(); audio.IndexNumber ??= track.TrackNumber; audio.ParentIndexNumber ??= track.DiscNumber; } @@ -309,13 +310,15 @@ namespace MediaBrowser.Providers.MediaInfo if (!audio.LockedFields.Contains(MetadataField.Genres)) { - var genres = string.IsNullOrEmpty(track.Genre) ? mediaInfo.Genres : track.Genre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + var genres = string.IsNullOrEmpty(track.Genre) ? mediaInfo.Genres : track.Genre.Split(InternalValueSeparator); if (libraryOptions.UseCustomTagDelimiters) { genres = genres.SelectMany(g => SplitWithCustomDelimiter(g, libraryOptions.CustomTagDelimiters, libraryOptions.DelimiterWhitelist)).ToArray(); } + genres = genres.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); + audio.Genres = options.ReplaceAllMetadata || audio.Genres is null || audio.Genres.Length == 0 ? genres : audio.Genres; diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 246ba2733f..f486f150da 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -16,6 +16,7 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; @@ -400,7 +401,7 @@ namespace MediaBrowser.Providers.MediaInfo { video.Genres = Array.Empty(); - foreach (var genre in data.Genres) + foreach (var genre in data.Genres.Trimmed()) { video.AddGenre(genre); } @@ -509,9 +510,9 @@ namespace MediaBrowser.Providers.MediaInfo { PeopleHelper.AddPerson(people, new PersonInfo { - Name = person.Name, + Name = person.Name.Trim(), Type = person.Type, - Role = person.Role + Role = person.Role.Trim() }); } diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs index a39bd16cea..daebe85d69 100644 --- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs +++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs @@ -187,7 +187,7 @@ namespace MediaBrowser.Providers.Music { PeopleHelper.AddPerson(people, new PersonInfo { - Name = albumArtist, + Name = albumArtist.Trim(), Type = PersonKind.AlbumArtist }); } @@ -196,7 +196,7 @@ namespace MediaBrowser.Providers.Music { PeopleHelper.AddPerson(people, new PersonInfo { - Name = artist, + Name = artist.Trim(), Type = PersonKind.Artist }); } diff --git a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs index de0da7f7bd..ad9edb031c 100644 --- a/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs +++ b/MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs @@ -421,7 +421,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb { var person = new PersonInfo { - Name = result.Director, + Name = result.Director.Trim(), Type = PersonKind.Director }; @@ -432,7 +432,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb { var person = new PersonInfo { - Name = result.Writer, + Name = result.Writer.Trim(), Type = PersonKind.Writer }; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index 8d68e2dcfe..582e05b793 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -12,6 +12,7 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; using TMDbLib.Objects.Find; @@ -234,7 +235,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies var genres = movieResult.Genres; - foreach (var genre in genres.Select(g => g.Name)) + foreach (var genre in genres.Select(g => g.Name).Trimmed()) { movie.AddGenre(genre); } @@ -254,7 +255,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies var personInfo = new PersonInfo { Name = actor.Name.Trim(), - Role = actor.Character, + Role = actor.Character.Trim(), Type = PersonKind.Actor, SortOrder = actor.Order }; @@ -289,7 +290,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies var personInfo = new PersonInfo { Name = person.Name.Trim(), - Role = person.Job, + Role = person.Job?.Trim(), Type = type }; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs index e628abde55..4ee1645531 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs @@ -211,7 +211,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV metadataResult.AddPerson(new PersonInfo { Name = actor.Name.Trim(), - Role = actor.Character, + Role = actor.Character.Trim(), Type = PersonKind.Actor, SortOrder = actor.Order }); @@ -225,7 +225,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV metadataResult.AddPerson(new PersonInfo { Name = guest.Name.Trim(), - Role = guest.Character, + Role = guest.Character.Trim(), Type = PersonKind.GuestStar, SortOrder = guest.Order }); @@ -249,7 +249,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV metadataResult.AddPerson(new PersonInfo { Name = person.Name.Trim(), - Role = person.Job, + Role = person.Job?.Trim(), Type = type }); } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs index 3f208b5993..b0a1e00df9 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeasonProvider.cs @@ -82,12 +82,13 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var cast = credits.Cast.OrderBy(c => c.Order).Take(Plugin.Instance.Configuration.MaxCastMembers).ToList(); for (var i = 0; i < cast.Count; i++) { + var member = cast[i]; result.AddPerson(new PersonInfo { - Name = cast[i].Name.Trim(), - Role = cast[i].Character, + Name = member.Name.Trim(), + Role = member.Character.Trim(), Type = PersonKind.Actor, - SortOrder = cast[i].Order + SortOrder = member.Order }); } } @@ -108,7 +109,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV result.AddPerson(new PersonInfo { Name = person.Name.Trim(), - Role = person.Job, + Role = person.Job?.Trim(), Type = type }); } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs index e4062740fe..9ace9c6743 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs @@ -330,7 +330,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV var personInfo = new PersonInfo { Name = actor.Name.Trim(), - Role = actor.Character, + Role = actor.Character.Trim(), Type = PersonKind.Actor, SortOrder = actor.Order, ImageUrl = _tmdbClientManager.GetPosterUrl(actor.ProfilePath) @@ -368,7 +368,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV yield return new PersonInfo { Name = person.Name.Trim(), - Role = person.Job, + Role = person.Job?.Trim(), Type = type }; } diff --git a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs index 2385e70485..4cb6f81b73 100644 --- a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs @@ -8,6 +8,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; @@ -55,12 +56,12 @@ namespace MediaBrowser.XbmcMetadata.Savers { var album = (MusicAlbum)item; - foreach (var artist in album.Artists) + foreach (var artist in album.Artists.Trimmed().OrderBy(artist => artist)) { writer.WriteElementString("artist", artist); } - foreach (var artist in album.AlbumArtists) + foreach (var artist in album.AlbumArtists.Trimmed().OrderBy(artist => artist)) { writer.WriteElementString("albumartist", artist); } @@ -70,11 +71,19 @@ namespace MediaBrowser.XbmcMetadata.Savers private void AddTracks(IEnumerable tracks, XmlWriter writer) { - foreach (var track in tracks.OrderBy(i => i.ParentIndexNumber ?? 0).ThenBy(i => i.IndexNumber ?? 0)) + foreach (var track in tracks + .OrderBy(i => i.ParentIndexNumber ?? 0) + .ThenBy(i => i.IndexNumber ?? 0) + .ThenBy(i => i.Name?.Trim())) { writer.WriteStartElement("track"); - if (track.IndexNumber.HasValue) + if (track.ParentIndexNumber.HasValue && track.ParentIndexNumber.Value != 0) + { + writer.WriteElementString("disc", track.ParentIndexNumber.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (track.IndexNumber.HasValue && track.IndexNumber.Value != 0) { writer.WriteElementString("position", track.IndexNumber.Value.ToString(CultureInfo.InvariantCulture)); } diff --git a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs index 813d75f6c1..e13ba9385f 100644 --- a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs @@ -1,11 +1,13 @@ using System.Collections.Generic; using System.Globalization; using System.IO; +using System.Linq; using System.Xml; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.IO; using MediaBrowser.XbmcMetadata.Configuration; using Microsoft.Extensions.Logging; @@ -69,7 +71,10 @@ namespace MediaBrowser.XbmcMetadata.Savers private void AddAlbums(IList albums, XmlWriter writer) { - foreach (var album in albums) + foreach (var album in albums + .OrderBy(album => album.ProductionYear ?? 0) + .ThenBy(album => album.SortName?.Trim()) + .ThenBy(album => album.Name?.Trim())) { writer.WriteStartElement("album"); diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index 2afec3f6cd..7c94b25c4c 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -19,6 +19,7 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; @@ -488,7 +489,9 @@ namespace MediaBrowser.XbmcMetadata.Savers var directors = people .Where(i => i.IsType(PersonKind.Director)) - .Select(i => i.Name) + .Select(i => i.Name?.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(i => i) .ToList(); foreach (var person in directors) @@ -498,8 +501,9 @@ namespace MediaBrowser.XbmcMetadata.Savers var writers = people .Where(i => i.IsType(PersonKind.Writer)) - .Select(i => i.Name) + .Select(i => i.Name?.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(i => i) .ToList(); foreach (var person in writers) @@ -512,7 +516,7 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteElementString("credits", person); } - foreach (var trailer in item.RemoteTrailers) + foreach (var trailer in item.RemoteTrailers.OrderBy(t => t.Url?.Trim())) { writer.WriteElementString("trailer", GetOutputTrailerUrl(trailer.Url)); } @@ -660,22 +664,22 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteElementString("tagline", item.Tagline); } - foreach (var country in item.ProductionLocations) + foreach (var country in item.ProductionLocations.Trimmed().OrderBy(country => country)) { writer.WriteElementString("country", country); } - foreach (var genre in item.Genres) + foreach (var genre in item.Genres.Trimmed().OrderBy(genre => genre)) { writer.WriteElementString("genre", genre); } - foreach (var studio in item.Studios) + foreach (var studio in item.Studios.Trimmed().OrderBy(studio => studio)) { writer.WriteElementString("studio", studio); } - foreach (var tag in item.Tags) + foreach (var tag in item.Tags.Trimmed().OrderBy(tag => tag)) { if (item is MusicAlbum || item is MusicArtist) { @@ -752,7 +756,7 @@ namespace MediaBrowser.XbmcMetadata.Savers if (item.ProviderIds is not null) { - foreach (var providerKey in item.ProviderIds.Keys) + foreach (var providerKey in item.ProviderIds.Keys.OrderBy(providerKey => providerKey)) { var providerId = item.ProviderIds[providerKey]; if (!string.IsNullOrEmpty(providerId) && !writtenProviderIds.Contains(providerKey)) @@ -764,7 +768,7 @@ namespace MediaBrowser.XbmcMetadata.Savers XmlConvert.VerifyName(tagName); Logger.LogDebug("Saving custom provider tagname {0}", tagName); - writer.WriteElementString(GetTagForProviderKey(providerKey), providerId); + writer.WriteElementString(tagName, providerId); } catch (ArgumentException) { @@ -785,7 +789,10 @@ namespace MediaBrowser.XbmcMetadata.Savers AddUserData(item, writer, userManager, userDataRepo, options); - AddActors(people, writer, libraryManager, options.SaveImagePathsInNfo); + if (item is not MusicAlbum && item is not MusicArtist) + { + AddActors(people, writer, libraryManager, options.SaveImagePathsInNfo); + } if (item is BoxSet folder) { @@ -797,6 +804,8 @@ namespace MediaBrowser.XbmcMetadata.Savers { var items = item.LinkedChildren .Where(i => i.Type == LinkedChildType.Manual) + .OrderBy(i => i.Path?.Trim()) + .ThenBy(i => i.LibraryItemId?.Trim()) .ToList(); foreach (var link in items) @@ -839,7 +848,7 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteElementString("poster", GetImagePathToSave(image, libraryManager)); } - foreach (var backdrop in item.GetImages(ImageType.Backdrop)) + foreach (var backdrop in item.GetImages(ImageType.Backdrop).OrderBy(b => b.Path?.Trim())) { writer.WriteElementString("fanart", GetImagePathToSave(backdrop, libraryManager)); } @@ -913,7 +922,9 @@ namespace MediaBrowser.XbmcMetadata.Savers private void AddActors(List people, XmlWriter writer, ILibraryManager libraryManager, bool saveImagePath) { - foreach (var person in people) + foreach (var person in people + .OrderBy(person => person.SortOrder ?? 0) + .ThenBy(person => person.Name?.Trim())) { if (person.IsType(PersonKind.Director) || person.IsType(PersonKind.Writer)) { diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs index bc344d87e0..3ff8749e74 100644 --- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs @@ -8,6 +8,7 @@ using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; +using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; @@ -100,7 +101,7 @@ namespace MediaBrowser.XbmcMetadata.Savers if (item is MusicVideo musicVideo) { - foreach (var artist in musicVideo.Artists) + foreach (var artist in musicVideo.Artists.Trimmed().OrderBy(artist => artist)) { writer.WriteElementString("artist", artist); } From 1e7acec01799e3cfe6fd2a8630dbd8f6e3338251 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 26 Oct 2024 10:31:01 +0000 Subject: [PATCH 02/56] Added Setup overlay app to communicate status of startup --- Jellyfin.Server/Program.cs | 25 +++- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 131 ++++++++++++++++++ .../Manager/NetworkManager.cs | 23 ++- 3 files changed, 167 insertions(+), 12 deletions(-) create mode 100644 Jellyfin.Server/ServerSetupApp/SetupServer.cs diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 295fb8112f..0bbcfa6a64 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -7,10 +7,13 @@ using System.Reflection; using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; +using Jellyfin.Networking.Manager; using Jellyfin.Server.Extensions; using Jellyfin.Server.Helpers; using Jellyfin.Server.Implementations; +using Jellyfin.Server.ServerSetupApp; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; using MediaBrowser.Controller; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; @@ -42,6 +45,9 @@ namespace Jellyfin.Server public const string LoggingConfigFileSystem = "logging.json"; private static readonly SerilogLoggerFactory _loggerFactory = new SerilogLoggerFactory(); + private static SetupServer? _setupServer = new(); + + private static IHost? _jfHost = null; private static long _startTimestamp; private static ILogger _logger = NullLogger.Instance; private static bool _restartOnShutdown; @@ -68,6 +74,7 @@ namespace Jellyfin.Server { _startTimestamp = Stopwatch.GetTimestamp(); ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options); + await _setupServer!.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths).ConfigureAwait(false); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); @@ -122,6 +129,8 @@ namespace Jellyfin.Server if (_restartOnShutdown) { _startTimestamp = Stopwatch.GetTimestamp(); + _setupServer = new SetupServer(); + await _setupServer.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths).ConfigureAwait(false); } } while (_restartOnShutdown); } @@ -133,11 +142,9 @@ namespace Jellyfin.Server _loggerFactory, options, startupConfig); - - IHost? host = null; try { - host = Host.CreateDefaultBuilder() + _jfHost = Host.CreateDefaultBuilder() .UseConsoleLifetime() .ConfigureServices(services => appHost.Init(services)) .ConfigureWebHostDefaults(webHostBuilder => @@ -154,14 +161,18 @@ namespace Jellyfin.Server .Build(); // Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection. - appHost.ServiceProvider = host.Services; + appHost.ServiceProvider = _jfHost.Services; await appHost.InitializeServices().ConfigureAwait(false); Migrations.MigrationRunner.Run(appHost, _loggerFactory); try { - await host.StartAsync().ConfigureAwait(false); + await Task.Delay(50000).ConfigureAwait(false); + await _setupServer!.StopAsync().ConfigureAwait(false); + _setupServer.Dispose(); + _setupServer = null!; + await _jfHost.StartAsync().ConfigureAwait(false); if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket()) { @@ -180,7 +191,7 @@ namespace Jellyfin.Server _logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp)); - await host.WaitForShutdownAsync().ConfigureAwait(false); + await _jfHost.WaitForShutdownAsync().ConfigureAwait(false); _restartOnShutdown = appHost.ShouldRestart; } catch (Exception ex) @@ -205,7 +216,7 @@ namespace Jellyfin.Server } } - host?.Dispose(); + _jfHost?.Dispose(); } } diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs new file mode 100644 index 0000000000..61fe0fdd8c --- /dev/null +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -0,0 +1,131 @@ +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Networking.Manager; +using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Net; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Hosting; +using SQLitePCL; + +namespace Jellyfin.Server.ServerSetupApp; + +/// +/// Creates a fake application pipeline that will only exist for as long as the main app is not started. +/// +public sealed class SetupServer : IDisposable +{ + private IHost? _startupServer; + private bool _disposed; + + /// + /// Starts the Bind-All Setup aspcore server to provide a reflection on the current core setup. + /// + /// The networkmanager. + /// The application paths. + /// A Task. + public async Task RunAsync(Func networkManagerFactory, IApplicationPaths applicationPaths) + { + ThrowIfDisposed(); + _startupServer = Host.CreateDefaultBuilder() + .UseConsoleLifetime() + .ConfigureServices(serv => + { + serv.AddHealthChecks() + .AddCheck("StartupCheck"); + }) + .ConfigureWebHostDefaults(webHostBuilder => + { + webHostBuilder + .UseKestrel() + .Configure(app => + { + app.UseHealthChecks("/health"); + + app.Map("/startup/logger", loggerRoute => + { + loggerRoute.Run(async context => + { + var networkManager = networkManagerFactory(); + if (context.Connection.RemoteIpAddress is null || networkManager is null || !networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress)) + { + context.Response.StatusCode = (int)HttpStatusCode.Unauthorized; + return; + } + + var logfilePath = Directory.EnumerateFiles(applicationPaths.LogDirectoryPath).Select(e => new FileInfo(e)).OrderBy(f => f.CreationTimeUtc).FirstOrDefault()?.FullName; + if (logfilePath is not null) + { + await context.Response.SendFileAsync(logfilePath, CancellationToken.None).ConfigureAwait(false); + } + }); + }); + + app.Run((context) => + { + context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; + context.Response.Headers.RetryAfter = new Microsoft.Extensions.Primitives.StringValues("60"); + context.Response.WriteAsync("

Jellyfin Server still starting. Please wait.

"); + var networkManager = networkManagerFactory(); + if (networkManager is not null && context.Connection.RemoteIpAddress is not null && networkManager.IsInLocalNetwork(context.Connection.RemoteIpAddress)) + { + context.Response.WriteAsync("

You can download the current logfiles here.

"); + } + + return Task.CompletedTask; + }); + }); + }) + .Build(); + await _startupServer.StartAsync().ConfigureAwait(false); + } + + /// + /// Stops the Setup server. + /// + /// A task. Duh. + public async Task StopAsync() + { + ThrowIfDisposed(); + if (_startupServer is null) + { + throw new InvalidOperationException("Tried to stop a non existing startup server"); + } + + await _startupServer.StopAsync().ConfigureAwait(false); + _startupServer.Dispose(); + } + + /// + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _startupServer?.Dispose(); + } + + private void ThrowIfDisposed() + { + ObjectDisposedException.ThrowIf(_disposed, this); + } + + private class SetupHealthcheck : IHealthCheck + { + public Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) + { + return Task.FromResult(HealthCheckResult.Degraded("Server is still starting up.")); + } + } +} diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index 5a13cc4173..7a22dd8526 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -921,6 +921,19 @@ public class NetworkManager : INetworkManager, IDisposable /// public bool IsInLocalNetwork(IPAddress address) + { + return NetworkManager.IsInLocalNetwork(address, TrustAllIPv6Interfaces, _lanSubnets, _excludedSubnets); + } + + /// + /// Checks a ip address to match any lansubnet given but not to be in any excluded subnet. + /// + /// The IP address to checl. + /// Whenever all IPV6 subnet address shall be permitted. + /// The list of subnets to permit. + /// The list of subnets to never permit. + /// The check if the given IP address is in any provided subnet. + public static bool IsInLocalNetwork(IPAddress address, bool trustAllIpv6, IReadOnlyList lanSubnets, IReadOnlyList excludedSubnets) { ArgumentNullException.ThrowIfNull(address); @@ -930,23 +943,23 @@ public class NetworkManager : INetworkManager, IDisposable address = address.MapToIPv4(); } - if ((TrustAllIPv6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) + if ((trustAllIpv6 && address.AddressFamily == AddressFamily.InterNetworkV6) || IPAddress.IsLoopback(address)) { return true; } // As private addresses can be redefined by Configuration.LocalNetworkAddresses - return CheckIfLanAndNotExcluded(address); + return CheckIfLanAndNotExcluded(address, lanSubnets, excludedSubnets); } - private bool CheckIfLanAndNotExcluded(IPAddress address) + private static bool CheckIfLanAndNotExcluded(IPAddress address, IReadOnlyList lanSubnets, IReadOnlyList excludedSubnets) { - foreach (var lanSubnet in _lanSubnets) + foreach (var lanSubnet in lanSubnets) { if (lanSubnet.Contains(address)) { - foreach (var excludedSubnet in _excludedSubnets) + foreach (var excludedSubnet in excludedSubnets) { if (excludedSubnet.Contains(address)) { From cd81a698a6020a5ab4aa469e2350cbcc4e09e8a4 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 26 Oct 2024 10:34:11 +0000 Subject: [PATCH 03/56] Reverted change to network manager --- .../Manager/NetworkManager.cs | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index 7a22dd8526..5a13cc4173 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -921,19 +921,6 @@ public class NetworkManager : INetworkManager, IDisposable /// public bool IsInLocalNetwork(IPAddress address) - { - return NetworkManager.IsInLocalNetwork(address, TrustAllIPv6Interfaces, _lanSubnets, _excludedSubnets); - } - - /// - /// Checks a ip address to match any lansubnet given but not to be in any excluded subnet. - /// - /// The IP address to checl. - /// Whenever all IPV6 subnet address shall be permitted. - /// The list of subnets to permit. - /// The list of subnets to never permit. - /// The check if the given IP address is in any provided subnet. - public static bool IsInLocalNetwork(IPAddress address, bool trustAllIpv6, IReadOnlyList lanSubnets, IReadOnlyList excludedSubnets) { ArgumentNullException.ThrowIfNull(address); @@ -943,23 +930,23 @@ public class NetworkManager : INetworkManager, IDisposable address = address.MapToIPv4(); } - if ((trustAllIpv6 && address.AddressFamily == AddressFamily.InterNetworkV6) + if ((TrustAllIPv6Interfaces && address.AddressFamily == AddressFamily.InterNetworkV6) || IPAddress.IsLoopback(address)) { return true; } // As private addresses can be redefined by Configuration.LocalNetworkAddresses - return CheckIfLanAndNotExcluded(address, lanSubnets, excludedSubnets); + return CheckIfLanAndNotExcluded(address); } - private static bool CheckIfLanAndNotExcluded(IPAddress address, IReadOnlyList lanSubnets, IReadOnlyList excludedSubnets) + private bool CheckIfLanAndNotExcluded(IPAddress address) { - foreach (var lanSubnet in lanSubnets) + foreach (var lanSubnet in _lanSubnets) { if (lanSubnet.Contains(address)) { - foreach (var excludedSubnet in excludedSubnets) + foreach (var excludedSubnet in _excludedSubnets) { if (excludedSubnet.Contains(address)) { From ebabaac6b1c4eca7203f4b477fa5e79bd786760c Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 26 Oct 2024 10:47:38 +0000 Subject: [PATCH 04/56] removed dbg timeout --- Jellyfin.Server/Program.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 0bbcfa6a64..a3b16d6a7d 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -168,7 +168,6 @@ namespace Jellyfin.Server try { - await Task.Delay(50000).ConfigureAwait(false); await _setupServer!.StopAsync().ConfigureAwait(false); _setupServer.Dispose(); _setupServer = null!; From dc029d549c0da8e0747d46f51a06621a16eb61df Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 26 Oct 2024 11:08:20 +0000 Subject: [PATCH 05/56] removed double dispose --- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 61fe0fdd8c..fc0680e40f 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -101,7 +101,6 @@ public sealed class SetupServer : IDisposable } await _startupServer.StopAsync().ConfigureAwait(false); - _startupServer.Dispose(); } /// From 41c27d4e7e197308f3ff978c59e538028bbf4ef4 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 26 Oct 2024 16:59:12 +0000 Subject: [PATCH 06/56] ATV requested endpoint mock --- Jellyfin.Server/Program.cs | 18 ++++---- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 43 ++++++++++++++++++- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index a3b16d6a7d..922a06802a 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -46,7 +46,7 @@ namespace Jellyfin.Server private static readonly SerilogLoggerFactory _loggerFactory = new SerilogLoggerFactory(); private static SetupServer? _setupServer = new(); - + private static CoreAppHost? _appHost; private static IHost? _jfHost = null; private static long _startTimestamp; private static ILogger _logger = NullLogger.Instance; @@ -74,7 +74,7 @@ namespace Jellyfin.Server { _startTimestamp = Stopwatch.GetTimestamp(); ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options); - await _setupServer!.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths).ConfigureAwait(false); + await _setupServer!.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); @@ -130,18 +130,19 @@ namespace Jellyfin.Server { _startTimestamp = Stopwatch.GetTimestamp(); _setupServer = new SetupServer(); - await _setupServer.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths).ConfigureAwait(false); + await _setupServer.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); } } while (_restartOnShutdown); } private static async Task StartServer(IServerApplicationPaths appPaths, StartupOptions options, IConfiguration startupConfig) { - using var appHost = new CoreAppHost( - appPaths, - _loggerFactory, - options, - startupConfig); + using CoreAppHost appHost = new CoreAppHost( + appPaths, + _loggerFactory, + options, + startupConfig); + _appHost = appHost; try { _jfHost = Host.CreateDefaultBuilder() @@ -215,6 +216,7 @@ namespace Jellyfin.Server } } + _appHost = null; _jfHost?.Dispose(); } } diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index fc0680e40f..ea4804753b 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -7,6 +7,8 @@ using System.Threading.Tasks; using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; +using MediaBrowser.Controller; +using MediaBrowser.Model.System; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Hosting; @@ -31,8 +33,12 @@ public sealed class SetupServer : IDisposable ///
/// The networkmanager. /// The application paths. + /// The servers application host. /// A Task. - public async Task RunAsync(Func networkManagerFactory, IApplicationPaths applicationPaths) + public async Task RunAsync( + Func networkManagerFactory, + IApplicationPaths applicationPaths, + Func serverApplicationHost) { ThrowIfDisposed(); _startupServer = Host.CreateDefaultBuilder() @@ -69,6 +75,41 @@ public sealed class SetupServer : IDisposable }); }); + app.Map("/System/Info/Public", systemRoute => + { + systemRoute.Run(async context => + { + var jfApplicationHost = serverApplicationHost(); + + var retryCounter = 0; + while (jfApplicationHost is null && retryCounter < 5) + { + await Task.Delay(500).ConfigureAwait(false); + jfApplicationHost = serverApplicationHost(); + retryCounter++; + } + + if (jfApplicationHost is null) + { + context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; + context.Response.Headers.RetryAfter = new Microsoft.Extensions.Primitives.StringValues("60"); + return; + } + + var sysInfo = new PublicSystemInfo + { + Version = jfApplicationHost.ApplicationVersionString, + ProductName = jfApplicationHost.Name, + Id = jfApplicationHost.SystemId, + ServerName = jfApplicationHost.FriendlyName, + LocalAddress = jfApplicationHost.GetSmartApiUrl(context.Request), + StartupWizardCompleted = false + }; + + await context.Response.WriteAsJsonAsync(sysInfo).ConfigureAwait(false); + }); + }); + app.Run((context) => { context.Response.StatusCode = (int)HttpStatusCode.ServiceUnavailable; From 6454a35ef831157fb10d8cbdf39017b2df2b8449 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Wed, 22 Jan 2025 18:20:57 +0100 Subject: [PATCH 07/56] Extract trickplay files into own subdirectory --- .../AppBase/BaseApplicationPaths.cs | 58 +++++-------------- .../Trickplay/TrickplayManager.cs | 4 +- .../Migrations/Routines/MoveTrickplayFiles.cs | 24 +++++++- .../Configuration/IApplicationPaths.cs | 6 ++ 4 files changed, 46 insertions(+), 46 deletions(-) diff --git a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs index dc845b2d7e..f0cca9efd0 100644 --- a/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs +++ b/Emby.Server.Implementations/AppBase/BaseApplicationPaths.cs @@ -34,76 +34,46 @@ namespace Emby.Server.Implementations.AppBase DataPath = Directory.CreateDirectory(Path.Combine(ProgramDataPath, "data")).FullName; } - /// - /// Gets the path to the program data folder. - /// - /// The program data path. + /// public string ProgramDataPath { get; } /// public string WebPath { get; } - /// - /// Gets the path to the system folder. - /// - /// The path to the system folder. + /// public string ProgramSystemPath { get; } = AppContext.BaseDirectory; - /// - /// Gets the folder path to the data directory. - /// - /// The data directory. + /// public string DataPath { get; } /// public string VirtualDataPath => "%AppDataPath%"; - /// - /// Gets the image cache path. - /// - /// The image cache path. + /// public string ImageCachePath => Path.Combine(CachePath, "images"); - /// - /// Gets the path to the plugin directory. - /// - /// The plugins path. + /// public string PluginsPath => Path.Combine(ProgramDataPath, "plugins"); - /// - /// Gets the path to the plugin configurations directory. - /// - /// The plugin configurations path. + /// public string PluginConfigurationsPath => Path.Combine(PluginsPath, "configurations"); - /// - /// Gets the path to the log directory. - /// - /// The log directory path. + /// public string LogDirectoryPath { get; } - /// - /// Gets the path to the application configuration root directory. - /// - /// The configuration directory path. + /// public string ConfigurationDirectoryPath { get; } - /// - /// Gets the path to the system configuration file. - /// - /// The system configuration file path. + /// public string SystemConfigurationFilePath => Path.Combine(ConfigurationDirectoryPath, "system.xml"); - /// - /// Gets or sets the folder path to the cache directory. - /// - /// The cache directory. + /// public string CachePath { get; set; } - /// - /// Gets the folder path to the temp directory within the cache folder. - /// - /// The temp directory. + /// public string TempDirectory => Path.Join(Path.GetTempPath(), "jellyfin"); + + /// + public string TrickplayPath => Path.Combine(DataPath, "trickplay"); } } diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index cd73d67c3b..e94673bcec 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -602,9 +602,11 @@ public class TrickplayManager : ITrickplayManager /// public string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false) { + var basePath = _config.ApplicationPaths.TrickplayPath; + var idString = item.Id.ToString("N", CultureInfo.InvariantCulture); var path = saveWithMedia ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay")) - : Path.Combine(item.GetInternalMetadataPath(), "trickplay"); + : Path.Combine(basePath, idString); var subdirectory = string.Format( CultureInfo.InvariantCulture, diff --git a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs index c1a9e88949..f4ebac3778 100644 --- a/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs +++ b/Jellyfin.Server/Migrations/Routines/MoveTrickplayFiles.cs @@ -39,7 +39,7 @@ public class MoveTrickplayFiles : IMigrationRoutine } /// - public Guid Id => new("4EF123D5-8EFF-4B0B-869D-3AED07A60E1B"); + public Guid Id => new("9540D44A-D8DC-11EF-9CBB-B77274F77C52"); /// public string Name => "MoveTrickplayFiles"; @@ -89,6 +89,12 @@ public class MoveTrickplayFiles : IMigrationRoutine { _fileSystem.MoveDirectory(oldPath, newPath); } + + oldPath = GetNewOldTrickplayDirectory(item, trickplayInfo.TileWidth, trickplayInfo.TileHeight, trickplayInfo.Width, false); + if (_fileSystem.DirectoryExists(oldPath)) + { + _fileSystem.MoveDirectory(oldPath, newPath); + } } } while (previousCount == Limit); @@ -101,4 +107,20 @@ public class MoveTrickplayFiles : IMigrationRoutine return width.HasValue ? Path.Combine(path, width.Value.ToString(CultureInfo.InvariantCulture)) : path; } + + private string GetNewOldTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false) + { + var path = saveWithMedia + ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay")) + : Path.Combine(item.GetInternalMetadataPath(), "trickplay"); + + var subdirectory = string.Format( + CultureInfo.InvariantCulture, + "{0} - {1}x{2}", + width.ToString(CultureInfo.InvariantCulture), + tileWidth.ToString(CultureInfo.InvariantCulture), + tileHeight.ToString(CultureInfo.InvariantCulture)); + + return Path.Combine(path, subdirectory); + } } diff --git a/MediaBrowser.Common/Configuration/IApplicationPaths.cs b/MediaBrowser.Common/Configuration/IApplicationPaths.cs index 57c6546675..7a8ab32361 100644 --- a/MediaBrowser.Common/Configuration/IApplicationPaths.cs +++ b/MediaBrowser.Common/Configuration/IApplicationPaths.cs @@ -84,5 +84,11 @@ namespace MediaBrowser.Common.Configuration /// /// The magic string used for virtual path manipulation. string VirtualDataPath { get; } + + /// + /// Gets the path used for storing trickplay files. + /// + /// The trickplay path. + string TrickplayPath { get; } } } From 533ceeaaf299c9396f82e11e16314afbc03407f8 Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 4 Feb 2025 16:52:17 +0800 Subject: [PATCH 08/56] Fix subnet contains check We are still using `Subnet.Contains` a lot but that does not handle IPv4 mapped to IPv6 addresses at all. It was partially fixed by #12094 in local network checking, but it may not always happen on LAN. Also make all local network checking to use IsInLocalNetwork method instead of just performing `Subnet.Contains` which is not accurate. Filter out all link-local addresses for external interface matching. --- MediaBrowser.Common/Net/NetworkUtils.cs | 19 +++++++++ .../Manager/NetworkManager.cs | 42 +++++++++---------- 2 files changed, 38 insertions(+), 23 deletions(-) diff --git a/MediaBrowser.Common/Net/NetworkUtils.cs b/MediaBrowser.Common/Net/NetworkUtils.cs index 7380963520..e21fdeb3d4 100644 --- a/MediaBrowser.Common/Net/NetworkUtils.cs +++ b/MediaBrowser.Common/Net/NetworkUtils.cs @@ -326,4 +326,23 @@ public static partial class NetworkUtils return new IPAddress(BitConverter.GetBytes(broadCastIPAddress)); } + + /// + /// Check if a subnet contains an address. This method also handles IPv4 mapped to IPv6 addresses. + /// + /// The . + /// The . + /// Whether the supplied IP is in the supplied network. + public static bool SubNetContainsAddress(IPNetwork network, IPAddress address) + { + ArgumentNullException.ThrowIfNull(address); + ArgumentNullException.ThrowIfNull(network); + + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + return network.Contains(address); + } } diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index dd01e9533b..15b3edbd59 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -689,10 +689,10 @@ public class NetworkManager : INetworkManager, IDisposable { // Comma separated list of IP addresses or IP/netmask entries for networks that will be allowed to connect remotely. // If left blank, all remote addresses will be allowed. - if (_remoteAddressFilter.Any() && !_lanSubnets.Any(x => x.Contains(remoteIP))) + if (_remoteAddressFilter.Any() && !IsInLocalNetwork(remoteIP)) { // remoteAddressFilter is a whitelist or blacklist. - var matches = _remoteAddressFilter.Count(remoteNetwork => remoteNetwork.Contains(remoteIP)); + var matches = _remoteAddressFilter.Count(remoteNetwork => NetworkUtils.SubNetContainsAddress(remoteNetwork, remoteIP)); if ((!config.IsRemoteIPFilterBlacklist && matches > 0) || (config.IsRemoteIPFilterBlacklist && matches == 0)) { @@ -816,7 +816,7 @@ public class NetworkManager : INetworkManager, IDisposable _logger.LogWarning("IPv4 is disabled in Jellyfin, but enabled in the OS. This may affect how the interface is selected."); } - bool isExternal = !_lanSubnets.Any(network => network.Contains(source)); + bool isExternal = !IsInLocalNetwork(source); _logger.LogDebug("Trying to get bind address for source {Source} - External: {IsExternal}", source, isExternal); if (!skipOverrides && MatchesPublishedServerUrl(source, isExternal, out result)) @@ -863,7 +863,7 @@ public class NetworkManager : INetworkManager, IDisposable // (For systems with multiple internal network cards, and multiple subnets) foreach (var intf in availableInterfaces) { - if (intf.Subnet.Contains(source)) + if (NetworkUtils.SubNetContainsAddress(intf.Subnet, source)) { result = NetworkUtils.FormatIPString(intf.Address); _logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result); @@ -891,21 +891,11 @@ public class NetworkManager : INetworkManager, IDisposable { if (NetworkUtils.TryParseToSubnet(address, out var subnet)) { - return IPAddress.IsLoopback(subnet.Prefix) || (_lanSubnets.Any(x => x.Contains(subnet.Prefix)) && !_excludedSubnets.Any(x => x.Contains(subnet.Prefix))); + return IsInLocalNetwork(subnet.Prefix); } - if (NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled)) - { - foreach (var ept in addresses) - { - if (IPAddress.IsLoopback(ept) || (_lanSubnets.Any(x => x.Contains(ept)) && !_excludedSubnets.Any(x => x.Contains(ept)))) - { - return true; - } - } - } - - return false; + return NetworkUtils.TryParseHost(address, out var addresses, IsIPv4Enabled, IsIPv6Enabled) + && addresses.Any(IsInLocalNetwork); } /// @@ -940,6 +930,11 @@ public class NetworkManager : INetworkManager, IDisposable return CheckIfLanAndNotExcluded(address); } + /// + /// Check if the address is in the LAN and not excluded. + /// + /// The IP address to check. The caller should make sure this is not an IPv4MappedToIPv6 address. + /// Boolean indicates whether the address is in LAN. private bool CheckIfLanAndNotExcluded(IPAddress address) { foreach (var lanSubnet in _lanSubnets) @@ -979,7 +974,7 @@ public class NetworkManager : INetworkManager, IDisposable { // Only use matching internal subnets // Prefer more specific (bigger subnet prefix) overrides - validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && x.Data.Subnet.Contains(source)) + validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && NetworkUtils.SubNetContainsAddress(x.Data.Subnet, source)) .OrderByDescending(x => x.Data.Subnet.PrefixLength) .ToList(); } @@ -987,7 +982,7 @@ public class NetworkManager : INetworkManager, IDisposable { // Only use matching external subnets // Prefer more specific (bigger subnet prefix) overrides - validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && x.Data.Subnet.Contains(source)) + validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && NetworkUtils.SubNetContainsAddress(x.Data.Subnet, source)) .OrderByDescending(x => x.Data.Subnet.PrefixLength) .ToList(); } @@ -995,7 +990,7 @@ public class NetworkManager : INetworkManager, IDisposable foreach (var data in validPublishedServerUrls) { // Get interface matching override subnet - var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => data.Data.Subnet.Contains(x.Address)); + var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => NetworkUtils.SubNetContainsAddress(data.Data.Subnet, x.Address)); if (intf?.Address is not null || (data.Data.AddressFamily == AddressFamily.InterNetwork && data.Data.Address.Equals(IPAddress.Any)) @@ -1058,6 +1053,7 @@ public class NetworkManager : INetworkManager, IDisposable if (isInExternalSubnet) { var externalInterfaces = _interfaces.Where(x => !IsInLocalNetwork(x.Address)) + .Where(x => !IsLinkLocalAddress(x.Address)) .OrderBy(x => x.Index) .ToList(); if (externalInterfaces.Count > 0) @@ -1065,7 +1061,7 @@ public class NetworkManager : INetworkManager, IDisposable // Check to see if any of the external bind interfaces are in the same subnet as the source. // If none exists, this will select the first external interface if there is one. bindAddress = externalInterfaces - .OrderByDescending(x => x.Subnet.Contains(source)) + .OrderByDescending(x => NetworkUtils.SubNetContainsAddress(x.Subnet, source)) .ThenByDescending(x => x.Subnet.PrefixLength) .ThenBy(x => x.Index) .Select(x => x.Address) @@ -1083,7 +1079,7 @@ public class NetworkManager : INetworkManager, IDisposable // Check to see if any of the internal bind interfaces are in the same subnet as the source. // If none exists, this will select the first internal interface if there is one. bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address)) - .OrderByDescending(x => x.Subnet.Contains(source)) + .OrderByDescending(x => NetworkUtils.SubNetContainsAddress(x.Subnet, source)) .ThenByDescending(x => x.Subnet.PrefixLength) .ThenBy(x => x.Index) .Select(x => x.Address) @@ -1127,7 +1123,7 @@ public class NetworkManager : INetworkManager, IDisposable // (For systems with multiple network cards and/or multiple subnets) foreach (var intf in extResult) { - if (intf.Subnet.Contains(source)) + if (NetworkUtils.SubNetContainsAddress(intf.Subnet, source)) { result = NetworkUtils.FormatIPString(intf.Address); _logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result); From 9aec576c763eeb6a2d1538175d397ab933227e5a Mon Sep 17 00:00:00 2001 From: gnattu Date: Wed, 5 Feb 2025 08:04:29 +0800 Subject: [PATCH 09/56] Typo Co-authored-by: Cody Robibero --- MediaBrowser.Common/Net/NetworkUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MediaBrowser.Common/Net/NetworkUtils.cs b/MediaBrowser.Common/Net/NetworkUtils.cs index e21fdeb3d4..a498d6271b 100644 --- a/MediaBrowser.Common/Net/NetworkUtils.cs +++ b/MediaBrowser.Common/Net/NetworkUtils.cs @@ -333,7 +333,7 @@ public static partial class NetworkUtils /// The . /// The . /// Whether the supplied IP is in the supplied network. - public static bool SubNetContainsAddress(IPNetwork network, IPAddress address) + public static bool SubnetContainsAddress(IPNetwork network, IPAddress address) { ArgumentNullException.ThrowIfNull(address); ArgumentNullException.ThrowIfNull(network); From 4e64b261a8a6f3b02a82d295b347755e46ad1cc2 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Wed, 5 Feb 2025 18:13:28 -0600 Subject: [PATCH 10/56] Moved Trimmed to Jellyfin.Extensions.StringExtensions --- MediaBrowser.Controller/Sorting/SortExtensions.cs | 5 ----- MediaBrowser.Providers/MediaInfo/AudioFileProber.cs | 1 + MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs | 2 +- MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs | 2 +- MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs | 1 + src/Jellyfin.Extensions/StringExtensions.cs | 12 ++++++++++++ 6 files changed, 16 insertions(+), 7 deletions(-) diff --git a/MediaBrowser.Controller/Sorting/SortExtensions.cs b/MediaBrowser.Controller/Sorting/SortExtensions.cs index db934e0f47..f9c0d39ddd 100644 --- a/MediaBrowser.Controller/Sorting/SortExtensions.cs +++ b/MediaBrowser.Controller/Sorting/SortExtensions.cs @@ -30,10 +30,5 @@ namespace MediaBrowser.Controller.Sorting { return list.ThenByDescending(getName, _comparer); } - - public static IEnumerable Trimmed(this IEnumerable values) - { - return values.Select(i => (i ?? string.Empty).Trim()); - } } } diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 1308e06f9a..0e22dd96ed 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using ATL; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index 228c51959d..266e1861f9 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Jellyfin.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Chapters; using MediaBrowser.Controller.Configuration; @@ -16,7 +17,6 @@ using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; -using MediaBrowser.Controller.Sorting; using MediaBrowser.Controller.Subtitles; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Dlna; diff --git a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs index 4cb6f81b73..774539c95e 100644 --- a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs @@ -4,11 +4,11 @@ using System.Globalization; using System.IO; using System.Linq; using System.Xml; +using Jellyfin.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs index 2ff2fc716f..d119751791 100644 --- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Xml; +using Jellyfin.Extensions; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; diff --git a/src/Jellyfin.Extensions/StringExtensions.cs b/src/Jellyfin.Extensions/StringExtensions.cs index 4b9677d9f4..715cbf2209 100644 --- a/src/Jellyfin.Extensions/StringExtensions.cs +++ b/src/Jellyfin.Extensions/StringExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Text.RegularExpressions; using ICU4N.Text; @@ -123,5 +125,15 @@ namespace Jellyfin.Extensions { return (_transliterator.Value is null) ? text : _transliterator.Value.Transliterate(text); } + + /// + /// Ensures all strings are non-null and trimmed of leading an trailing blanks. + /// + /// The enumerable of strings to trim. + /// The enumeration of trimmed strings. + public static IEnumerable Trimmed(this IEnumerable values) + { + return values.Select(i => (i ?? string.Empty).Trim()); + } } } From 5303445c9b4c9934145151f20c084033ffd1e7c6 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sat, 7 Dec 2024 19:26:58 +0100 Subject: [PATCH 11/56] Migrate to IExternalUrlProvider --- .../Providers/IExternalId.cs | 6 -- .../Providers/ExternalIdInfo.cs | 14 +-- .../Manager/ProviderManager.cs | 32 +----- .../Movies/ImdbExternalId.cs | 3 - .../Movies/ImdbExternalUrlProvider.cs | 27 +++++ .../Movies/ImdbPersonExternalId.cs | 3 - MediaBrowser.Providers/Music/ImvdbId.cs | 3 - .../Plugins/AudioDb/AudioDbAlbumExternalId.cs | 3 - .../AudioDbAlbumExternalUrlProvider.cs | 32 ++++++ .../AudioDb/AudioDbArtistExternalId.cs | 3 - .../AudioDbArtistExternalUrlProvider.cs | 33 ++++++ .../AudioDb/AudioDbOtherAlbumExternalId.cs | 3 - .../AudioDb/AudioDbOtherArtistExternalId.cs | 3 - .../MusicBrainzAlbumArtistExternalId.cs | 3 - ...sicBrainzAlbumArtistExternalUrlProvider.cs | 29 +++++ .../MusicBrainz/MusicBrainzAlbumExternalId.cs | 3 - .../MusicBrainzAlbumExternalUrlProvider.cs | 29 +++++ .../MusicBrainzArtistExternalId.cs | 3 - .../MusicBrainzArtistExternalUrlProvider.cs | 33 ++++++ .../MusicBrainzOtherArtistExternalId.cs | 3 - .../MusicBrainzReleaseGroupExternalId.cs | 3 - ...icBrainzReleaseGroupExternalUrlProvider.cs | 29 +++++ .../MusicBrainzTrackExternalUrlProvider.cs | 29 +++++ .../Plugins/MusicBrainz/MusicBrainzTrackId.cs | 3 - .../Tmdb/BoxSets/TmdbBoxSetExternalId.cs | 3 - .../Tmdb/Movies/TmdbMovieExternalId.cs | 3 - .../Tmdb/People/TmdbPersonExternalId.cs | 3 - .../Plugins/Tmdb/TV/TmdbSeriesExternalId.cs | 3 - .../Plugins/Tmdb/TmdbExternalUrlProvider.cs | 101 ++++++++++++++++++ MediaBrowser.Providers/TV/Zap2ItExternalId.cs | 3 - .../TV/Zap2ItExternalUrlProvider.cs | 25 +++++ .../Parsers/EpisodeNfoProviderTests.cs | 2 +- .../Parsers/MovieNfoParserTests.cs | 2 +- .../Parsers/MusicAlbumNfoProviderTests.cs | 2 +- .../Parsers/MusicArtistNfoParserTests.cs | 2 +- 35 files changed, 374 insertions(+), 107 deletions(-) create mode 100644 MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs create mode 100644 MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalUrlProvider.cs create mode 100644 MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalUrlProvider.cs create mode 100644 MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs create mode 100644 MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs create mode 100644 MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalUrlProvider.cs create mode 100644 MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs create mode 100644 MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs create mode 100644 MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs create mode 100644 MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs diff --git a/MediaBrowser.Controller/Providers/IExternalId.cs b/MediaBrowser.Controller/Providers/IExternalId.cs index f451eac6dd..584c3297a9 100644 --- a/MediaBrowser.Controller/Providers/IExternalId.cs +++ b/MediaBrowser.Controller/Providers/IExternalId.cs @@ -31,12 +31,6 @@ namespace MediaBrowser.Controller.Providers /// ExternalIdMediaType? Type { get; } - /// - /// Gets the URL format string for this id. - /// - [Obsolete("Obsolete in 10.10, to be removed in 10.11")] - string? UrlFormatString { get; } - /// /// Determines whether this id supports a given item type. /// diff --git a/MediaBrowser.Model/Providers/ExternalIdInfo.cs b/MediaBrowser.Model/Providers/ExternalIdInfo.cs index 1f5163aa8e..e7a3099243 100644 --- a/MediaBrowser.Model/Providers/ExternalIdInfo.cs +++ b/MediaBrowser.Model/Providers/ExternalIdInfo.cs @@ -1,5 +1,3 @@ -using System; - namespace MediaBrowser.Model.Providers { /// @@ -13,15 +11,11 @@ namespace MediaBrowser.Model.Providers /// Name of the external id provider (IE: IMDB, MusicBrainz, etc). /// Key for this id. This key should be unique across all providers. /// Specific media type for this id. - /// URL format string. - public ExternalIdInfo(string name, string key, ExternalIdMediaType? type, string? urlFormatString) + public ExternalIdInfo(string name, string key, ExternalIdMediaType? type) { Name = name; Key = key; Type = type; -#pragma warning disable CS0618 // Type or member is obsolete - Remove 10.11 - UrlFormatString = urlFormatString; -#pragma warning restore CS0618 // Type or member is obsolete } /// @@ -46,11 +40,5 @@ namespace MediaBrowser.Model.Providers /// This can be used along with the to localize the external id on the client. /// public ExternalIdMediaType? Type { get; set; } - - /// - /// Gets or sets the URL format string. - /// - [Obsolete("Obsolete in 10.10, to be removed in 10.11")] - public string? UrlFormatString { get; set; } } } diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 8c45abe252..856f33b497 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -899,35 +899,10 @@ namespace MediaBrowser.Providers.Manager /// public IEnumerable GetExternalUrls(BaseItem item) { -#pragma warning disable CS0618 // Type or member is obsolete - Remove 10.11 - var legacyExternalIdUrls = GetExternalIds(item) - .Select(i => - { - var urlFormatString = i.UrlFormatString; - if (string.IsNullOrEmpty(urlFormatString) - || !item.TryGetProviderId(i.Key, out var providerId)) - { - return null; - } - - return new ExternalUrl - { - Name = i.ProviderName, - Url = string.Format( - CultureInfo.InvariantCulture, - urlFormatString, - providerId) - }; - }) - .OfType(); -#pragma warning restore CS0618 // Type or member is obsolete - - var externalUrls = _externalUrlProviders + return _externalUrlProviders .SelectMany(p => p .GetExternalUrls(item) .Select(externalUrl => new ExternalUrl { Name = p.Name, Url = externalUrl })); - - return legacyExternalIdUrls.Concat(externalUrls).OrderBy(u => u.Name); } /// @@ -937,10 +912,7 @@ namespace MediaBrowser.Providers.Manager .Select(i => new ExternalIdInfo( name: i.ProviderName, key: i.Key, - type: i.Type, -#pragma warning disable CS0618 // Type or member is obsolete - Remove 10.11 - urlFormatString: i.UrlFormatString)); -#pragma warning restore CS0618 // Type or member is obsolete + type: i.Type)); } /// diff --git a/MediaBrowser.Providers/Movies/ImdbExternalId.cs b/MediaBrowser.Providers/Movies/ImdbExternalId.cs index a8d74aa0b5..def0b13c07 100644 --- a/MediaBrowser.Providers/Movies/ImdbExternalId.cs +++ b/MediaBrowser.Providers/Movies/ImdbExternalId.cs @@ -21,9 +21,6 @@ namespace MediaBrowser.Providers.Movies /// public ExternalIdMediaType? Type => null; - /// - public string UrlFormatString => "https://www.imdb.com/title/{0}"; - /// public bool Supports(IHasProviderIds item) { diff --git a/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs b/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs new file mode 100644 index 0000000000..eadcc976af --- /dev/null +++ b/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs @@ -0,0 +1,27 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Movies; + +/// +/// External URLs for IMDb. +/// +public class ImdbExternalUrlProvider : IExternalUrlProvider +{ + /// + public string Name => "IMDb"; + + /// + public IEnumerable GetExternalUrls(BaseItem item) + { + var baseUrl = "https://www.imdb.com/"; + var externalId = item.GetProviderId(MetadataProvider.Imdb); + + if (!string.IsNullOrEmpty(externalId)) + { + yield return baseUrl + $"title/{externalId}"; + } + } +} diff --git a/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs b/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs index 8151ab4715..aa2b2fae95 100644 --- a/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs +++ b/MediaBrowser.Providers/Movies/ImdbPersonExternalId.cs @@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Movies /// public ExternalIdMediaType? Type => ExternalIdMediaType.Person; - /// - public string UrlFormatString => "https://www.imdb.com/name/{0}"; - /// public bool Supports(IHasProviderIds item) => item is Person; } diff --git a/MediaBrowser.Providers/Music/ImvdbId.cs b/MediaBrowser.Providers/Music/ImvdbId.cs index ed69f369c0..b2c0b7019e 100644 --- a/MediaBrowser.Providers/Music/ImvdbId.cs +++ b/MediaBrowser.Providers/Music/ImvdbId.cs @@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Music /// public ExternalIdMediaType? Type => null; - /// - public string? UrlFormatString => null; - /// public bool Supports(IHasProviderIds item) => item is MusicVideo; diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs index 138cfef19a..622bb1dba4 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalId.cs @@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb /// public ExternalIdMediaType? Type => null; - /// - public string UrlFormatString => "https://www.theaudiodb.com/album/{0}"; - /// public bool Supports(IHasProviderIds item) => item is MusicAlbum; } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalUrlProvider.cs new file mode 100644 index 0000000000..1615f1ce59 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalUrlProvider.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Plugins.AudioDb; + +/// +/// External artist URLs for AudioDb. +/// +public class AudioDbAlbumExternalUrlProvider : IExternalUrlProvider +{ + /// + public string Name => "TheAudioDb Album"; + + /// + public IEnumerable GetExternalUrls(BaseItem item) + { + var externalId = item.GetProviderId(MetadataProvider.AudioDbAlbum); + if (!string.IsNullOrEmpty(externalId)) + { + var baseUrl = "https://www.theaudiodb.com/"; + switch (item) + { + case MusicAlbum: + yield return baseUrl + $"album/{externalId}"; + break; + } + } + } +} diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs index 8aceb48c0c..3b5955b5be 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalId.cs @@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb /// public ExternalIdMediaType? Type => ExternalIdMediaType.Artist; - /// - public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}"; - /// public bool Supports(IHasProviderIds item) => item is MusicArtist; } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalUrlProvider.cs new file mode 100644 index 0000000000..5c5057fa1a --- /dev/null +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalUrlProvider.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Plugins.AudioDb; + +/// +/// External artist URLs for AudioDb. +/// +public class AudioDbArtistExternalUrlProvider : IExternalUrlProvider +{ + /// + public string Name => "TheAudioDb Artist"; + + /// + public IEnumerable GetExternalUrls(BaseItem item) + { + var externalId = item.GetProviderId(MetadataProvider.AudioDbArtist); + if (!string.IsNullOrEmpty(externalId)) + { + var baseUrl = "https://www.theaudiodb.com/"; + switch (item) + { + case MusicAlbum: + case Person: + yield return baseUrl + $"artist/{externalId}"; + break; + } + } + } +} diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs index 014481da24..fdfd330cd3 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherAlbumExternalId.cs @@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb /// public ExternalIdMediaType? Type => ExternalIdMediaType.Album; - /// - public string UrlFormatString => "https://www.theaudiodb.com/album/{0}"; - /// public bool Supports(IHasProviderIds item) => item is Audio; } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs index 7875391043..5a39ec1cd9 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbOtherArtistExternalId.cs @@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.Plugins.AudioDb /// public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist; - /// - public string UrlFormatString => "https://www.theaudiodb.com/artist/{0}"; - /// public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs index 825fe32fa2..f1fc4a137b 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalId.cs @@ -19,9 +19,6 @@ public class MusicBrainzAlbumArtistExternalId : IExternalId /// public ExternalIdMediaType? Type => ExternalIdMediaType.AlbumArtist; - /// - public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}"; - /// public bool Supports(IHasProviderIds item) => item is Audio; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs new file mode 100644 index 0000000000..3de18f4ccf --- /dev/null +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Plugins.MusicBrainz; + +/// +/// External album artist URLs for MusicBrainz. +/// +public class MusicBrainzAlbumArtistExternalUrlProvider : IExternalUrlProvider +{ + /// + public string Name => "MusicBrainz Album Artist"; + + /// + public IEnumerable GetExternalUrls(BaseItem item) + { + if (item is MusicAlbum) + { + var externalId = item.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist); + if (!string.IsNullOrEmpty(externalId)) + { + yield return Plugin.Instance!.Configuration.Server + $"/artist/{externalId}"; + } + } + } +} diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs index b7d53984c5..48784e0ecd 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalId.cs @@ -19,9 +19,6 @@ public class MusicBrainzAlbumExternalId : IExternalId /// public ExternalIdMediaType? Type => ExternalIdMediaType.Album; - /// - public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/release/{0}"; - /// public bool Supports(IHasProviderIds item) => item is Audio || item is MusicAlbum; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs new file mode 100644 index 0000000000..6d0afdd508 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Plugins.MusicBrainz; + +/// +/// External album URLs for MusicBrainz. +/// +public class MusicBrainzAlbumExternalUrlProvider : IExternalUrlProvider +{ + /// + public string Name => "MusicBrainz Album"; + + /// + public IEnumerable GetExternalUrls(BaseItem item) + { + if (item is MusicAlbum) + { + var externalId = item.GetProviderId(MetadataProvider.MusicBrainzArtist); + if (!string.IsNullOrEmpty(externalId)) + { + yield return Plugin.Instance!.Configuration.Server + $"/release/{externalId}"; + } + } + } +} diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs index b3f001618d..bd5d67ed1b 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalId.cs @@ -19,9 +19,6 @@ public class MusicBrainzArtistExternalId : IExternalId /// public ExternalIdMediaType? Type => ExternalIdMediaType.Artist; - /// - public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}"; - /// public bool Supports(IHasProviderIds item) => item is MusicArtist; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalUrlProvider.cs new file mode 100644 index 0000000000..cd71191bff --- /dev/null +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalUrlProvider.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Plugins.MusicBrainz; + +/// +/// External artist URLs for MusicBrainz. +/// +public class MusicBrainzArtistExternalUrlProvider : IExternalUrlProvider +{ + /// + public string Name => "MusicBrainz Artist"; + + /// + public IEnumerable GetExternalUrls(BaseItem item) + { + var externalId = item.GetProviderId(MetadataProvider.MusicBrainzArtist); + if (!string.IsNullOrEmpty(externalId)) + { + switch (item) + { + case MusicAlbum: + case Person: + yield return Plugin.Instance!.Configuration.Server + $"/artist/{externalId}"; + + break; + } + } + } +} diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs index a0a922293d..470cdad662 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzOtherArtistExternalId.cs @@ -19,9 +19,6 @@ public class MusicBrainzOtherArtistExternalId : IExternalId /// public ExternalIdMediaType? Type => ExternalIdMediaType.OtherArtist; - /// - public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/artist/{0}"; - /// public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs index 47b6d69633..c19b62abfe 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalId.cs @@ -19,9 +19,6 @@ public class MusicBrainzReleaseGroupExternalId : IExternalId /// public ExternalIdMediaType? Type => ExternalIdMediaType.ReleaseGroup; - /// - public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/release-group/{0}"; - /// public bool Supports(IHasProviderIds item) => item is Audio or MusicAlbum; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs new file mode 100644 index 0000000000..9bc0103794 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Plugins.MusicBrainz; + +/// +/// External release group URLs for MusicBrainz. +/// +public class MusicBrainzReleaseGroupExternalUrlProvider : IExternalUrlProvider +{ + /// + public string Name => "MusicBrainz Release Group"; + + /// + public IEnumerable GetExternalUrls(BaseItem item) + { + if (item is MusicAlbum) + { + var externalId = item.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup); + if (!string.IsNullOrEmpty(externalId)) + { + yield return Plugin.Instance!.Configuration.Server + $"/release-group/{externalId}"; + } + } + } +} diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs new file mode 100644 index 0000000000..fc26dc54df --- /dev/null +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.Plugins.MusicBrainz; + +/// +/// External track URLs for MusicBrainz. +/// +public class MusicBrainzTrackExternalUrlProvider : IExternalUrlProvider +{ + /// + public string Name => "MusicBrainz Track"; + + /// + public IEnumerable GetExternalUrls(BaseItem item) + { + if (item is Audio) + { + var externalId = item.GetProviderId(MetadataProvider.MusicBrainzArtist); + if (!string.IsNullOrEmpty(externalId)) + { + yield return Plugin.Instance!.Configuration.Server + $"/track/{externalId}"; + } + } + } +} diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs index cb4345660d..6a7b6f5412 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackId.cs @@ -19,9 +19,6 @@ public class MusicBrainzTrackId : IExternalId /// public ExternalIdMediaType? Type => ExternalIdMediaType.Track; - /// - public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/track/{0}"; - /// public bool Supports(IHasProviderIds item) => item is Audio; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs index d453a4ff44..2076589d34 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/BoxSets/TmdbBoxSetExternalId.cs @@ -20,9 +20,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.BoxSets /// public ExternalIdMediaType? Type => ExternalIdMediaType.BoxSet; - /// - public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "collection/{0}"; - /// public bool Supports(IHasProviderIds item) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs index 6d6032e8f6..9a1d872ec2 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieExternalId.cs @@ -20,9 +20,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies /// public ExternalIdMediaType? Type => ExternalIdMediaType.Movie; - /// - public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "movie/{0}"; - /// public bool Supports(IHasProviderIds item) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs index d26a70028c..2c0787b15d 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/People/TmdbPersonExternalId.cs @@ -19,9 +19,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.People /// public ExternalIdMediaType? Type => ExternalIdMediaType.Person; - /// - public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "person/{0}"; - /// public bool Supports(IHasProviderIds item) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs index 5f2d7909a9..840cec9841 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesExternalId.cs @@ -19,9 +19,6 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV /// public ExternalIdMediaType? Type => ExternalIdMediaType.Series; - /// - public string UrlFormatString => TmdbUtils.BaseTmdbUrl + "tv/{0}"; - /// public bool Supports(IHasProviderIds item) { diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs new file mode 100644 index 0000000000..b8fd18f286 --- /dev/null +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; +using MediaBrowser.Controller.Entities.TV; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; +using TMDbLib.Objects.TvShows; + +namespace MediaBrowser.Providers.Plugins.Tmdb; + +/// +/// External URLs for TMDb. +/// +public class TmdbExternalUrlProvider : IExternalUrlProvider +{ + /// + public string Name => "TMDB"; + + /// + public IEnumerable GetExternalUrls(BaseItem item) + { + switch (item) + { + case Series: + var externalId = item.GetProviderId(MetadataProvider.Tmdb); + if (!string.IsNullOrEmpty(externalId)) + { + yield return TmdbUtils.BaseTmdbUrl + $"tv/{externalId}"; + } + + break; + case Season season: + var seriesExternalId = season.Series.GetProviderId(MetadataProvider.Tmdb); + if (!string.IsNullOrEmpty(seriesExternalId)) + { + var orderString = season.Series.DisplayOrder; + if (string.IsNullOrEmpty(orderString)) + { + // Default order is airdate + yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{season.IndexNumber}"; + } + + if (Enum.TryParse(season.Series.DisplayOrder, out var order)) + { + if (order.Equals(TvGroupType.OriginalAirDate)) + { + yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{season.IndexNumber}"; + } + } + } + + break; + case Episode episode: + seriesExternalId = episode.Series.GetProviderId(MetadataProvider.Tmdb); + if (!string.IsNullOrEmpty(seriesExternalId)) + { + var orderString = episode.Series.DisplayOrder; + if (string.IsNullOrEmpty(orderString)) + { + // Default order is airdate + yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{episode.Season.IndexNumber}/episode/{episode.IndexNumber}"; + } + + if (Enum.TryParse(orderString, out var order)) + { + if (order.Equals(TvGroupType.OriginalAirDate)) + { + yield return TmdbUtils.BaseTmdbUrl + $"tv/{seriesExternalId}/season/{episode.Season.IndexNumber}/episode/{episode.IndexNumber}"; + } + } + } + + break; + case Movie: + externalId = item.GetProviderId(MetadataProvider.Tmdb); + if (!string.IsNullOrEmpty(externalId)) + { + yield return TmdbUtils.BaseTmdbUrl + $"movie/{externalId}"; + } + + break; + case Person: + externalId = item.GetProviderId(MetadataProvider.Tmdb); + if (!string.IsNullOrEmpty(externalId)) + { + yield return TmdbUtils.BaseTmdbUrl + $"person/{externalId}"; + } + + break; + case BoxSet: + externalId = item.GetProviderId(MetadataProvider.Tmdb); + if (!string.IsNullOrEmpty(externalId)) + { + yield return TmdbUtils.BaseTmdbUrl + $"collection/{externalId}"; + } + + break; + } + } +} diff --git a/MediaBrowser.Providers/TV/Zap2ItExternalId.cs b/MediaBrowser.Providers/TV/Zap2ItExternalId.cs index 3cb18e4248..8907d7744a 100644 --- a/MediaBrowser.Providers/TV/Zap2ItExternalId.cs +++ b/MediaBrowser.Providers/TV/Zap2ItExternalId.cs @@ -18,9 +18,6 @@ namespace MediaBrowser.Providers.TV /// public ExternalIdMediaType? Type => null; - /// - public string UrlFormatString => "http://tvlistings.zap2it.com/overview.html?programSeriesId={0}"; - /// public bool Supports(IHasProviderIds item) => item is Series; } diff --git a/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs b/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs new file mode 100644 index 0000000000..f6516fddeb --- /dev/null +++ b/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model.Entities; + +namespace MediaBrowser.Providers.TV; + +/// +/// External URLs for TMDb. +/// +public class Zap2ItExternalUrlProvider : IExternalUrlProvider +{ + /// + public string Name => "Zap2It"; + + /// + public IEnumerable GetExternalUrls(BaseItem item) + { + var externalId = item.GetProviderId(MetadataProvider.Zap2It); + if (!string.IsNullOrEmpty(externalId)) + { + yield return $"http://tvlistings.zap2it.com/overview.html?programSeriesId={externalId}"; + } + } +} diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs index f9126ce9bb..a04b37f215 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs @@ -26,7 +26,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers var providerManager = new Mock(); var imdbExternalId = new ImdbExternalId(); - var externalIdInfo = new ExternalIdInfo(imdbExternalId.ProviderName, imdbExternalId.Key, imdbExternalId.Type, imdbExternalId.UrlFormatString); + var externalIdInfo = new ExternalIdInfo(imdbExternalId.ProviderName, imdbExternalId.Key, imdbExternalId.Type); providerManager.Setup(x => x.GetExternalIdInfos(It.IsAny())) .Returns(new[] { externalIdInfo }); diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs index 9c2655154d..a71a08d8cd 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs @@ -34,7 +34,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers var providerManager = new Mock(); var tmdbExternalId = new TmdbMovieExternalId(); - var externalIdInfo = new ExternalIdInfo(tmdbExternalId.ProviderName, tmdbExternalId.Key, tmdbExternalId.Type, tmdbExternalId.UrlFormatString); + var externalIdInfo = new ExternalIdInfo(tmdbExternalId.ProviderName, tmdbExternalId.Key, tmdbExternalId.Type); providerManager.Setup(x => x.GetExternalIdInfos(It.IsAny())) .Returns(new[] { externalIdInfo }); diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs index f815dfaa9a..24e9b9feeb 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicAlbumNfoProviderTests.cs @@ -24,7 +24,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers var providerManager = new Mock(); var musicBrainzArtist = new MusicBrainzArtistExternalId(); - var externalIdInfo = new ExternalIdInfo(musicBrainzArtist.ProviderName, musicBrainzArtist.Key, musicBrainzArtist.Type, "MusicBrainzServer"); + var externalIdInfo = new ExternalIdInfo(musicBrainzArtist.ProviderName, musicBrainzArtist.Key, musicBrainzArtist.Type); providerManager.Setup(x => x.GetExternalIdInfos(It.IsAny())) .Returns(new[] { externalIdInfo }); diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs index 78183d9ffd..4d1956bde7 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MusicArtistNfoParserTests.cs @@ -24,7 +24,7 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers var providerManager = new Mock(); var musicBrainzArtist = new MusicBrainzArtistExternalId(); - var externalIdInfo = new ExternalIdInfo(musicBrainzArtist.ProviderName, musicBrainzArtist.Key, musicBrainzArtist.Type, "MusicBrainzServer"); + var externalIdInfo = new ExternalIdInfo(musicBrainzArtist.ProviderName, musicBrainzArtist.Key, musicBrainzArtist.Type); providerManager.Setup(x => x.GetExternalIdInfos(It.IsAny())) .Returns(new[] { externalIdInfo }); From 5ff2767012e2970cedb697a48eabd4949c348f2c Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Fri, 21 Feb 2025 11:58:46 +0100 Subject: [PATCH 12/56] Use TryGetProviderId where possible --- .../Entities/Audio/MusicArtist.cs | 6 +-- .../Movies/ImdbExternalUrlProvider.cs | 4 +- .../AudioDbAlbumExternalUrlProvider.cs | 3 +- .../AudioDb/AudioDbAlbumImageProvider.cs | 7 +-- .../AudioDbArtistExternalUrlProvider.cs | 3 +- .../AudioDb/AudioDbArtistImageProvider.cs | 13 +++--- ...sicBrainzAlbumArtistExternalUrlProvider.cs | 3 +- .../MusicBrainzAlbumExternalUrlProvider.cs | 3 +- .../MusicBrainzArtistExternalUrlProvider.cs | 3 +- ...icBrainzReleaseGroupExternalUrlProvider.cs | 3 +- .../MusicBrainzTrackExternalUrlProvider.cs | 3 +- .../Plugins/Tmdb/TmdbExternalUrlProvider.cs | 18 +++----- .../TV/Zap2ItExternalUrlProvider.cs | 3 +- .../Savers/BaseNfoSaver.cs | 44 +++++-------------- .../Savers/MovieNfoSaver.cs | 4 +- .../Savers/SeriesNfoSaver.cs | 4 +- .../Recordings/RecordingsMetadataManager.cs | 13 ++---- 17 files changed, 42 insertions(+), 95 deletions(-) diff --git a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs index ecb3ac3a68..52221ad9e3 100644 --- a/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs +++ b/MediaBrowser.Controller/Entities/Audio/MusicArtist.cs @@ -138,11 +138,9 @@ namespace MediaBrowser.Controller.Entities.Audio private static List GetUserDataKeys(MusicArtist item) { var list = new List(); - var id = item.GetProviderId(MetadataProvider.MusicBrainzArtist); - - if (!string.IsNullOrEmpty(id)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out var externalId)) { - list.Add("Artist-Musicbrainz-" + id); + list.Add("Artist-Musicbrainz-" + externalId); } list.Add("Artist-" + (item.Name ?? string.Empty).RemoveDiacritics()); diff --git a/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs b/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs index eadcc976af..ff8ad1d612 100644 --- a/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs @@ -17,9 +17,7 @@ public class ImdbExternalUrlProvider : IExternalUrlProvider public IEnumerable GetExternalUrls(BaseItem item) { var baseUrl = "https://www.imdb.com/"; - var externalId = item.GetProviderId(MetadataProvider.Imdb); - - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.Imdb, out var externalId)) { yield return baseUrl + $"title/{externalId}"; } diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalUrlProvider.cs index 1615f1ce59..01d2841059 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumExternalUrlProvider.cs @@ -17,8 +17,7 @@ public class AudioDbAlbumExternalUrlProvider : IExternalUrlProvider /// public IEnumerable GetExternalUrls(BaseItem item) { - var externalId = item.GetProviderId(MetadataProvider.AudioDbAlbum); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.AudioDbAlbum, out var externalId)) { var baseUrl = "https://www.theaudiodb.com/"; switch (item) diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs index 8a516e1ce7..d2eeb7f079 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumImageProvider.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net.Http; using System.Text.Json; using System.Threading; @@ -50,9 +49,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb /// public async Task> GetImages(BaseItem item, CancellationToken cancellationToken) { - var id = item.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup); - - if (!string.IsNullOrWhiteSpace(id)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out var id)) { await AudioDbAlbumProvider.Current.EnsureInfo(id, cancellationToken).ConfigureAwait(false); @@ -70,7 +67,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb } } - return Enumerable.Empty(); + return []; } private List GetImages(AudioDbAlbumProvider.Album item) diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalUrlProvider.cs index 5c5057fa1a..56b0d9bcb2 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistExternalUrlProvider.cs @@ -17,8 +17,7 @@ public class AudioDbArtistExternalUrlProvider : IExternalUrlProvider /// public IEnumerable GetExternalUrls(BaseItem item) { - var externalId = item.GetProviderId(MetadataProvider.AudioDbArtist); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.AudioDbArtist, out var externalId)) { var baseUrl = "https://www.theaudiodb.com/"; switch (item) diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs index 4e7757cd26..88730f34d2 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistImageProvider.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using System.IO; -using System.Linq; using System.Net.Http; using System.Text.Json; using System.Threading; @@ -43,21 +42,19 @@ namespace MediaBrowser.Providers.Plugins.AudioDb /// public IEnumerable GetSupportedImages(BaseItem item) { - return new ImageType[] - { + return + [ ImageType.Primary, ImageType.Logo, ImageType.Banner, ImageType.Backdrop - }; + ]; } /// public async Task> GetImages(BaseItem item, CancellationToken cancellationToken) { - var id = item.GetProviderId(MetadataProvider.MusicBrainzArtist); - - if (!string.IsNullOrWhiteSpace(id)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out var id)) { await AudioDbArtistProvider.Current.EnsureArtistInfo(id, cancellationToken).ConfigureAwait(false); @@ -75,7 +72,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb } } - return Enumerable.Empty(); + return []; } private List GetImages(AudioDbArtistProvider.Artist item) diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs index 3de18f4ccf..29dbbc58c1 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs @@ -19,8 +19,7 @@ public class MusicBrainzAlbumArtistExternalUrlProvider : IExternalUrlProvider { if (item is MusicAlbum) { - var externalId = item.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out var externalId)) { yield return Plugin.Instance!.Configuration.Server + $"/artist/{externalId}"; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs index 6d0afdd508..f838dcf4c8 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs @@ -19,8 +19,7 @@ public class MusicBrainzAlbumExternalUrlProvider : IExternalUrlProvider { if (item is MusicAlbum) { - var externalId = item.GetProviderId(MetadataProvider.MusicBrainzArtist); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out var externalId)) { yield return Plugin.Instance!.Configuration.Server + $"/release/{externalId}"; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalUrlProvider.cs index cd71191bff..ee5a597c62 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzArtistExternalUrlProvider.cs @@ -17,8 +17,7 @@ public class MusicBrainzArtistExternalUrlProvider : IExternalUrlProvider /// public IEnumerable GetExternalUrls(BaseItem item) { - var externalId = item.GetProviderId(MetadataProvider.MusicBrainzArtist); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out var externalId)) { switch (item) { diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs index 9bc0103794..dd0a939f72 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzReleaseGroupExternalUrlProvider.cs @@ -19,8 +19,7 @@ public class MusicBrainzReleaseGroupExternalUrlProvider : IExternalUrlProvider { if (item is MusicAlbum) { - var externalId = item.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out var externalId)) { yield return Plugin.Instance!.Configuration.Server + $"/release-group/{externalId}"; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs index fc26dc54df..59e6f42b19 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzTrackExternalUrlProvider.cs @@ -19,8 +19,7 @@ public class MusicBrainzTrackExternalUrlProvider : IExternalUrlProvider { if (item is Audio) { - var externalId = item.GetProviderId(MetadataProvider.MusicBrainzArtist); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out var externalId)) { yield return Plugin.Instance!.Configuration.Server + $"/track/{externalId}"; } diff --git a/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs index b8fd18f286..bec800c035 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/TmdbExternalUrlProvider.cs @@ -23,16 +23,14 @@ public class TmdbExternalUrlProvider : IExternalUrlProvider switch (item) { case Series: - var externalId = item.GetProviderId(MetadataProvider.Tmdb); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.Tmdb, out var externalId)) { yield return TmdbUtils.BaseTmdbUrl + $"tv/{externalId}"; } break; case Season season: - var seriesExternalId = season.Series.GetProviderId(MetadataProvider.Tmdb); - if (!string.IsNullOrEmpty(seriesExternalId)) + if (season.Series.TryGetProviderId(MetadataProvider.Tmdb, out var seriesExternalId)) { var orderString = season.Series.DisplayOrder; if (string.IsNullOrEmpty(orderString)) @@ -52,8 +50,7 @@ public class TmdbExternalUrlProvider : IExternalUrlProvider break; case Episode episode: - seriesExternalId = episode.Series.GetProviderId(MetadataProvider.Tmdb); - if (!string.IsNullOrEmpty(seriesExternalId)) + if (episode.Series.TryGetProviderId(MetadataProvider.Imdb, out seriesExternalId)) { var orderString = episode.Series.DisplayOrder; if (string.IsNullOrEmpty(orderString)) @@ -73,24 +70,21 @@ public class TmdbExternalUrlProvider : IExternalUrlProvider break; case Movie: - externalId = item.GetProviderId(MetadataProvider.Tmdb); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.Tmdb, out externalId)) { yield return TmdbUtils.BaseTmdbUrl + $"movie/{externalId}"; } break; case Person: - externalId = item.GetProviderId(MetadataProvider.Tmdb); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.Tmdb, out externalId)) { yield return TmdbUtils.BaseTmdbUrl + $"person/{externalId}"; } break; case BoxSet: - externalId = item.GetProviderId(MetadataProvider.Tmdb); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.Tmdb, out externalId)) { yield return TmdbUtils.BaseTmdbUrl + $"collection/{externalId}"; } diff --git a/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs b/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs index f6516fddeb..52b0583e58 100644 --- a/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs +++ b/MediaBrowser.Providers/TV/Zap2ItExternalUrlProvider.cs @@ -16,8 +16,7 @@ public class Zap2ItExternalUrlProvider : IExternalUrlProvider /// public IEnumerable GetExternalUrls(BaseItem item) { - var externalId = item.GetProviderId(MetadataProvider.Zap2It); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.Zap2It, out var externalId)) { yield return $"http://tvlistings.zap2it.com/overview.html?programSeriesId={externalId}"; } diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index 51c5a20803..0786e7afde 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -544,16 +544,13 @@ namespace MediaBrowser.XbmcMetadata.Savers writer.WriteElementString("aspectratio", hasAspectRatio.AspectRatio); } - var tmdbCollection = item.GetProviderId(MetadataProvider.TmdbCollection); - - if (!string.IsNullOrEmpty(tmdbCollection)) + if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdbCollection)) { writer.WriteElementString("collectionnumber", tmdbCollection); writtenProviderIds.Add(MetadataProvider.TmdbCollection.ToString()); } - var imdb = item.GetProviderId(MetadataProvider.Imdb); - if (!string.IsNullOrEmpty(imdb)) + if (item.TryGetProviderId(MetadataProvider.Imdb, out var imdb)) { if (item is Series) { @@ -570,16 +567,14 @@ namespace MediaBrowser.XbmcMetadata.Savers // Series xml saver already saves this if (item is not Series) { - var tvdb = item.GetProviderId(MetadataProvider.Tvdb); - if (!string.IsNullOrEmpty(tvdb)) + if (item.TryGetProviderId(MetadataProvider.Tvdb, out var tvdb)) { writer.WriteElementString("tvdbid", tvdb); writtenProviderIds.Add(MetadataProvider.Tvdb.ToString()); } } - var tmdb = item.GetProviderId(MetadataProvider.Tmdb); - if (!string.IsNullOrEmpty(tmdb)) + if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdb)) { writer.WriteElementString("tmdbid", tmdb); writtenProviderIds.Add(MetadataProvider.Tmdb.ToString()); @@ -687,64 +682,49 @@ namespace MediaBrowser.XbmcMetadata.Savers } } - var externalId = item.GetProviderId(MetadataProvider.AudioDbArtist); - - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.AudioDbArtist, out var externalId)) { writer.WriteElementString("audiodbartistid", externalId); writtenProviderIds.Add(MetadataProvider.AudioDbArtist.ToString()); } - externalId = item.GetProviderId(MetadataProvider.AudioDbAlbum); - - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.AudioDbAlbum, out externalId)) { writer.WriteElementString("audiodbalbumid", externalId); writtenProviderIds.Add(MetadataProvider.AudioDbAlbum.ToString()); } - externalId = item.GetProviderId(MetadataProvider.Zap2It); - - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.Zap2It, out externalId)) { writer.WriteElementString("zap2itid", externalId); writtenProviderIds.Add(MetadataProvider.Zap2It.ToString()); } - externalId = item.GetProviderId(MetadataProvider.MusicBrainzAlbum); - - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out externalId)) { writer.WriteElementString("musicbrainzalbumid", externalId); writtenProviderIds.Add(MetadataProvider.MusicBrainzAlbum.ToString()); } - externalId = item.GetProviderId(MetadataProvider.MusicBrainzAlbumArtist); - - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out externalId)) { writer.WriteElementString("musicbrainzalbumartistid", externalId); writtenProviderIds.Add(MetadataProvider.MusicBrainzAlbumArtist.ToString()); } - externalId = item.GetProviderId(MetadataProvider.MusicBrainzArtist); - - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out externalId)) { writer.WriteElementString("musicbrainzartistid", externalId); writtenProviderIds.Add(MetadataProvider.MusicBrainzArtist.ToString()); } - externalId = item.GetProviderId(MetadataProvider.MusicBrainzReleaseGroup); - - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out externalId)) { writer.WriteElementString("musicbrainzreleasegroupid", externalId); writtenProviderIds.Add(MetadataProvider.MusicBrainzReleaseGroup.ToString()); } - externalId = item.GetProviderId(MetadataProvider.TvRage); - if (!string.IsNullOrEmpty(externalId)) + if (item.TryGetProviderId(MetadataProvider.TvRage, out externalId)) { writer.WriteElementString("tvrageid", externalId); writtenProviderIds.Add(MetadataProvider.TvRage.ToString()); diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs index e85e369d91..a5909762d6 100644 --- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs @@ -91,9 +91,7 @@ namespace MediaBrowser.XbmcMetadata.Savers /// protected override void WriteCustomElements(BaseItem item, XmlWriter writer) { - var imdb = item.GetProviderId(MetadataProvider.Imdb); - - if (!string.IsNullOrEmpty(imdb)) + if (item.TryGetProviderId(MetadataProvider.Imdb, out var imdb)) { writer.WriteElementString("id", imdb); } diff --git a/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs index 083f22e5d2..1ac6768a16 100644 --- a/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/SeriesNfoSaver.cs @@ -54,9 +54,7 @@ namespace MediaBrowser.XbmcMetadata.Savers { var series = (Series)item; - var tvdb = item.GetProviderId(MetadataProvider.Tvdb); - - if (!string.IsNullOrEmpty(tvdb)) + if (item.TryGetProviderId(MetadataProvider.Tvdb, out var tvdb)) { writer.WriteElementString("id", tvdb); diff --git a/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs b/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs index b2b82332df..3a2c463695 100644 --- a/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs +++ b/src/Jellyfin.LiveTv/Recordings/RecordingsMetadataManager.cs @@ -344,15 +344,12 @@ public class RecordingsMetadataManager await writer.WriteElementStringAsync(null, "credits", null, person).ConfigureAwait(false); } - var tmdbCollection = item.GetProviderId(MetadataProvider.TmdbCollection); - - if (!string.IsNullOrEmpty(tmdbCollection)) + if (item.TryGetProviderId(MetadataProvider.TmdbCollection, out var tmdbCollection)) { await writer.WriteElementStringAsync(null, "collectionnumber", null, tmdbCollection).ConfigureAwait(false); } - var imdb = item.GetProviderId(MetadataProvider.Imdb); - if (!string.IsNullOrEmpty(imdb)) + if (item.TryGetProviderId(MetadataProvider.Imdb, out var imdb)) { if (!isSeriesEpisode) { @@ -365,8 +362,7 @@ public class RecordingsMetadataManager lockData = false; } - var tvdb = item.GetProviderId(MetadataProvider.Tvdb); - if (!string.IsNullOrEmpty(tvdb)) + if (item.TryGetProviderId(MetadataProvider.Tvdb, out var tvdb)) { await writer.WriteElementStringAsync(null, "tvdbid", null, tvdb).ConfigureAwait(false); @@ -374,8 +370,7 @@ public class RecordingsMetadataManager lockData = false; } - var tmdb = item.GetProviderId(MetadataProvider.Tmdb); - if (!string.IsNullOrEmpty(tmdb)) + if (item.TryGetProviderId(MetadataProvider.Tmdb, out var tmdb)) { await writer.WriteElementStringAsync(null, "tmdbid", null, tmdb).ConfigureAwait(false); From a05b3be1b3234102e4225aed57f24a598fd8edf1 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Fri, 21 Feb 2025 11:00:01 +0000 Subject: [PATCH 13/56] Fixed nullability on startupService --- Jellyfin.Server/Program.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 922a06802a..8523639e77 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -45,7 +45,7 @@ namespace Jellyfin.Server public const string LoggingConfigFileSystem = "logging.json"; private static readonly SerilogLoggerFactory _loggerFactory = new SerilogLoggerFactory(); - private static SetupServer? _setupServer = new(); + private static SetupServer _setupServer = new(); private static CoreAppHost? _appHost; private static IHost? _jfHost = null; private static long _startTimestamp; @@ -74,7 +74,7 @@ namespace Jellyfin.Server { _startTimestamp = Stopwatch.GetTimestamp(); ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options); - await _setupServer!.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); + await _setupServer.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); @@ -169,7 +169,7 @@ namespace Jellyfin.Server try { - await _setupServer!.StopAsync().ConfigureAwait(false); + await _setupServer.StopAsync().ConfigureAwait(false); _setupServer.Dispose(); _setupServer = null!; await _jfHost.StartAsync().ConfigureAwait(false); From 7735aafef5908f2d15f6dc2b3da3bb3795fd83d2 Mon Sep 17 00:00:00 2001 From: JPVenson Date: Fri, 21 Feb 2025 11:05:47 +0000 Subject: [PATCH 14/56] renaming of jfHost usings cleanup --- Jellyfin.Server/Program.cs | 17 ++++++++--------- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 3 --- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/Jellyfin.Server/Program.cs b/Jellyfin.Server/Program.cs index 8523639e77..3d92caac46 100644 --- a/Jellyfin.Server/Program.cs +++ b/Jellyfin.Server/Program.cs @@ -7,7 +7,6 @@ using System.Reflection; using System.Threading.Tasks; using CommandLine; using Emby.Server.Implementations; -using Jellyfin.Networking.Manager; using Jellyfin.Server.Extensions; using Jellyfin.Server.Helpers; using Jellyfin.Server.Implementations; @@ -47,7 +46,7 @@ namespace Jellyfin.Server private static readonly SerilogLoggerFactory _loggerFactory = new SerilogLoggerFactory(); private static SetupServer _setupServer = new(); private static CoreAppHost? _appHost; - private static IHost? _jfHost = null; + private static IHost? _jellyfinHost = null; private static long _startTimestamp; private static ILogger _logger = NullLogger.Instance; private static bool _restartOnShutdown; @@ -74,7 +73,7 @@ namespace Jellyfin.Server { _startTimestamp = Stopwatch.GetTimestamp(); ServerApplicationPaths appPaths = StartupHelpers.CreateApplicationPaths(options); - await _setupServer.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); + await _setupServer.RunAsync(static () => _jellyfinHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); // $JELLYFIN_LOG_DIR needs to be set for the logger configuration manager Environment.SetEnvironmentVariable("JELLYFIN_LOG_DIR", appPaths.LogDirectoryPath); @@ -130,7 +129,7 @@ namespace Jellyfin.Server { _startTimestamp = Stopwatch.GetTimestamp(); _setupServer = new SetupServer(); - await _setupServer.RunAsync(static () => _jfHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); + await _setupServer.RunAsync(static () => _jellyfinHost?.Services?.GetService(), appPaths, static () => _appHost).ConfigureAwait(false); } } while (_restartOnShutdown); } @@ -145,7 +144,7 @@ namespace Jellyfin.Server _appHost = appHost; try { - _jfHost = Host.CreateDefaultBuilder() + _jellyfinHost = Host.CreateDefaultBuilder() .UseConsoleLifetime() .ConfigureServices(services => appHost.Init(services)) .ConfigureWebHostDefaults(webHostBuilder => @@ -162,7 +161,7 @@ namespace Jellyfin.Server .Build(); // Re-use the host service provider in the app host since ASP.NET doesn't allow a custom service collection. - appHost.ServiceProvider = _jfHost.Services; + appHost.ServiceProvider = _jellyfinHost.Services; await appHost.InitializeServices().ConfigureAwait(false); Migrations.MigrationRunner.Run(appHost, _loggerFactory); @@ -172,7 +171,7 @@ namespace Jellyfin.Server await _setupServer.StopAsync().ConfigureAwait(false); _setupServer.Dispose(); _setupServer = null!; - await _jfHost.StartAsync().ConfigureAwait(false); + await _jellyfinHost.StartAsync().ConfigureAwait(false); if (!OperatingSystem.IsWindows() && startupConfig.UseUnixSocket()) { @@ -191,7 +190,7 @@ namespace Jellyfin.Server _logger.LogInformation("Startup complete {Time:g}", Stopwatch.GetElapsedTime(_startTimestamp)); - await _jfHost.WaitForShutdownAsync().ConfigureAwait(false); + await _jellyfinHost.WaitForShutdownAsync().ConfigureAwait(false); _restartOnShutdown = appHost.ShouldRestart; } catch (Exception ex) @@ -217,7 +216,7 @@ namespace Jellyfin.Server } _appHost = null; - _jfHost?.Dispose(); + _jellyfinHost?.Dispose(); } } diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index ea4804753b..09b7434eff 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -4,19 +4,16 @@ using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; -using Jellyfin.Networking.Manager; using MediaBrowser.Common.Configuration; using MediaBrowser.Common.Net; using MediaBrowser.Controller; using MediaBrowser.Model.System; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Hosting; -using SQLitePCL; namespace Jellyfin.Server.ServerSetupApp; From 963f2357a966dd7a5a6ab248155cc52ce066753b Mon Sep 17 00:00:00 2001 From: JPVenson Date: Fri, 21 Feb 2025 11:06:28 +0000 Subject: [PATCH 15/56] simplified logfile path --- Jellyfin.Server/ServerSetupApp/SetupServer.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Jellyfin.Server/ServerSetupApp/SetupServer.cs b/Jellyfin.Server/ServerSetupApp/SetupServer.cs index 09b7434eff..9e2cf5bc8b 100644 --- a/Jellyfin.Server/ServerSetupApp/SetupServer.cs +++ b/Jellyfin.Server/ServerSetupApp/SetupServer.cs @@ -64,10 +64,14 @@ public sealed class SetupServer : IDisposable return; } - var logfilePath = Directory.EnumerateFiles(applicationPaths.LogDirectoryPath).Select(e => new FileInfo(e)).OrderBy(f => f.CreationTimeUtc).FirstOrDefault()?.FullName; - if (logfilePath is not null) + var logFilePath = new DirectoryInfo(applicationPaths.LogDirectoryPath) + .EnumerateFiles() + .OrderBy(f => f.CreationTimeUtc) + .FirstOrDefault() + ?.FullName; + if (logFilePath is not null) { - await context.Response.SendFileAsync(logfilePath, CancellationToken.None).ConfigureAwait(false); + await context.Response.SendFileAsync(logFilePath, CancellationToken.None).ConfigureAwait(false); } }); }); From 260f1323d8bf73d4fc671991ed743d90cfe4aade Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sat, 22 Feb 2025 18:59:37 +0100 Subject: [PATCH 16/56] Apply suggestions from code review Co-authored-by: Cody Robibero --- .../MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs | 2 +- .../Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs index 29dbbc58c1..f4b3f4f8c2 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumArtistExternalUrlProvider.cs @@ -19,7 +19,7 @@ public class MusicBrainzAlbumArtistExternalUrlProvider : IExternalUrlProvider { if (item is MusicAlbum) { - if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out var externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out var externalId)) { yield return Plugin.Instance!.Configuration.Server + $"/artist/{externalId}"; } diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs index f838dcf4c8..b9d3b48353 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzAlbumExternalUrlProvider.cs @@ -19,7 +19,7 @@ public class MusicBrainzAlbumExternalUrlProvider : IExternalUrlProvider { if (item is MusicAlbum) { - if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out var externalId)) + if (item.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out var externalId)) { yield return Plugin.Instance!.Configuration.Server + $"/release/{externalId}"; } From 7f8eb179a615449c3ef87b1952af1899c904024a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 23 Feb 2025 12:38:16 +0000 Subject: [PATCH 17/56] Update dependency z440.atl.core to 6.18.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 854c5a6df8..d23d704d5b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -79,7 +79,7 @@ - + From 114591c1aacbdf4d07e95c536ea2e42af1c5ab0d Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Tue, 25 Feb 2025 01:51:38 -0600 Subject: [PATCH 18/56] Clean up usings and honor SortName --- MediaBrowser.Controller/Entities/BaseItem.cs | 1 - .../Probing/ProbeResultNormalizer.cs | 1 - .../MediaInfo/AudioFileProber.cs | 1 - .../Plugins/Tmdb/Movies/TmdbMovieProvider.cs | 1 - .../Savers/AlbumNfoSaver.cs | 1 + .../Savers/ArtistNfoSaver.cs | 3 +-- .../Savers/BaseNfoSaver.cs | 20 ++++++++++++++++++- .../Savers/MovieNfoSaver.cs | 1 - 8 files changed, 21 insertions(+), 8 deletions(-) diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 29b0b6861b..95d0f311e4 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -24,7 +24,6 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; -using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; diff --git a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs index c6a2ca5a4e..6b0fd9a147 100644 --- a/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs +++ b/MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs @@ -12,7 +12,6 @@ using Jellyfin.Data.Enums; using Jellyfin.Extensions; using MediaBrowser.Controller.Extensions; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Globalization; diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 0e22dd96ed..b504da48f7 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -14,7 +14,6 @@ using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; -using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index ce10d4a8a2..9bb6507fe6 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -12,7 +12,6 @@ using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; -using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Providers; using TMDbLib.Objects.Find; diff --git a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs index 774539c95e..440296f095 100644 --- a/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs @@ -74,6 +74,7 @@ namespace MediaBrowser.XbmcMetadata.Savers foreach (var track in tracks .OrderBy(i => i.ParentIndexNumber ?? 0) .ThenBy(i => i.IndexNumber ?? 0) + .ThenBy(i => SortNameOrName(i)) .ThenBy(i => i.Name?.Trim())) { writer.WriteStartElement("track"); diff --git a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs index e1d006bfae..b5ba2d24f2 100644 --- a/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs @@ -7,7 +7,6 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.IO; using MediaBrowser.XbmcMetadata.Configuration; using Microsoft.Extensions.Logging; @@ -73,7 +72,7 @@ namespace MediaBrowser.XbmcMetadata.Savers { foreach (var album in albums .OrderBy(album => album.ProductionYear ?? 0) - .ThenBy(album => album.SortName?.Trim()) + .ThenBy(album => SortNameOrName(album)) .ThenBy(album => album.Name?.Trim())) { writer.WriteStartElement("album"); diff --git a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs index 9c006e2064..f14bd437a8 100644 --- a/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs @@ -19,7 +19,6 @@ using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; -using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Configuration; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; @@ -1038,5 +1037,24 @@ namespace MediaBrowser.XbmcMetadata.Savers private string GetTagForProviderKey(string providerKey) => providerKey.ToLowerInvariant() + "id"; + + protected static string SortNameOrName(BaseItem item) + { + if (item == null) + { + return string.Empty; + } + + if (item.SortName != null) + { + string trimmed = item.SortName.Trim(); + if (trimmed.Length > 0) + { + return trimmed; + } + } + + return (item.Name ?? string.Empty).Trim(); + } } } diff --git a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs index d119751791..a32491c458 100644 --- a/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs +++ b/MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs @@ -9,7 +9,6 @@ using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; -using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Entities; using MediaBrowser.Model.IO; using Microsoft.Extensions.Logging; From 82b3135dd9eaa2cc0ef29d82d8cb7196d4724394 Mon Sep 17 00:00:00 2001 From: Zero King Date: Sun, 2 Mar 2025 01:03:55 +0800 Subject: [PATCH 19/56] Fix possible NullReferenceException in playlist warning --- Emby.Server.Implementations/Playlists/PlaylistManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Playlists/PlaylistManager.cs b/Emby.Server.Implementations/Playlists/PlaylistManager.cs index daeb7fed88..9e780a49e5 100644 --- a/Emby.Server.Implementations/Playlists/PlaylistManager.cs +++ b/Emby.Server.Implementations/Playlists/PlaylistManager.cs @@ -310,7 +310,7 @@ namespace Emby.Server.Implementations.Playlists var item = playlist.LinkedChildren.FirstOrDefault(i => string.Equals(entryId, i.ItemId?.ToString("N", CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase)); if (item is null) { - _logger.LogWarning("Modified item not found in playlist. ItemId: {ItemId}, PlaylistId: {PlaylistId}", item.ItemId, playlistId); + _logger.LogWarning("Modified item not found in playlist. ItemId: {ItemId}, PlaylistId: {PlaylistId}", entryId, playlistId); return; } From aad7506e854e5118bf70a5f103e2128f765ea7f2 Mon Sep 17 00:00:00 2001 From: Lampan-git Date: Sun, 2 Mar 2025 11:23:01 -0500 Subject: [PATCH 20/56] Backport pull request #13618 from jellyfin/release-10.10.z Include Role and SortOrder in MergePeople to fix "Search for missing metadata" Original-merge: fcdef875a2b0e49bc0ebeec12797c91ddb8f9bdc Merged-by: Bond-009 Backported-by: Bond_009 --- MediaBrowser.Providers/Manager/MetadataService.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 778fbc7125..1d3ddc4e24 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -1162,6 +1162,16 @@ namespace MediaBrowser.Providers.Manager { person.ImageUrl = personInSource.ImageUrl; } + + if (!string.IsNullOrWhiteSpace(personInSource.Role) && string.IsNullOrWhiteSpace(person.Role)) + { + person.Role = personInSource.Role; + } + + if (personInSource.SortOrder.HasValue && !person.SortOrder.HasValue) + { + person.SortOrder = personInSource.SortOrder; + } } } } From efb901c36976c006f2b9ad8420a06181819c9016 Mon Sep 17 00:00:00 2001 From: IDisposable Date: Sun, 2 Mar 2025 11:23:02 -0500 Subject: [PATCH 21/56] Backport pull request #13639 from jellyfin/release-10.10.z Support more rating formats Original-merge: 4f94d23011c4af755e6e05cc42f47befc7e43fcb Merged-by: Bond-009 Backported-by: Bond_009 --- .../Localization/LocalizationManager.cs | 6 ++++-- .../Localization/LocalizationManagerTests.cs | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Emby.Server.Implementations/Localization/LocalizationManager.cs b/Emby.Server.Implementations/Localization/LocalizationManager.cs index c939a5e099..754a01329b 100644 --- a/Emby.Server.Implementations/Localization/LocalizationManager.cs +++ b/Emby.Server.Implementations/Localization/LocalizationManager.cs @@ -286,8 +286,10 @@ namespace Emby.Server.Implementations.Localization } // Fairly common for some users to have "Rated R" in their rating field - rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase); - rating = rating.Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase); + rating = rating.Replace("Rated :", string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace("Rated:", string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace("Rated ", string.Empty, StringComparison.OrdinalIgnoreCase) + .Trim(); // Use rating system matching the language if (!string.IsNullOrEmpty(countryCode)) diff --git a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs index 65f018ee3f..cc67dbc397 100644 --- a/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs +++ b/tests/Jellyfin.Server.Implementations.Tests/Localization/LocalizationManagerTests.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using System.Runtime.InteropServices; using System.Threading.Tasks; using Emby.Server.Implementations.Localization; using MediaBrowser.Controller.Configuration; @@ -116,6 +115,10 @@ namespace Jellyfin.Server.Implementations.Tests.Localization [InlineData("TV-MA", "US", 17)] [InlineData("XXX", "asdf", 1000)] [InlineData("Germany: FSK-18", "DE", 18)] + [InlineData("Rated : R", "US", 17)] + [InlineData("Rated: R", "US", 17)] + [InlineData("Rated R", "US", 17)] + [InlineData(" PG-13 ", "US", 13)] public async Task GetRatingLevel_GivenValidString_Success(string value, string countryCode, int expectedLevel) { var localizationManager = Setup(new ServerConfiguration() From 728819780a81cc8146d18d0605c8ad6ce00c522f Mon Sep 17 00:00:00 2001 From: "Troj@" Date: Tue, 4 Mar 2025 12:59:08 +0000 Subject: [PATCH 22/56] Translated using Weblate (Belarusian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/be/ --- Emby.Server.Implementations/Localization/Core/be.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json index 97aa0ca58c..9e62c8a74c 100644 --- a/Emby.Server.Implementations/Localization/Core/be.json +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -16,7 +16,7 @@ "Collections": "Калекцыі", "Default": "Па змаўчанні", "FailedLoginAttemptWithUserName": "Няўдалая спроба ўваходу з {0}", - "Folders": "Папкі", + "Folders": "Тэчкі", "Favorites": "Абранае", "External": "Знешні", "Genres": "Жанры", From ab369f27f7793c7fa7f482b71a7cdca5b8c54ee7 Mon Sep 17 00:00:00 2001 From: "Troj@" Date: Tue, 4 Mar 2025 18:49:44 +0000 Subject: [PATCH 23/56] Translated using Weblate (Belarusian) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/be/ --- Emby.Server.Implementations/Localization/Core/be.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/be.json b/Emby.Server.Implementations/Localization/Core/be.json index 9e62c8a74c..d5da04fb9f 100644 --- a/Emby.Server.Implementations/Localization/Core/be.json +++ b/Emby.Server.Implementations/Localization/Core/be.json @@ -1,6 +1,6 @@ { "Sync": "Сінхранізаваць", - "Playlists": "Плэйлісты", + "Playlists": "Спісы прайгравання", "Latest": "Апошні", "LabelIpAddressValue": "IP-адрас: {0}", "ItemAddedWithName": "{0} быў дададзены ў бібліятэку", From 70b8fa73f081ba406a16ccb17bdc7c8c7a39f2b1 Mon Sep 17 00:00:00 2001 From: Roman Dordzheev Date: Sat, 8 Mar 2025 13:55:21 +0300 Subject: [PATCH 24/56] Include SortName in LibraryDb migration query --- Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index 3289484f93..e3f5b18e7d 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -89,7 +89,7 @@ public class MigrateLibraryDb : IMigrationRoutine Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId, DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, SeasonName, SeasonId, SeriesId, PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate, - ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType FROM TypedBaseItems + ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName FROM TypedBaseItems """; dbContext.BaseItems.ExecuteDelete(); @@ -1034,6 +1034,11 @@ public class MigrateLibraryDb : IMigrationRoutine entity.MediaType = mediaType; } + if (reader.TryGetString(index++, out var sortName)) + { + entity.SortName = sortName; + } + var baseItem = BaseItemRepository.DeserialiseBaseItem(entity, _logger, null, false); var dataKeys = baseItem.GetUserDataKeys(); userDataKeys.AddRange(dataKeys); From cb650c69b813c2d0fef1a6fca6d42b81af3f2e12 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 9 Mar 2025 12:45:55 +0000 Subject: [PATCH 25/56] Update dependency z440.atl.core to 6.19.0 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index d23d704d5b..7e04a7326e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -79,7 +79,7 @@ - + From f5adbc029636f1555ee9b425f2ac40d72a433c82 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 9 Mar 2025 17:00:13 +0000 Subject: [PATCH 26/56] Update CI dependencies --- .github/workflows/ci-codeql-analysis.yml | 6 +++--- .github/workflows/ci-openapi.yml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 1f166d10c8..850214fec1 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '9.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 + uses: github/codeql-action/init@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 + uses: github/codeql-action/autobuild@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@b56ba49b26e50535fa1e7f7db0f4f7b4bf65d80d # v3.28.10 + uses: github/codeql-action/analyze@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 85a7a33bcd..5a32849877 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -172,7 +172,7 @@ jobs: strip_components: 1 target: "/srv/incoming/openapi/unstable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}" - name: Move openapi.json (unstable) into place - uses: appleboy/ssh-action@8faa84277b88b6cd1455986f459aa66cf72bc8a3 # v1.2.1 + uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2 with: host: "${{ secrets.REPO_HOST }}" username: "${{ secrets.REPO_USER }}" @@ -234,7 +234,7 @@ jobs: strip_components: 1 target: "/srv/incoming/openapi/stable/jellyfin-openapi-${{ env.JELLYFIN_VERSION }}" - name: Move openapi.json (stable) into place - uses: appleboy/ssh-action@8faa84277b88b6cd1455986f459aa66cf72bc8a3 # v1.2.1 + uses: appleboy/ssh-action@2ead5e36573f08b82fbfce1504f1a4b05a647c6f # v1.2.2 with: host: "${{ secrets.REPO_HOST }}" username: "${{ secrets.REPO_USER }}" From 8ef7b4f9b5792f5a669f3f15e90697a9a90121b2 Mon Sep 17 00:00:00 2001 From: Thunderstrike116 Date: Sun, 9 Mar 2025 20:06:30 +0000 Subject: [PATCH 27/56] Translated using Weblate (Greek) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/el/ --- Emby.Server.Implementations/Localization/Core/el.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index 55f266032e..619c61d7de 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -96,7 +96,7 @@ "TaskCleanLogsDescription": "Διαγράφει αρχεία καταγραφής που είναι πάνω από {0} ημέρες.", "TaskCleanLogs": "Εκκαθάριση Καταλόγου Καταγραφής", "TaskRefreshLibraryDescription": "Σαρώνει την βιβλιοθήκη πολυμέσων σας για νέα αρχεία και ανανεώνει τα μεταδεδομένα.", - "TaskRefreshLibrary": "Βιβλιοθήκη Σάρωσης Πολυμέσων", + "TaskRefreshLibrary": "Σάρωση Βιβλιοθήκης Πολυμέσων", "TaskRefreshChapterImagesDescription": "Δημιουργεί μικρογραφίες για βίντεο που έχουν κεφάλαια.", "TaskRefreshChapterImages": "Εξαγωγή Εικόνων Κεφαλαίου", "TaskCleanCacheDescription": "Διαγράφει αρχεία προσωρινής μνήμης που δεν χρειάζονται πλέον το σύστημα.", From 0d7eb489309eb0bca00854cc0dddd5e19c332067 Mon Sep 17 00:00:00 2001 From: Thunderstrike116 Date: Sun, 9 Mar 2025 20:18:30 +0000 Subject: [PATCH 28/56] Translated using Weblate (Greek) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/el/ --- Emby.Server.Implementations/Localization/Core/el.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index 619c61d7de..53e094bfc7 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -28,7 +28,7 @@ "HomeVideos": "Προσωπικά Βίντεο", "Inherit": "Κληρονόμηση", "ItemAddedWithName": "{0} προστέθηκε στη βιβλιοθήκη", - "ItemRemovedWithName": "{0} διαγράφηκε από τη βιβλιοθήκη", + "ItemRemovedWithName": "Το {0} διαγράφτηκε από τη βιβλιοθήκη", "LabelIpAddressValue": "Διεύθυνση IP: {0}", "LabelRunningTimeValue": "Διάρκεια: {0}", "Latest": "Πρόσφατα", From de5b6470beef2497d268de15c9c91f57f6203d0f Mon Sep 17 00:00:00 2001 From: congerh Date: Mon, 10 Mar 2025 06:56:51 -0400 Subject: [PATCH 29/56] Backport pull request #13659 from jellyfin/release-10.10.z Upgrade LrcParser to 2025.228.1 Original-merge: ae6a7acf1465572fb00fb49629ca1e78fab2f8f9 Merged-by: Bond-009 Backported-by: Bond_009 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7e04a7326e..7ae2ec3ab9 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -22,7 +22,7 @@ - + From f1dd065eca88f6ee5a1d162587fd207da64aa679 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Mon, 10 Mar 2025 11:50:28 -0400 Subject: [PATCH 30/56] Include CleanName in LibraryDb migration query --- Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index e3f5b18e7d..dfe497c490 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -89,7 +89,7 @@ public class MigrateLibraryDb : IMigrationRoutine Audio, ExternalServiceId, IsInMixedFolder, DateLastSaved, LockedFields, Studios, Tags, TrailerTypes, OriginalTitle, PrimaryVersionId, DateLastMediaAdded, Album, LUFS, NormalizationGain, CriticRating, IsVirtualItem, SeriesName, UserDataKey, SeasonName, SeasonId, SeriesId, PresentationUniqueKey, InheritedParentalRatingValue, ExternalSeriesId, Tagline, ProviderIds, Images, ProductionLocations, ExtraIds, TotalBitrate, - ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName FROM TypedBaseItems + ExtraType, Artists, AlbumArtists, ExternalId, SeriesPresentationUniqueKey, ShowId, OwnerId, MediaType, SortName, CleanName FROM TypedBaseItems """; dbContext.BaseItems.ExecuteDelete(); @@ -1039,6 +1039,11 @@ public class MigrateLibraryDb : IMigrationRoutine entity.SortName = sortName; } + if (reader.TryGetString(index++, out var cleanName)) + { + entity.CleanName = cleanName; + } + var baseItem = BaseItemRepository.DeserialiseBaseItem(entity, _logger, null, false); var dataKeys = baseItem.GetUserDataKeys(); userDataKeys.AddRange(dataKeys); From 490e087b46b50a4702a43368c6a4b2d3183ea5ec Mon Sep 17 00:00:00 2001 From: Thunderstrike116 Date: Tue, 11 Mar 2025 17:11:33 +0000 Subject: [PATCH 31/56] Translated using Weblate (Greek) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/el/ --- Emby.Server.Implementations/Localization/Core/el.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index 53e094bfc7..24502c8199 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -11,7 +11,7 @@ "Collections": "Συλλογές", "DeviceOfflineWithName": "Ο/Η {0} αποσυνδέθηκε", "DeviceOnlineWithName": "Ο/Η {0} συνδέθηκε", - "FailedLoginAttemptWithUserName": "Αποτυχημένη προσπάθεια σύνδεσης από {0}", + "FailedLoginAttemptWithUserName": "Αποτυχία προσπάθειας σύνδεσης από {0}", "Favorites": "Αγαπημένα", "Folders": "Φάκελοι", "Genres": "Είδη", From 237e7bd44b3c9a6f76892be1c6a925bcde64bdbf Mon Sep 17 00:00:00 2001 From: gnattu Date: Wed, 12 Mar 2025 08:40:33 -0400 Subject: [PATCH 32/56] Backport pull request #13694 from jellyfin/release-10.10.z Clone fallback audio tags instead of use ATL.Track.set Original-merge: 9eb2044eae50c69be4cb3830887bdd5da15ee920 Merged-by: Bond-009 Backported-by: Bond_009 --- .../MediaInfo/AudioFileProber.cs | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 963b611515..8a259ac541 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -175,11 +175,15 @@ namespace MediaBrowser.Providers.MediaInfo _logger.LogWarning("File {File} only has ID3v1 tags, some fields may be truncated", audio.Path); } - track.Title = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title; - track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album; - track.Year = track.Year is null or 0 ? mediaInfo.ProductionYear : track.Year; - track.TrackNumber = track.TrackNumber is null or 0 ? mediaInfo.IndexNumber : track.TrackNumber; - track.DiscNumber = track.DiscNumber is null or 0 ? mediaInfo.ParentIndexNumber : track.DiscNumber; + // We should never use the property setter of the ATL.Track class. + // That setter is meant for its own tag parser and external editor usage and will have unwanted side effects + // For example, setting the Year property will also set the Date property, which is not what we want here. + // To properly handle fallback values, we make a clone of those fields when valid. + var trackTitle = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title; + var trackAlbum = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album; + var trackYear = track.Year is null or 0 ? mediaInfo.ProductionYear : track.Year; + var trackTrackNumber = track.TrackNumber is null or 0 ? mediaInfo.IndexNumber : track.TrackNumber; + var trackDiscNumber = track.DiscNumber is null or 0 ? mediaInfo.ParentIndexNumber : track.DiscNumber; if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast)) { @@ -276,22 +280,22 @@ namespace MediaBrowser.Providers.MediaInfo } } - if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(track.Title)) + if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(trackTitle)) { - audio.Name = track.Title; + audio.Name = trackTitle; } if (options.ReplaceAllMetadata) { - audio.Album = track.Album; - audio.IndexNumber = track.TrackNumber; - audio.ParentIndexNumber = track.DiscNumber; + audio.Album = trackAlbum; + audio.IndexNumber = trackTrackNumber; + audio.ParentIndexNumber = trackDiscNumber; } else { - audio.Album ??= track.Album; - audio.IndexNumber ??= track.TrackNumber; - audio.ParentIndexNumber ??= track.DiscNumber; + audio.Album ??= trackAlbum; + audio.IndexNumber ??= trackTrackNumber; + audio.ParentIndexNumber ??= trackDiscNumber; } if (track.Date.HasValue) @@ -299,11 +303,12 @@ namespace MediaBrowser.Providers.MediaInfo audio.PremiereDate = track.Date; } - if (track.Year.HasValue) + if (trackYear.HasValue) { - var year = track.Year.Value; + var year = trackYear.Value; audio.ProductionYear = year; + // ATL library handles such fallback this with its own internal logic, but we also need to handle it here for the ffprobe fallbacks. if (!audio.PremiereDate.HasValue) { try @@ -312,7 +317,7 @@ namespace MediaBrowser.Providers.MediaInfo } catch (ArgumentOutOfRangeException ex) { - _logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, track.Year); + _logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, trackYear); } } } From cbca153132572a7816d49962c2f6cfda0ab8ae9b Mon Sep 17 00:00:00 2001 From: gnattu Date: Thu, 13 Mar 2025 06:27:12 +0800 Subject: [PATCH 33/56] More typos --- .../Manager/NetworkManager.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Jellyfin.Networking/Manager/NetworkManager.cs b/src/Jellyfin.Networking/Manager/NetworkManager.cs index 15b3edbd59..e14daadc75 100644 --- a/src/Jellyfin.Networking/Manager/NetworkManager.cs +++ b/src/Jellyfin.Networking/Manager/NetworkManager.cs @@ -692,7 +692,7 @@ public class NetworkManager : INetworkManager, IDisposable if (_remoteAddressFilter.Any() && !IsInLocalNetwork(remoteIP)) { // remoteAddressFilter is a whitelist or blacklist. - var matches = _remoteAddressFilter.Count(remoteNetwork => NetworkUtils.SubNetContainsAddress(remoteNetwork, remoteIP)); + var matches = _remoteAddressFilter.Count(remoteNetwork => NetworkUtils.SubnetContainsAddress(remoteNetwork, remoteIP)); if ((!config.IsRemoteIPFilterBlacklist && matches > 0) || (config.IsRemoteIPFilterBlacklist && matches == 0)) { @@ -863,7 +863,7 @@ public class NetworkManager : INetworkManager, IDisposable // (For systems with multiple internal network cards, and multiple subnets) foreach (var intf in availableInterfaces) { - if (NetworkUtils.SubNetContainsAddress(intf.Subnet, source)) + if (NetworkUtils.SubnetContainsAddress(intf.Subnet, source)) { result = NetworkUtils.FormatIPString(intf.Address); _logger.LogDebug("{Source}: Found interface with matching subnet, using it as bind address: {Result}", source, result); @@ -974,7 +974,7 @@ public class NetworkManager : INetworkManager, IDisposable { // Only use matching internal subnets // Prefer more specific (bigger subnet prefix) overrides - validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && NetworkUtils.SubNetContainsAddress(x.Data.Subnet, source)) + validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsInternalOverride && NetworkUtils.SubnetContainsAddress(x.Data.Subnet, source)) .OrderByDescending(x => x.Data.Subnet.PrefixLength) .ToList(); } @@ -982,7 +982,7 @@ public class NetworkManager : INetworkManager, IDisposable { // Only use matching external subnets // Prefer more specific (bigger subnet prefix) overrides - validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && NetworkUtils.SubNetContainsAddress(x.Data.Subnet, source)) + validPublishedServerUrls = _publishedServerUrls.Where(x => x.IsExternalOverride && NetworkUtils.SubnetContainsAddress(x.Data.Subnet, source)) .OrderByDescending(x => x.Data.Subnet.PrefixLength) .ToList(); } @@ -990,7 +990,7 @@ public class NetworkManager : INetworkManager, IDisposable foreach (var data in validPublishedServerUrls) { // Get interface matching override subnet - var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => NetworkUtils.SubNetContainsAddress(data.Data.Subnet, x.Address)); + var intf = _interfaces.OrderBy(x => x.Index).FirstOrDefault(x => NetworkUtils.SubnetContainsAddress(data.Data.Subnet, x.Address)); if (intf?.Address is not null || (data.Data.AddressFamily == AddressFamily.InterNetwork && data.Data.Address.Equals(IPAddress.Any)) @@ -1061,7 +1061,7 @@ public class NetworkManager : INetworkManager, IDisposable // Check to see if any of the external bind interfaces are in the same subnet as the source. // If none exists, this will select the first external interface if there is one. bindAddress = externalInterfaces - .OrderByDescending(x => NetworkUtils.SubNetContainsAddress(x.Subnet, source)) + .OrderByDescending(x => NetworkUtils.SubnetContainsAddress(x.Subnet, source)) .ThenByDescending(x => x.Subnet.PrefixLength) .ThenBy(x => x.Index) .Select(x => x.Address) @@ -1079,7 +1079,7 @@ public class NetworkManager : INetworkManager, IDisposable // Check to see if any of the internal bind interfaces are in the same subnet as the source. // If none exists, this will select the first internal interface if there is one. bindAddress = _interfaces.Where(x => IsInLocalNetwork(x.Address)) - .OrderByDescending(x => NetworkUtils.SubNetContainsAddress(x.Subnet, source)) + .OrderByDescending(x => NetworkUtils.SubnetContainsAddress(x.Subnet, source)) .ThenByDescending(x => x.Subnet.PrefixLength) .ThenBy(x => x.Index) .Select(x => x.Address) @@ -1123,7 +1123,7 @@ public class NetworkManager : INetworkManager, IDisposable // (For systems with multiple network cards and/or multiple subnets) foreach (var intf in extResult) { - if (NetworkUtils.SubNetContainsAddress(intf.Subnet, source)) + if (NetworkUtils.SubnetContainsAddress(intf.Subnet, source)) { result = NetworkUtils.FormatIPString(intf.Address); _logger.LogDebug("{Source}: Found external interface with matching subnet, using it as bind address: {Result}", source, result); From 8cb5ea60d68bb49350a310857d043b67e10ab8b7 Mon Sep 17 00:00:00 2001 From: Thunderstrike116 Date: Wed, 12 Mar 2025 16:55:01 +0000 Subject: [PATCH 34/56] Translated using Weblate (Greek) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/el/ --- .../Localization/Core/el.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index 24502c8199..631e659d5c 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -27,7 +27,7 @@ "HeaderRecordingGroups": "Ομάδες Ηχογράφησης", "HomeVideos": "Προσωπικά Βίντεο", "Inherit": "Κληρονόμηση", - "ItemAddedWithName": "{0} προστέθηκε στη βιβλιοθήκη", + "ItemAddedWithName": "Το {0} προστέθηκε στη βιβλιοθήκη", "ItemRemovedWithName": "Το {0} διαγράφτηκε από τη βιβλιοθήκη", "LabelIpAddressValue": "Διεύθυνση IP: {0}", "LabelRunningTimeValue": "Διάρκεια: {0}", @@ -40,7 +40,7 @@ "Movies": "Ταινίες", "Music": "Μουσική", "MusicVideos": "Μουσικά Βίντεο", - "NameInstallFailed": "{0} η εγκατάσταση απέτυχε", + "NameInstallFailed": "H εγκατάσταση του {0} απέτυχε", "NameSeasonNumber": "Κύκλος {0}", "NameSeasonUnknown": "Άγνωστος Κύκλος", "NewVersionIsAvailable": "Μια νέα έκδοση του διακομιστή Jellyfin είναι διαθέσιμη για λήψη.", @@ -54,7 +54,7 @@ "NotificationOptionPluginError": "Αποτυχία του πρόσθετου", "NotificationOptionPluginInstalled": "Το πρόσθετο εγκαταστάθηκε", "NotificationOptionPluginUninstalled": "Το πρόσθετο απεγκαταστάθηκε", - "NotificationOptionPluginUpdateInstalled": "Η αναβάθμιση του πρόσθετου εγκαταστάθηκε", + "NotificationOptionPluginUpdateInstalled": "Η ενημέρωση του πρόσθετου εγκαταστάθηκε", "NotificationOptionServerRestartRequired": "Ο διακομιστής χρειάζεται επανεκκίνηση", "NotificationOptionTaskFailed": "Αποτυχία προγραμματισμένης εργασίας", "NotificationOptionUserLockedOut": "Ο χρήστης αποκλείστηκε", @@ -63,9 +63,9 @@ "Photos": "Φωτογραφίες", "Playlists": "Λίστες αναπαραγωγής", "Plugin": "Πρόσθετο", - "PluginInstalledWithName": "{0} εγκαταστήθηκε", - "PluginUninstalledWithName": "{0} έχει απεγκατασταθεί", - "PluginUpdatedWithName": "{0} έχει αναβαθμιστεί", + "PluginInstalledWithName": "Το {0} εγκαταστάθηκε", + "PluginUninstalledWithName": "Το {0} έχει απεγκατασταθεί", + "PluginUpdatedWithName": "Το {0} ενημερώθηκε", "ProviderValue": "Πάροχος: {0}", "ScheduledTaskFailedWithName": "{0} αποτυχία", "ScheduledTaskStartedWithName": "{0} ξεκίνησε", From b346d12e1c1c15e39a131f1ad4efb530c5d38c32 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 13 Mar 2025 19:36:21 -0600 Subject: [PATCH 35/56] Update Microsoft to 9.0.3 (#13702) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Directory.Packages.props | 50 ++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7ae2ec3ab9..89311142cd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -24,29 +24,29 @@ - - + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + @@ -75,9 +75,9 @@ - - - + + + From 14e3b2214ac7ea64ff6412b8363c9e4d3091f51f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 13 Mar 2025 19:36:31 -0600 Subject: [PATCH 36/56] Update dependency dotnet-ef to 9.0.3 (#13703) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index ea2675a3d0..bc2098a53b 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "dotnet-ef": { - "version": "9.0.2", + "version": "9.0.3", "commands": [ "dotnet-ef" ] From 7d6bf5cb0d1ee2f22e9ac96985f93dc9c7f72e81 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 13 Mar 2025 19:36:41 -0600 Subject: [PATCH 37/56] Update dependency python to 3.13 (#13701) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/commands.yml | 2 +- .github/workflows/issue-template-check.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/commands.yml b/.github/workflows/commands.yml index 1ab7ae029d..082084ed4f 100644 --- a/.github/workflows/commands.yml +++ b/.github/workflows/commands.yml @@ -46,7 +46,7 @@ jobs: - name: install python uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: - python-version: '3.12' + python-version: '3.13' cache: 'pip' - name: install python packages run: pip install -r rename/requirements.txt diff --git a/.github/workflows/issue-template-check.yml b/.github/workflows/issue-template-check.yml index 3c5ba68f91..e3e8019568 100644 --- a/.github/workflows/issue-template-check.yml +++ b/.github/workflows/issue-template-check.yml @@ -16,7 +16,7 @@ jobs: - name: install python uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0 with: - python-version: '3.12' + python-version: '3.13' cache: 'pip' - name: install python packages run: pip install -r main-repo-triage/requirements.txt From 0eed5ee79b12b00426bd18d9830271c4a7f841cc Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Fri, 14 Mar 2025 15:17:18 +0100 Subject: [PATCH 38/56] Fix build and tests (#13718) --- .../MusicBrainz/MusicBrainzRecordingId.cs | 3 -- .../MediaInfo/MediaInfoResolverTests.cs | 52 ++++++++----------- 2 files changed, 21 insertions(+), 34 deletions(-) diff --git a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzRecordingId.cs b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzRecordingId.cs index d2af628067..89d8b9b998 100644 --- a/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzRecordingId.cs +++ b/MediaBrowser.Providers/Plugins/MusicBrainz/MusicBrainzRecordingId.cs @@ -19,9 +19,6 @@ public class MusicBrainzRecordingId : IExternalId /// public ExternalIdMediaType? Type => ExternalIdMediaType.Recording; - /// - public string UrlFormatString => Plugin.Instance!.Configuration.Server + "/recording/{0}"; - /// public bool Supports(IHasProviderIds item) => item is Audio; } diff --git a/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs b/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs index db427308c2..222e624aa2 100644 --- a/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs +++ b/tests/Jellyfin.Providers.Tests/MediaInfo/MediaInfoResolverTests.cs @@ -217,68 +217,58 @@ public class MediaInfoResolverTests string file = "My.Video.srt"; data.Add( file, - new[] - { + [ CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0) - }, - new[] - { + ], + [ CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0) - }); + ]); // filename has metadata file = "My.Video.Title1.default.forced.sdh.en.srt"; data.Add( file, - new[] - { + [ CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0) - }, - new[] - { + ], + [ CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title1", 0, true, true, true) - }); + ]); // single stream with metadata file = "My.Video.mks"; data.Add( file, - new[] - { + [ CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, true, true) - }, - new[] - { - CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, true, true) - }); + ], + [ + CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title", 0, true, false, true) + ]); // stream wins for title/language, filename wins for flags when conflicting file = "My.Video.Title2.default.forced.sdh.en.srt"; data.Add( file, - new[] - { + [ CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 0) - }, - new[] - { + ], + [ CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 0, true, true, true) - }); + ]); // multiple stream with metadata - filename flags ignored but other data filled in when missing from stream file = "My.Video.Title3.default.forced.en.srt"; data.Add( file, - new[] - { + [ CreateMediaStream(VideoDirectoryPath + "/" + file, null, null, 0, true, true), CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 1) - }, - new[] - { + ], + [ CreateMediaStream(VideoDirectoryPath + "/" + file, "eng", "Title3", 0, true, true), CreateMediaStream(VideoDirectoryPath + "/" + file, "fra", "Metadata", 1) - }); + ]); return data; } From e684f26c9732352fea948971706287ad36126ec4 Mon Sep 17 00:00:00 2001 From: Bond-009 Date: Sat, 15 Mar 2025 15:35:08 +0100 Subject: [PATCH 39/56] Add start index to /Programs/Recommended endpoint (#13696) --- Jellyfin.Api/Controllers/LiveTvController.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Jellyfin.Api/Controllers/LiveTvController.cs b/Jellyfin.Api/Controllers/LiveTvController.cs index a3b4c87004..1c0a6af79d 100644 --- a/Jellyfin.Api/Controllers/LiveTvController.cs +++ b/Jellyfin.Api/Controllers/LiveTvController.cs @@ -698,6 +698,7 @@ public class LiveTvController : BaseJellyfinApiController /// Gets recommended live tv epgs. /// /// Optional. filter by user id. + /// Optional. The record index to start at. All items with a lower index will be dropped from the results. /// Optional. The maximum number of records to return. /// Optional. Filter by programs that are currently airing, or not. /// Optional. Filter by programs that have completed airing, or not. @@ -720,6 +721,7 @@ public class LiveTvController : BaseJellyfinApiController [ProducesResponseType(StatusCodes.Status200OK)] public async Task>> GetRecommendedPrograms( [FromQuery] Guid? userId, + [FromQuery] int? startIndex, [FromQuery] int? limit, [FromQuery] bool? isAiring, [FromQuery] bool? hasAired, @@ -744,6 +746,7 @@ public class LiveTvController : BaseJellyfinApiController var query = new InternalItemsQuery(user) { IsAiring = isAiring, + StartIndex = startIndex, Limit = limit, HasAired = hasAired, IsSeries = isSeries, From 6104d8d5f9f8958878a1e108b59c66c06d3f162b Mon Sep 17 00:00:00 2001 From: Joesph boukolos Date: Sun, 16 Mar 2025 04:29:47 +0000 Subject: [PATCH 40/56] Translated using Weblate (Esperanto) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/eo/ --- Emby.Server.Implementations/Localization/Core/eo.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/eo.json b/Emby.Server.Implementations/Localization/Core/eo.json index 0b595c2caf..42cce1096f 100644 --- a/Emby.Server.Implementations/Localization/Core/eo.json +++ b/Emby.Server.Implementations/Localization/Core/eo.json @@ -122,5 +122,9 @@ "AuthenticationSucceededWithUserName": "{0} sukcese aŭtentikigis", "TaskKeyframeExtractorDescription": "Eltiras ĉefkadrojn el videodosieroj por krei pli precizajn HLS-ludlistojn. Ĉi tiu tasko povas funkcii dum longa tempo.", "TaskKeyframeExtractor": "Eltiri Ĉefkadrojn", - "External": "Ekstera" + "External": "Ekstera", + "TaskAudioNormalizationDescription": "Skanas dosierojn por sonnivelaj normaligaj datumoj.", + "TaskRefreshTrickplayImages": "Generi la bildojn por TrickPlay (Antaŭrigardo rapida antaŭen)", + "TaskAudioNormalization": "Normaligo Sonnivela", + "HearingImpaired": "Surda" } From 407935d18143e87c64b0e557aba00d3b2699f151 Mon Sep 17 00:00:00 2001 From: theguymadmax Date: Sun, 16 Mar 2025 19:00:00 -0400 Subject: [PATCH 41/56] Fix IMDb URL for People --- MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs b/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs index ff8ad1d612..980bac102e 100644 --- a/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs +++ b/MediaBrowser.Providers/Movies/ImdbExternalUrlProvider.cs @@ -19,7 +19,14 @@ public class ImdbExternalUrlProvider : IExternalUrlProvider var baseUrl = "https://www.imdb.com/"; if (item.TryGetProviderId(MetadataProvider.Imdb, out var externalId)) { - yield return baseUrl + $"title/{externalId}"; + if (item is Person) + { + yield return baseUrl + $"name/{externalId}"; + } + else + { + yield return baseUrl + $"title/{externalId}"; + } } } } From 747fa4699a003db7e574b18e76f6c0b491bf8b21 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 17 Mar 2025 04:59:58 +0000 Subject: [PATCH 42/56] Update actions/setup-dotnet action to v4.3.1 --- .github/workflows/ci-codeql-analysis.yml | 2 +- .github/workflows/ci-compat.yml | 4 ++-- .github/workflows/ci-openapi.yml | 4 ++-- .github/workflows/ci-tests.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 850214fec1..4fe073a63a 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -22,7 +22,7 @@ jobs: - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Setup .NET - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: '9.0.x' diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index ca505790cc..167afb342b 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -17,7 +17,7 @@ jobs: repository: ${{ github.event.pull_request.head.repo.full_name }} - name: Setup .NET - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: '9.0.x' @@ -47,7 +47,7 @@ jobs: fetch-depth: 0 - name: Setup .NET - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: '9.0.x' diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index 5a32849877..c3f6b513c7 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -21,7 +21,7 @@ jobs: ref: ${{ github.event.pull_request.head.sha }} repository: ${{ github.event.pull_request.head.repo.full_name }} - name: Setup .NET - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: '9.0.x' - name: Generate openapi.json @@ -55,7 +55,7 @@ jobs: ANCESTOR_REF=$(git merge-base upstream/${{ github.base_ref }} origin/$HEAD_REF) git checkout --progress --force $ANCESTOR_REF - name: Setup .NET - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0 + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: '9.0.x' - name: Generate openapi.json diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index ec78396db0..04c8465e95 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -22,7 +22,7 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - - uses: actions/setup-dotnet@3951f0dfe7a07e2313ec93c75700083e2005cbab # v4.3.0 + - uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4.3.1 with: dotnet-version: ${{ env.SDK_VERSION }} From 62fc2b8d0d2a0f4cab13c25a89fc26c78b505caf Mon Sep 17 00:00:00 2001 From: Thunderstrike116 Date: Sun, 16 Mar 2025 09:41:30 +0000 Subject: [PATCH 43/56] Translated using Weblate (Greek) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/el/ --- Emby.Server.Implementations/Localization/Core/el.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/el.json b/Emby.Server.Implementations/Localization/Core/el.json index 631e659d5c..f3195f0ea0 100644 --- a/Emby.Server.Implementations/Localization/Core/el.json +++ b/Emby.Server.Implementations/Localization/Core/el.json @@ -125,7 +125,7 @@ "TaskKeyframeExtractor": "Εξαγωγέας βασικών καρέ βίντεο", "External": "Εξωτερικό", "HearingImpaired": "Με προβλήματα ακοής", - "TaskRefreshTrickplayImages": "Δημιουργήστε εικόνες Trickplay", + "TaskRefreshTrickplayImages": "Δημιουργία εικόνων Trickplay", "TaskRefreshTrickplayImagesDescription": "Δημιουργεί προεπισκοπήσεις trickplay για βίντεο σε ενεργοποιημένες βιβλιοθήκες.", "TaskAudioNormalization": "Ομοιομορφία ήχου", "TaskAudioNormalizationDescription": "Ανίχνευση αρχείων για δεδομένα ομοιομορφίας ήχου.", From e1392ca1b62355d9ce16177b9f69d2bd56c1e0d0 Mon Sep 17 00:00:00 2001 From: Blackspirits Date: Mon, 17 Mar 2025 19:09:51 +0000 Subject: [PATCH 44/56] Translated using Weblate (Portuguese (Portugal)) Translation: Jellyfin/Jellyfin Translate-URL: https://translate.jellyfin.org/projects/jellyfin/jellyfin-core/pt_PT/ --- Emby.Server.Implementations/Localization/Core/pt-PT.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/Core/pt-PT.json b/Emby.Server.Implementations/Localization/Core/pt-PT.json index 879bf64b0c..42ea5e0a46 100644 --- a/Emby.Server.Implementations/Localization/Core/pt-PT.json +++ b/Emby.Server.Implementations/Localization/Core/pt-PT.json @@ -1,6 +1,6 @@ { "Albums": "Álbuns", - "AppDeviceValues": "Aplicação {0}, Dispositivo: {1}", + "AppDeviceValues": "Aplicação: {0}, Dispositivo: {1}", "Application": "Aplicação", "Artists": "Artistas", "AuthenticationSucceededWithUserName": "{0} autenticado com sucesso", From 85b5bebda4a887bad03a114e727d9ee5d87961cc Mon Sep 17 00:00:00 2001 From: Cody Robibero Date: Tue, 18 Mar 2025 17:37:04 -0600 Subject: [PATCH 45/56] Add fast-path to getting just the SeriesPresentationUniqueKey for NextUp (#13687) * Add more optimized query to calculate series that should be processed for next up * Filter series based on last watched date --- .../Library/LibraryManager.cs | 15 ++ .../TV/TVSeriesManager.cs | 59 ++------ Jellyfin.Api/Controllers/TvShowsController.cs | 4 +- .../Item/BaseItemRepository.cs | 31 +++++ .../Library/ILibraryManager.cs | 9 ++ .../Persistence/IItemRepository.cs | 8 ++ MediaBrowser.Model/Querying/NextUpQuery.cs | 131 +++++++++--------- 7 files changed, 136 insertions(+), 121 deletions(-) diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index cc2092e21e..7b3a540398 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -1344,6 +1344,21 @@ namespace Emby.Server.Implementations.Library return _itemRepository.GetItemList(query); } + public IReadOnlyList GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection parents, DateTime dateCutoff) + { + SetTopParentIdsOrAncestors(query, parents); + + if (query.AncestorIds.Length == 0 && query.TopParentIds.Length == 0) + { + if (query.User is not null) + { + AddUserToQuery(query, query.User); + } + } + + return _itemRepository.GetNextUpSeriesKeys(query, dateCutoff); + } + public QueryResult QueryItems(InternalItemsQuery query) { if (query.User is not null) diff --git a/Emby.Server.Implementations/TV/TVSeriesManager.cs b/Emby.Server.Implementations/TV/TVSeriesManager.cs index f8ce473da3..10d27498bf 100644 --- a/Emby.Server.Implementations/TV/TVSeriesManager.cs +++ b/Emby.Server.Implementations/TV/TVSeriesManager.cs @@ -91,7 +91,7 @@ namespace Emby.Server.Implementations.TV if (!string.IsNullOrEmpty(presentationUniqueKey)) { - return GetResult(GetNextUpEpisodes(request, user, new[] { presentationUniqueKey }, options), request); + return GetResult(GetNextUpEpisodes(request, user, [presentationUniqueKey], options), request); } if (limit.HasValue) @@ -99,25 +99,9 @@ namespace Emby.Server.Implementations.TV limit = limit.Value + 10; } - var items = _libraryManager - .GetItemList( - new InternalItemsQuery(user) - { - IncludeItemTypes = new[] { BaseItemKind.Episode }, - OrderBy = new[] { (ItemSortBy.DatePlayed, SortOrder.Descending) }, - SeriesPresentationUniqueKey = presentationUniqueKey, - Limit = limit, - DtoOptions = new DtoOptions { Fields = new[] { ItemFields.SeriesPresentationUniqueKey }, EnableImages = false }, - GroupBySeriesPresentationUniqueKey = true - }, - parentsFolders.ToList()) - .Cast() - .Where(episode => !string.IsNullOrEmpty(episode.SeriesPresentationUniqueKey)) - .Select(GetUniqueSeriesKey) - .ToList(); + var nextUpSeriesKeys = _libraryManager.GetNextUpSeriesKeys(new InternalItemsQuery(user) { Limit = limit }, parentsFolders, request.NextUpDateCutoff); - // Avoid implicitly captured closure - var episodes = GetNextUpEpisodes(request, user, items.Distinct().ToArray(), options); + var episodes = GetNextUpEpisodes(request, user, nextUpSeriesKeys, options); return GetResult(episodes, request); } @@ -133,36 +117,11 @@ namespace Emby.Server.Implementations.TV .OrderByDescending(i => i.LastWatchedDate); } - // If viewing all next up for all series, remove first episodes - // But if that returns empty, keep those first episodes (avoid completely empty view) - var alwaysEnableFirstEpisode = !request.SeriesId.IsNullOrEmpty(); - var anyFound = false; - return allNextUp - .Where(i => - { - if (request.DisableFirstEpisode) - { - return i.LastWatchedDate != DateTime.MinValue; - } - - if (alwaysEnableFirstEpisode || (i.LastWatchedDate != DateTime.MinValue && i.LastWatchedDate.Date >= request.NextUpDateCutoff)) - { - anyFound = true; - return true; - } - - return !anyFound && i.LastWatchedDate == DateTime.MinValue; - }) .Select(i => i.GetEpisodeFunction()) .Where(i => i is not null)!; } - private static string GetUniqueSeriesKey(Episode episode) - { - return episode.SeriesPresentationUniqueKey; - } - private static string GetUniqueSeriesKey(Series series) { return series.GetPresentationUniqueKey(); @@ -178,13 +137,13 @@ namespace Emby.Server.Implementations.TV { AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, - IncludeItemTypes = new[] { BaseItemKind.Episode }, + IncludeItemTypes = [BaseItemKind.Episode], IsPlayed = true, Limit = 1, ParentIndexNumberNotEquals = 0, DtoOptions = new DtoOptions { - Fields = new[] { ItemFields.SortName }, + Fields = [ItemFields.SortName], EnableImages = false } }; @@ -202,8 +161,8 @@ namespace Emby.Server.Implementations.TV { AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, - IncludeItemTypes = new[] { BaseItemKind.Episode }, - OrderBy = new[] { (ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending) }, + IncludeItemTypes = [BaseItemKind.Episode], + OrderBy = [(ItemSortBy.ParentIndexNumber, SortOrder.Ascending), (ItemSortBy.IndexNumber, SortOrder.Ascending)], Limit = 1, IsPlayed = includePlayed, IsVirtualItem = false, @@ -228,7 +187,7 @@ namespace Emby.Server.Implementations.TV AncestorWithPresentationUniqueKey = null, SeriesPresentationUniqueKey = seriesKey, ParentIndexNumber = 0, - IncludeItemTypes = new[] { BaseItemKind.Episode }, + IncludeItemTypes = [BaseItemKind.Episode], IsPlayed = includePlayed, IsVirtualItem = false, DtoOptions = dtoOptions @@ -248,7 +207,7 @@ namespace Emby.Server.Implementations.TV consideredEpisodes.Add(nextEpisode); } - var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, new[] { (ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending) }) + var sortedConsideredEpisodes = _libraryManager.Sort(consideredEpisodes, user, [(ItemSortBy.AiredEpisodeOrder, SortOrder.Ascending)]) .Cast(); if (lastWatchedEpisode is not null) { diff --git a/Jellyfin.Api/Controllers/TvShowsController.cs b/Jellyfin.Api/Controllers/TvShowsController.cs index df46c2dac9..cc070244b1 100644 --- a/Jellyfin.Api/Controllers/TvShowsController.cs +++ b/Jellyfin.Api/Controllers/TvShowsController.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; +using Jellyfin.Api.Attributes; using Jellyfin.Api.Extensions; using Jellyfin.Api.Helpers; using Jellyfin.Api.ModelBinders; @@ -86,7 +87,7 @@ public class TvShowsController : BaseJellyfinApiController [FromQuery] bool? enableUserData, [FromQuery] DateTime? nextUpDateCutoff, [FromQuery] bool enableTotalRecordCount = true, - [FromQuery] bool disableFirstEpisode = false, + [FromQuery][ParameterObsolete] bool disableFirstEpisode = false, [FromQuery] bool enableResumable = true, [FromQuery] bool enableRewatching = false) { @@ -109,7 +110,6 @@ public class TvShowsController : BaseJellyfinApiController StartIndex = startIndex, User = user, EnableTotalRecordCount = enableTotalRecordCount, - DisableFirstEpisode = disableFirstEpisode, NextUpDateCutoff = nextUpDateCutoff ?? DateTime.MinValue, EnableResumable = enableResumable, EnableRewatching = enableRewatching diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 392b7de74f..e20ad79ad1 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -255,6 +255,37 @@ public sealed class BaseItemRepository return dbQuery.AsEnumerable().Where(e => e is not null).Select(w => DeserialiseBaseItem(w, filter.SkipDeserialization)).ToArray(); } + /// + public IReadOnlyList GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff) + { + ArgumentNullException.ThrowIfNull(filter); + ArgumentNullException.ThrowIfNull(filter.User); + + using var context = _dbProvider.CreateDbContext(); + + var query = context.BaseItems + .AsNoTracking() + .Where(i => filter.TopParentIds.Contains(i.TopParentId!.Value)) + .Where(i => i.Type == _itemTypeLookup.BaseItemKindNames[BaseItemKind.Episode]) + .Join( + context.UserData.AsNoTracking(), + i => new { UserId = filter.User.Id, ItemId = i.Id }, + u => new { UserId = u.UserId, ItemId = u.ItemId }, + (entity, data) => new { Item = entity, UserData = data }) + .GroupBy(g => g.Item.SeriesPresentationUniqueKey) + .Select(g => new { g.Key, LastPlayedDate = g.Max(u => u.UserData.LastPlayedDate) }) + .Where(g => g.Key != null && g.LastPlayedDate != null && g.LastPlayedDate >= dateCutoff) + .OrderByDescending(g => g.LastPlayedDate) + .Select(g => g.Key!); + + if (filter.Limit.HasValue) + { + query = query.Take(filter.Limit.Value); + } + + return query.ToArray(); + } + private IQueryable ApplyGroupingFilter(IQueryable dbQuery, InternalItemsQuery filter) { // This whole block is needed to filter duplicate entries on request diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 47b1cb16e8..03a28fd8c0 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -565,6 +565,15 @@ namespace MediaBrowser.Controller.Library /// List of items. IReadOnlyList GetItemList(InternalItemsQuery query, List parents); + /// + /// Gets the list of series presentation keys for next up. + /// + /// The query to use. + /// Items to use for query. + /// The minimum date for a series to have been most recently watched. + /// List of series presentation keys. + IReadOnlyList GetNextUpSeriesKeys(InternalItemsQuery query, IReadOnlyCollection parents, DateTime dateCutoff); + /// /// Gets the items result. /// diff --git a/MediaBrowser.Controller/Persistence/IItemRepository.cs b/MediaBrowser.Controller/Persistence/IItemRepository.cs index afe2d833d5..f1ed4fe274 100644 --- a/MediaBrowser.Controller/Persistence/IItemRepository.cs +++ b/MediaBrowser.Controller/Persistence/IItemRepository.cs @@ -59,6 +59,14 @@ public interface IItemRepository /// List<BaseItem>. IReadOnlyList GetItemList(InternalItemsQuery filter); + /// + /// Gets the list of series presentation keys for next up. + /// + /// The query. + /// The minimum date for a series to have been most recently watched. + /// The list of keys. + IReadOnlyList GetNextUpSeriesKeys(InternalItemsQuery filter, DateTime dateCutoff); + /// /// Updates the inherited values. /// diff --git a/MediaBrowser.Model/Querying/NextUpQuery.cs b/MediaBrowser.Model/Querying/NextUpQuery.cs index 8dece28a09..aee720aa7b 100644 --- a/MediaBrowser.Model/Querying/NextUpQuery.cs +++ b/MediaBrowser.Model/Querying/NextUpQuery.cs @@ -4,76 +4,69 @@ using System; using Jellyfin.Data.Entities; using MediaBrowser.Model.Entities; -namespace MediaBrowser.Model.Querying +namespace MediaBrowser.Model.Querying; + +public class NextUpQuery { - public class NextUpQuery + public NextUpQuery() { - public NextUpQuery() - { - EnableImageTypes = Array.Empty(); - EnableTotalRecordCount = true; - DisableFirstEpisode = false; - NextUpDateCutoff = DateTime.MinValue; - EnableResumable = false; - EnableRewatching = false; - } - - /// - /// Gets or sets the user. - /// - /// The user. - public required User User { get; set; } - - /// - /// Gets or sets the parent identifier. - /// - /// The parent identifier. - public Guid? ParentId { get; set; } - - /// - /// Gets or sets the series id. - /// - /// The series id. - public Guid? SeriesId { get; set; } - - /// - /// Gets or sets the start index. Use for paging. - /// - /// The start index. - public int? StartIndex { get; set; } - - /// - /// Gets or sets the maximum number of items to return. - /// - /// The limit. - public int? Limit { get; set; } - - /// - /// Gets or sets the enable image types. - /// - /// The enable image types. - public ImageType[] EnableImageTypes { get; set; } - - public bool EnableTotalRecordCount { get; set; } - - /// - /// Gets or sets a value indicating whether do disable sending first episode as next up. - /// - public bool DisableFirstEpisode { get; set; } - - /// - /// Gets or sets a value indicating the oldest date for a show to appear in Next Up. - /// - public DateTime NextUpDateCutoff { get; set; } - - /// - /// Gets or sets a value indicating whether to include resumable episodes as next up. - /// - public bool EnableResumable { get; set; } - - /// - /// Gets or sets a value indicating whether getting rewatching next up list. - /// - public bool EnableRewatching { get; set; } + EnableImageTypes = Array.Empty(); + EnableTotalRecordCount = true; + NextUpDateCutoff = DateTime.MinValue; + EnableResumable = false; + EnableRewatching = false; } + + /// + /// Gets or sets the user. + /// + /// The user. + public required User User { get; set; } + + /// + /// Gets or sets the parent identifier. + /// + /// The parent identifier. + public Guid? ParentId { get; set; } + + /// + /// Gets or sets the series id. + /// + /// The series id. + public Guid? SeriesId { get; set; } + + /// + /// Gets or sets the start index. Use for paging. + /// + /// The start index. + public int? StartIndex { get; set; } + + /// + /// Gets or sets the maximum number of items to return. + /// + /// The limit. + public int? Limit { get; set; } + + /// + /// Gets or sets the enable image types. + /// + /// The enable image types. + public ImageType[] EnableImageTypes { get; set; } + + public bool EnableTotalRecordCount { get; set; } + + /// + /// Gets or sets a value indicating the oldest date for a show to appear in Next Up. + /// + public DateTime NextUpDateCutoff { get; set; } + + /// + /// Gets or sets a value indicating whether to include resumable episodes as next up. + /// + public bool EnableResumable { get; set; } + + /// + /// Gets or sets a value indicating whether getting rewatching next up list. + /// + public bool EnableRewatching { get; set; } } From c24d0c1240d300a4912bb7c6810063b16078927f Mon Sep 17 00:00:00 2001 From: timminator <150205162+timminator@users.noreply.github.com> Date: Wed, 19 Mar 2025 00:40:06 +0100 Subject: [PATCH 46/56] Respect preferred language when selecting forced subtitles (#13098) Rework subtitle selection logic --- .../Library/MediaStreamSelector.cs | 86 +++++++++++++------ 1 file changed, 60 insertions(+), 26 deletions(-) diff --git a/Emby.Server.Implementations/Library/MediaStreamSelector.cs b/Emby.Server.Implementations/Library/MediaStreamSelector.cs index ea223e3ece..6791e3ca90 100644 --- a/Emby.Server.Implementations/Library/MediaStreamSelector.cs +++ b/Emby.Server.Implementations/Library/MediaStreamSelector.cs @@ -39,46 +39,48 @@ namespace Emby.Server.Implementations.Library return null; } + // Sort in the following order: Default > No tag > Forced var sortedStreams = streams .Where(i => i.Type == MediaStreamType.Subtitle) .OrderByDescending(x => x.IsExternal) - .ThenByDescending(x => x.IsForced && string.Equals(x.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) - .ThenByDescending(x => x.IsForced) .ThenByDescending(x => x.IsDefault) - .ThenByDescending(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) + .ThenByDescending(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages)) + .ThenByDescending(x => x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages)) + .ThenByDescending(x => x.IsForced && IsLanguageUndefined(x.Language)) + .ThenByDescending(x => x.IsForced) .ToList(); MediaStream? stream = null; + if (mode == SubtitlePlaybackMode.Default) { - // Load subtitles according to external, forced and default flags. - stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault); + // Load subtitles according to external, default and forced flags. + stream = sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsDefault || x.IsForced); } else if (mode == SubtitlePlaybackMode.Smart) { // Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages. - // If no subtitles of preferred language available, use default behaviour. + // If no subtitles of preferred language available, use none. + // If the audio language is one of the user's preferred subtitle languages behave like OnlyForced. if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) { - stream = sortedStreams.FirstOrDefault(x => preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ?? - sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault); + stream = sortedStreams.FirstOrDefault(x => MatchesPreferredLanguage(x.Language, preferredLanguages)); } else { - // Respect forced flag. - stream = sortedStreams.FirstOrDefault(x => x.IsForced); + stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault(); } } else if (mode == SubtitlePlaybackMode.Always) { - // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise default behaviour. - stream = sortedStreams.FirstOrDefault(x => !x.IsForced && preferredLanguages.Contains(x.Language, StringComparison.OrdinalIgnoreCase)) ?? - sortedStreams.FirstOrDefault(x => x.IsExternal || x.IsForced || x.IsDefault); + // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behaviour. + stream = sortedStreams.FirstOrDefault(x => !x.IsForced && MatchesPreferredLanguage(x.Language, preferredLanguages)) ?? + BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault(); } else if (mode == SubtitlePlaybackMode.OnlyForced) { - // Only load subtitles that are flagged forced. - stream = sortedStreams.FirstOrDefault(x => x.IsForced); + // Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language + stream = BehaviorOnlyForced(sortedStreams, preferredLanguages).FirstOrDefault(); } return stream?.Index; @@ -110,40 +112,72 @@ namespace Emby.Server.Implementations.Library if (mode == SubtitlePlaybackMode.Default) { // Prefer embedded metadata over smart logic - filteredStreams = sortedStreams.Where(s => s.IsForced || s.IsDefault) + // Load subtitles according to external, default, and forced flags. + filteredStreams = sortedStreams.Where(s => s.IsExternal || s.IsDefault || s.IsForced) .ToList(); } else if (mode == SubtitlePlaybackMode.Smart) { // Prefer smart logic over embedded metadata + // Only attempt to load subtitles if the audio language is not one of the user's preferred subtitle languages, otherwise OnlyForced behavior. if (!preferredLanguages.Contains(audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) { - filteredStreams = sortedStreams.Where(s => !s.IsForced && preferredLanguages.Contains(s.Language, StringComparison.OrdinalIgnoreCase)) + filteredStreams = sortedStreams.Where(s => MatchesPreferredLanguage(s.Language, preferredLanguages)) .ToList(); } + else + { + filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages); + } } else if (mode == SubtitlePlaybackMode.Always) { - // Always load the most suitable full subtitles - filteredStreams = sortedStreams.Where(s => !s.IsForced).ToList(); + // Always load (full/non-forced) subtitles of the user's preferred subtitle language if possible, otherwise OnlyForced behavior. + filteredStreams = sortedStreams.Where(s => !s.IsForced && MatchesPreferredLanguage(s.Language, preferredLanguages)) + .ToList() ?? BehaviorOnlyForced(sortedStreams, preferredLanguages); } else if (mode == SubtitlePlaybackMode.OnlyForced) { - // Always load the most suitable full subtitles - filteredStreams = sortedStreams.Where(s => s.IsForced).ToList(); + // Load subtitles that are flagged forced of the user's preferred subtitle language or with an undefined language + filteredStreams = BehaviorOnlyForced(sortedStreams, preferredLanguages); } - // Load forced subs if we have found no suitable full subtitles - var iterStreams = filteredStreams is null || filteredStreams.Count == 0 - ? sortedStreams.Where(s => s.IsForced && string.Equals(s.Language, audioTrackLanguage, StringComparison.OrdinalIgnoreCase)) - : filteredStreams; + // If filteredStreams is null, initialize it as an empty list to avoid null reference errors + filteredStreams ??= new List(); - foreach (var stream in iterStreams) + foreach (var stream in filteredStreams) { stream.Score = GetStreamScore(stream, preferredLanguages); } } + private static bool MatchesPreferredLanguage(string language, IReadOnlyList preferredLanguages) + { + // If preferredLanguages is empty, treat it as "any language" (wildcard) + return preferredLanguages.Count == 0 || + preferredLanguages.Contains(language, StringComparison.OrdinalIgnoreCase); + } + + private static bool IsLanguageUndefined(string language) + { + // Check for null, empty, or known placeholders + return string.IsNullOrEmpty(language) || + language.Equals("und", StringComparison.OrdinalIgnoreCase) || + language.Equals("unknown", StringComparison.OrdinalIgnoreCase) || + language.Equals("undetermined", StringComparison.OrdinalIgnoreCase) || + language.Equals("mul", StringComparison.OrdinalIgnoreCase) || + language.Equals("zxx", StringComparison.OrdinalIgnoreCase); + } + + private static List BehaviorOnlyForced(IEnumerable sortedStreams, IReadOnlyList preferredLanguages) + { + return sortedStreams + .Where(s => s.IsForced && (MatchesPreferredLanguage(s.Language, preferredLanguages) || IsLanguageUndefined(s.Language))) + .OrderByDescending(s => MatchesPreferredLanguage(s.Language, preferredLanguages)) + .ThenByDescending(s => IsLanguageUndefined(s.Language)) + .ToList(); + } + internal static int GetStreamScore(MediaStream stream, IReadOnlyList languagePreferences) { var index = languagePreferences.FindIndex(x => string.Equals(x, stream.Language, StringComparison.OrdinalIgnoreCase)); From 11fbca45ff8f417108d5655b7c0a4971e25df4c8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 18 Mar 2025 17:41:02 -0600 Subject: [PATCH 47/56] Update actions/download-artifact action to v4.2.0 (#13734) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/ci-compat.yml | 4 ++-- .github/workflows/ci-openapi.yml | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index 167afb342b..26299c202e 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -85,13 +85,13 @@ jobs: steps: - name: Download abi-head - uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 + uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 with: name: abi-head path: abi-head - name: Download abi-base - uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 + uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 with: name: abi-base path: abi-base diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index c3f6b513c7..b57be931c5 100644 --- a/.github/workflows/ci-openapi.yml +++ b/.github/workflows/ci-openapi.yml @@ -80,12 +80,12 @@ jobs: - openapi-base steps: - name: Download openapi-head - uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 + uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 with: name: openapi-head path: openapi-head - name: Download openapi-base - uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 + uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 with: name: openapi-base path: openapi-base @@ -158,7 +158,7 @@ jobs: run: |- echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV - name: Download openapi-head - uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 + uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 with: name: openapi-head path: openapi-head @@ -220,7 +220,7 @@ jobs: run: |- echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV - name: Download openapi-head - uses: actions/download-artifact@cc203385981b70ca67e1cc392babf9cc229d5806 # v4.1.9 + uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 with: name: openapi-head path: openapi-head From 3eca221cc6ea7af624153ff27a989aec37cb1cfb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 19 Mar 2025 18:27:37 +0000 Subject: [PATCH 48/56] Update CI dependencies --- .github/workflows/ci-codeql-analysis.yml | 6 +++--- .github/workflows/ci-compat.yml | 8 ++++---- .github/workflows/ci-openapi.yml | 12 ++++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci-codeql-analysis.yml b/.github/workflows/ci-codeql-analysis.yml index 4fe073a63a..6a3d4d3514 100644 --- a/.github/workflows/ci-codeql-analysis.yml +++ b/.github/workflows/ci-codeql-analysis.yml @@ -27,11 +27,11 @@ jobs: dotnet-version: '9.0.x' - name: Initialize CodeQL - uses: github/codeql-action/init@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 + uses: github/codeql-action/init@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 with: languages: ${{ matrix.language }} queries: +security-extended - name: Autobuild - uses: github/codeql-action/autobuild@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 + uses: github/codeql-action/autobuild@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11 + uses: github/codeql-action/analyze@5f8171a638ada777af81d42b55959a643bb29017 # v3.28.12 diff --git a/.github/workflows/ci-compat.yml b/.github/workflows/ci-compat.yml index 26299c202e..13b029e52c 100644 --- a/.github/workflows/ci-compat.yml +++ b/.github/workflows/ci-compat.yml @@ -26,7 +26,7 @@ jobs: dotnet build Jellyfin.Server -o ./out - name: Upload Head - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: abi-head retention-days: 14 @@ -65,7 +65,7 @@ jobs: dotnet build Jellyfin.Server -o ./out - name: Upload Head - uses: actions/upload-artifact@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: abi-base retention-days: 14 @@ -85,13 +85,13 @@ jobs: steps: - name: Download abi-head - uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: name: abi-head path: abi-head - name: Download abi-base - uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: name: abi-base path: abi-base diff --git a/.github/workflows/ci-openapi.yml b/.github/workflows/ci-openapi.yml index b57be931c5..95e090f9b6 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@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 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@4cec3d8aa04e39d1a68397de0c4cd6fb9dce8ec1 # v4.6.1 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: openapi-base retention-days: 14 @@ -80,12 +80,12 @@ jobs: - openapi-base steps: - name: Download openapi-head - uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: name: openapi-head path: openapi-head - name: Download openapi-base - uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: name: openapi-base path: openapi-base @@ -158,7 +158,7 @@ jobs: run: |- echo "JELLYFIN_VERSION=$(date +'%Y%m%d%H%M%S')" >> $GITHUB_ENV - name: Download openapi-head - uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: name: openapi-head path: openapi-head @@ -220,7 +220,7 @@ jobs: run: |- echo "JELLYFIN_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV - name: Download openapi-head - uses: actions/download-artifact@b14cf4c92620c250e1c074ab0a5800e37df86765 # v4.2.0 + uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 with: name: openapi-head path: openapi-head From aabaf1a656e732e5208e251271bd1553c2b576de Mon Sep 17 00:00:00 2001 From: Lampan-git Date: Thu, 20 Mar 2025 05:55:51 -0400 Subject: [PATCH 49/56] Backport pull request #13720 from jellyfin/release-10.10.z Fix regression where "Search for missing metadata" not handling cast having multiple roles Original-merge: 91ca81eca7d2c984a096a396cbd83d0111f41c9d Merged-by: Bond-009 Backported-by: Bond_009 --- .../Manager/MetadataService.cs | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 1d3ddc4e24..e8994693de 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -1146,13 +1146,24 @@ namespace MediaBrowser.Providers.Manager private static void MergePeople(IReadOnlyList source, IReadOnlyList target) { - foreach (var person in target) - { - var normalizedName = person.Name.RemoveDiacritics(); - var personInSource = source.FirstOrDefault(i => string.Equals(i.Name.RemoveDiacritics(), normalizedName, StringComparison.OrdinalIgnoreCase)); + var sourceByName = source.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase); + var targetByName = target.ToLookup(p => p.Name.RemoveDiacritics(), StringComparer.OrdinalIgnoreCase); - if (personInSource is not null) + foreach (var name in targetByName.Select(g => g.Key)) + { + var targetPeople = targetByName[name].ToArray(); + var sourcePeople = sourceByName[name].ToArray(); + + if (sourcePeople.Length == 0) { + continue; + } + + for (int i = 0; i < targetPeople.Length; i++) + { + var person = targetPeople[i]; + var personInSource = i < sourcePeople.Length ? sourcePeople[i] : sourcePeople[0]; + foreach (var providerId in personInSource.ProviderIds) { person.ProviderIds.TryAdd(providerId.Key, providerId.Value); From 350983e03cc354e083cccdd156d3672cc7125685 Mon Sep 17 00:00:00 2001 From: timminator <150205162+timminator@users.noreply.github.com> Date: Thu, 20 Mar 2025 14:10:48 +0100 Subject: [PATCH 50/56] Fix OnPlaybackStopped task erroring out (#13226) --- .../Library/MediaSourceManager.cs | 10 +++++++--- .../Session/SessionManager.cs | 16 +++++++++++++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/Emby.Server.Implementations/Library/MediaSourceManager.cs b/Emby.Server.Implementations/Library/MediaSourceManager.cs index 5795c47ccc..92a5e9ffd9 100644 --- a/Emby.Server.Implementations/Library/MediaSourceManager.cs +++ b/Emby.Server.Implementations/Library/MediaSourceManager.cs @@ -782,9 +782,13 @@ namespace Emby.Server.Implementations.Library { ArgumentException.ThrowIfNullOrEmpty(id); - // TODO probably shouldn't throw here but it is kept for "backwards compatibility" - var info = GetLiveStreamInfo(id) ?? throw new ResourceNotFoundException(); - return Task.FromResult(new Tuple(info.MediaSource, info as IDirectStreamProvider)); + var info = GetLiveStreamInfo(id); + if (info is null) + { + return Task.FromResult>(new Tuple(null, null)); + } + + return Task.FromResult>(new Tuple(info.MediaSource, info as IDirectStreamProvider)); } public ILiveStream GetLiveStreamInfo(string id) diff --git a/Emby.Server.Implementations/Session/SessionManager.cs b/Emby.Server.Implementations/Session/SessionManager.cs index 030da6f73e..df2acfc46c 100644 --- a/Emby.Server.Implementations/Session/SessionManager.cs +++ b/Emby.Server.Implementations/Session/SessionManager.cs @@ -343,6 +343,11 @@ namespace Emby.Server.Implementations.Session /// Task. private async Task UpdateNowPlayingItem(SessionInfo session, PlaybackProgressInfo info, BaseItem libraryItem, bool updateLastCheckInTime) { + if (session is null) + { + return; + } + if (string.IsNullOrEmpty(info.MediaSourceId)) { info.MediaSourceId = info.ItemId.ToString("N", CultureInfo.InvariantCulture); @@ -675,6 +680,11 @@ namespace Emby.Server.Implementations.Session private BaseItem GetNowPlayingItem(SessionInfo session, Guid itemId) { + if (session is null) + { + return null; + } + var item = session.FullNowPlayingItem; if (item is not null && item.Id.Equals(itemId)) { @@ -794,7 +804,11 @@ namespace Emby.Server.Implementations.Session ArgumentNullException.ThrowIfNull(info); - var session = GetSession(info.SessionId); + var session = GetSession(info.SessionId, false); + if (session is null) + { + return; + } var libraryItem = info.ItemId.IsEmpty() ? null From c77a0719c28ec6744c0618c18e93f2b36c7d95f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fernando=20Fern=C3=A1ndez?= Date: Sun, 23 Mar 2025 01:30:32 +0100 Subject: [PATCH 51/56] Clear dictionaries when not needed, use set for finding existing base items (#13749) --- .../Migrations/Routines/MigrateLibraryDb.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs index dfe497c490..d2fbcbec94 100644 --- a/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs +++ b/Jellyfin.Server/Migrations/Routines/MigrateLibraryDb.cs @@ -163,7 +163,6 @@ public class MigrateLibraryDb : IMigrationRoutine dbContext.UserData.ExecuteDelete(); var users = dbContext.Users.AsNoTracking().ToImmutableArray(); - var oldUserdata = new Dictionary(); foreach (var entity in queryResult) { @@ -184,6 +183,8 @@ public class MigrateLibraryDb : IMigrationRoutine dbContext.UserData.Add(userData); } + users.Clear(); + legacyBaseItemWithUserKeys.Clear(); _logger.LogInformation("Try saving {0} UserData entries.", dbContext.UserData.Local.Count); dbContext.SaveChanges(); @@ -220,11 +221,12 @@ public class MigrateLibraryDb : IMigrationRoutine dbContext.PeopleBaseItemMap.ExecuteDelete(); var peopleCache = new Dictionary Items)>(); + var baseItemIds = dbContext.BaseItems.Select(b => b.Id).ToHashSet(); foreach (SqliteDataReader reader in connection.Query(personsQuery)) { var itemId = reader.GetGuid(0); - if (!dbContext.BaseItems.Any(f => f.Id == itemId)) + if (!baseItemIds.Contains(itemId)) { _logger.LogError("Dont save person {0} because its not in use by any BaseItem", reader.GetString(1)); continue; @@ -256,12 +258,16 @@ public class MigrateLibraryDb : IMigrationRoutine }); } + baseItemIds.Clear(); + foreach (var item in peopleCache) { dbContext.Peoples.Add(item.Value.Person); dbContext.PeopleBaseItemMap.AddRange(item.Value.Items.DistinctBy(e => (e.ItemId, e.PeopleId))); } + peopleCache.Clear(); + _logger.LogInformation("Try saving {0} People entries.", dbContext.MediaStreamInfos.Local.Count); dbContext.SaveChanges(); migrationTotalTime += stopwatch.Elapsed; From 8b6aec7ce54df949b2940daa7f0c6b7d3201bda5 Mon Sep 17 00:00:00 2001 From: Adil Date: Sun, 23 Mar 2025 19:31:26 +0500 Subject: [PATCH 52/56] Rename Pakistan to select dropdown accessible name (#13752) --- Emby.Server.Implementations/Localization/countries.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Emby.Server.Implementations/Localization/countries.json b/Emby.Server.Implementations/Localization/countries.json index 0a11b3e458..d92dc880b1 100644 --- a/Emby.Server.Implementations/Localization/countries.json +++ b/Emby.Server.Implementations/Localization/countries.json @@ -336,7 +336,7 @@ "TwoLetterISORegionName": "IE" }, { - "DisplayName": "Islamic Republic of Pakistan", + "DisplayName": "Pakistan", "Name": "PK", "ThreeLetterISORegionName": "PAK", "TwoLetterISORegionName": "PK" From 8db6a39e92acfd76689e77c71b00ac96e60c515b Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sun, 23 Mar 2025 17:05:13 +0100 Subject: [PATCH 53/56] Remove all DB data on item removal, delete internal trickplay files (#13753) --- .../Data/CleanDatabaseScheduledTask.cs | 114 +++++++++--------- .../Library/LibraryManager.cs | 56 ++++++--- .../Library/PathManager.cs | 36 ++++++ .../Item/BaseItemRepository.cs | 23 ++-- .../Trickplay/TrickplayManager.cs | 16 +-- MediaBrowser.Controller/IO/IPathManager.cs | 17 +++ 6 files changed, 170 insertions(+), 92 deletions(-) create mode 100644 Emby.Server.Implementations/Library/PathManager.cs create mode 100644 MediaBrowser.Controller/IO/IPathManager.cs diff --git a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs index 7ea863d769..a83ded439c 100644 --- a/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs +++ b/Emby.Server.Implementations/Data/CleanDatabaseScheduledTask.cs @@ -5,80 +5,80 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Jellyfin.Server.Implementations; +using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Trickplay; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; -namespace Emby.Server.Implementations.Data +namespace Emby.Server.Implementations.Data; + +public class CleanDatabaseScheduledTask : ILibraryPostScanTask { - public class CleanDatabaseScheduledTask : ILibraryPostScanTask + private readonly ILibraryManager _libraryManager; + private readonly ILogger _logger; + private readonly IDbContextFactory _dbProvider; + + public CleanDatabaseScheduledTask( + ILibraryManager libraryManager, + ILogger logger, + IDbContextFactory dbProvider) { - private readonly ILibraryManager _libraryManager; - private readonly ILogger _logger; - private readonly IDbContextFactory _dbProvider; + _libraryManager = libraryManager; + _logger = logger; + _dbProvider = dbProvider; + } - public CleanDatabaseScheduledTask( - ILibraryManager libraryManager, - ILogger logger, - IDbContextFactory dbProvider) - { - _libraryManager = libraryManager; - _logger = logger; - _dbProvider = dbProvider; - } + public async Task Run(IProgress progress, CancellationToken cancellationToken) + { + await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false); + } - public async Task Run(IProgress progress, CancellationToken cancellationToken) + private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress progress) + { + var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery { - await CleanDeadItems(cancellationToken, progress).ConfigureAwait(false); - } + HasDeadParentId = true + }); - private async Task CleanDeadItems(CancellationToken cancellationToken, IProgress progress) + var numComplete = 0; + var numItems = itemIds.Count + 1; + + _logger.LogDebug("Cleaning {Number} items with dead parent links", numItems); + + foreach (var itemId in itemIds) { - var itemIds = _libraryManager.GetItemIds(new InternalItemsQuery + cancellationToken.ThrowIfCancellationRequested(); + + var item = _libraryManager.GetItemById(itemId); + if (item is not null) { - HasDeadParentId = true - }); + _logger.LogInformation("Cleaning item {Item} type: {Type} path: {Path}", item.Name, item.GetType().Name, item.Path ?? string.Empty); - var numComplete = 0; - var numItems = itemIds.Count + 1; - - _logger.LogDebug("Cleaning {0} items with dead parent links", numItems); - - foreach (var itemId in itemIds) - { - cancellationToken.ThrowIfCancellationRequested(); - - var item = _libraryManager.GetItemById(itemId); - - if (item is not null) + _libraryManager.DeleteItem(item, new DeleteOptions { - _logger.LogInformation("Cleaning item {0} type: {1} path: {2}", item.Name, item.GetType().Name, item.Path ?? string.Empty); - - _libraryManager.DeleteItem(item, new DeleteOptions - { - DeleteFileLocation = false - }); - } - - numComplete++; - double percent = numComplete; - percent /= numItems; - progress.Report(percent * 100); + DeleteFileLocation = false + }); } - var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); - await using (context.ConfigureAwait(false)) - { - var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); - await using (transaction.ConfigureAwait(false)) - { - await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); - await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); - } - } - - progress.Report(100); + numComplete++; + double percent = numComplete; + percent /= numItems; + progress.Report(percent * 100); } + + var context = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + await using (context.ConfigureAwait(false)) + { + var transaction = await context.Database.BeginTransactionAsync(cancellationToken).ConfigureAwait(false); + await using (transaction.ConfigureAwait(false)) + { + await context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + } + } + + progress.Report(100); } } diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 7b3a540398..3432aa3222 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -78,6 +78,7 @@ namespace Emby.Server.Implementations.Library private readonly NamingOptions _namingOptions; private readonly IPeopleRepository _peopleRepository; private readonly ExtraResolver _extraResolver; + private readonly IPathManager _pathManager; /// /// The _root folder sync lock. @@ -113,7 +114,8 @@ namespace Emby.Server.Implementations.Library /// The image processor. /// The naming options. /// The directory service. - /// The People Repository. + /// The people repository. + /// The path manager. public LibraryManager( IServerApplicationHost appHost, ILoggerFactory loggerFactory, @@ -130,7 +132,8 @@ namespace Emby.Server.Implementations.Library IImageProcessor imageProcessor, NamingOptions namingOptions, IDirectoryService directoryService, - IPeopleRepository peopleRepository) + IPeopleRepository peopleRepository, + IPathManager pathManager) { _appHost = appHost; _logger = loggerFactory.CreateLogger(); @@ -148,6 +151,7 @@ namespace Emby.Server.Implementations.Library _cache = new ConcurrentDictionary(); _namingOptions = namingOptions; _peopleRepository = peopleRepository; + _pathManager = pathManager; _extraResolver = new ExtraResolver(loggerFactory.CreateLogger(), namingOptions, directoryService); _configurationManager.ConfigurationUpdated += ConfigurationUpdated; @@ -200,33 +204,33 @@ namespace Emby.Server.Implementations.Library /// Gets or sets the postscan tasks. /// /// The postscan tasks. - private ILibraryPostScanTask[] PostscanTasks { get; set; } = Array.Empty(); + private ILibraryPostScanTask[] PostscanTasks { get; set; } = []; /// /// Gets or sets the intro providers. /// /// The intro providers. - private IIntroProvider[] IntroProviders { get; set; } = Array.Empty(); + private IIntroProvider[] IntroProviders { get; set; } = []; /// /// Gets or sets the list of entity resolution ignore rules. /// /// The entity resolution ignore rules. - private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = Array.Empty(); + private IResolverIgnoreRule[] EntityResolutionIgnoreRules { get; set; } = []; /// /// Gets or sets the list of currently registered entity resolvers. /// /// The entity resolvers enumerable. - private IItemResolver[] EntityResolvers { get; set; } = Array.Empty(); + private IItemResolver[] EntityResolvers { get; set; } = []; - private IMultiItemResolver[] MultiItemResolvers { get; set; } = Array.Empty(); + private IMultiItemResolver[] MultiItemResolvers { get; set; } = []; /// /// Gets or sets the comparers. /// /// The comparers. - private IBaseItemComparer[] Comparers { get; set; } = Array.Empty(); + private IBaseItemComparer[] Comparers { get; set; } = []; public bool IsScanRunning { get; private set; } @@ -359,7 +363,7 @@ namespace Emby.Server.Implementations.Library var children = item.IsFolder ? ((Folder)item).GetRecursiveChildren(false) - : Array.Empty(); + : []; foreach (var metadataPath in GetMetadataPaths(item, children)) { @@ -465,14 +469,28 @@ namespace Emby.Server.Implementations.Library ReportItemRemoved(item, parent); } - private static List GetMetadataPaths(BaseItem item, IEnumerable children) + private List GetMetadataPaths(BaseItem item, IEnumerable children) + { + var list = GetInternalMetadataPaths(item); + foreach (var child in children) + { + list.AddRange(GetInternalMetadataPaths(child)); + } + + return list; + } + + private List GetInternalMetadataPaths(BaseItem item) { var list = new List { item.GetInternalMetadataPath() }; - list.AddRange(children.Select(i => i.GetInternalMetadataPath())); + if (item is Video video) + { + list.Add(_pathManager.GetTrickplayDirectory(video)); + } return list; } @@ -593,7 +611,7 @@ namespace Emby.Server.Implementations.Library { _logger.LogError(ex, "Error in GetFilteredFileSystemEntries isPhysicalRoot: {0} IsVf: {1}", isPhysicalRoot, isVf); - files = Array.Empty(); + files = []; } else { @@ -1463,7 +1481,7 @@ namespace Emby.Server.Implementations.Library // Optimize by querying against top level views query.TopParentIds = parents.SelectMany(i => GetTopParentIdsForQuery(i, query.User)).ToArray(); - query.AncestorIds = Array.Empty(); + query.AncestorIds = []; // Prevent searching in all libraries due to empty filter if (query.TopParentIds.Length == 0) @@ -1583,7 +1601,7 @@ namespace Emby.Server.Implementations.Library return GetTopParentIdsForQuery(displayParent, user); } - return Array.Empty(); + return []; } if (!view.ParentId.IsEmpty()) @@ -1594,7 +1612,7 @@ namespace Emby.Server.Implementations.Library return GetTopParentIdsForQuery(displayParent, user); } - return Array.Empty(); + return []; } // Handle grouping @@ -1609,7 +1627,7 @@ namespace Emby.Server.Implementations.Library .SelectMany(i => GetTopParentIdsForQuery(i, user)); } - return Array.Empty(); + return []; } if (item is CollectionFolder collectionFolder) @@ -1623,7 +1641,7 @@ namespace Emby.Server.Implementations.Library return new[] { topParent.Id }; } - return Array.Empty(); + return []; } /// @@ -1667,7 +1685,7 @@ namespace Emby.Server.Implementations.Library { _logger.LogError(ex, "Error getting intros"); - return Enumerable.Empty(); + return []; } } @@ -2894,7 +2912,7 @@ namespace Emby.Server.Implementations.Library { var path = Path.Combine(virtualFolderPath, collectionType.ToString()!.ToLowerInvariant() + ".collection"); // Can't be null with legal values? - await File.WriteAllBytesAsync(path, Array.Empty()).ConfigureAwait(false); + await File.WriteAllBytesAsync(path, []).ConfigureAwait(false); } CollectionFolder.SaveLibraryOptions(virtualFolderPath, options); diff --git a/Emby.Server.Implementations/Library/PathManager.cs b/Emby.Server.Implementations/Library/PathManager.cs new file mode 100644 index 0000000000..c910abadbc --- /dev/null +++ b/Emby.Server.Implementations/Library/PathManager.cs @@ -0,0 +1,36 @@ +using System.Globalization; +using System.IO; +using MediaBrowser.Controller.Configuration; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; + +namespace Emby.Server.Implementations.Library; + +/// +/// IPathManager implementation. +/// +public class PathManager : IPathManager +{ + private readonly IServerConfigurationManager _config; + + /// + /// Initializes a new instance of the class. + /// + /// The server configuration manager. + public PathManager( + IServerConfigurationManager config) + { + _config = config; + } + + /// + public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false) + { + var basePath = _config.ApplicationPaths.TrickplayPath; + var idString = item.Id.ToString("N", CultureInfo.InvariantCulture); + + return saveWithMedia + ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay")) + : Path.Combine(basePath, idString); + } +} diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index e20ad79ad1..630a169cba 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -101,16 +101,23 @@ public sealed class BaseItemRepository using var context = _dbProvider.CreateDbContext(); using var transaction = context.Database.BeginTransaction(); + context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).ExecuteDelete(); + context.AttachmentStreamInfos.Where(e => e.ItemId == id).ExecuteDelete(); + context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete(); + context.BaseItemMetadataFields.Where(e => e.ItemId == id).ExecuteDelete(); + context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete(); + context.BaseItemTrailerTypes.Where(e => e.ItemId == id).ExecuteDelete(); + context.BaseItems.Where(e => e.Id == id).ExecuteDelete(); + context.Chapters.Where(e => e.ItemId == id).ExecuteDelete(); + context.CustomItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete(); + context.ItemDisplayPreferences.Where(e => e.ItemId == id).ExecuteDelete(); + context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete(); + context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete(); + context.MediaSegments.Where(e => e.ItemId == id).ExecuteDelete(); + context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete(); context.PeopleBaseItemMap.Where(e => e.ItemId == id).ExecuteDelete(); context.Peoples.Where(e => e.BaseItems!.Count == 0).ExecuteDelete(); - context.Chapters.Where(e => e.ItemId == id).ExecuteDelete(); - context.MediaStreamInfos.Where(e => e.ItemId == id).ExecuteDelete(); - context.AncestorIds.Where(e => e.ItemId == id || e.ParentItemId == id).ExecuteDelete(); - context.ItemValuesMap.Where(e => e.ItemId == id).ExecuteDelete(); - context.ItemValues.Where(e => e.BaseItemsMap!.Count == 0).ExecuteDelete(); - context.BaseItemImageInfos.Where(e => e.ItemId == id).ExecuteDelete(); - context.BaseItemProviders.Where(e => e.ItemId == id).ExecuteDelete(); - context.BaseItems.Where(e => e.Id == id).ExecuteDelete(); + context.TrickplayInfos.Where(e => e.ItemId == id).ExecuteDelete(); context.SaveChanges(); transaction.Commit(); } diff --git a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs index 9c0f5b57b4..6949ec1a8c 100644 --- a/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs +++ b/Jellyfin.Server.Implementations/Trickplay/TrickplayManager.cs @@ -12,6 +12,7 @@ using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Trickplay; @@ -37,9 +38,10 @@ public class TrickplayManager : ITrickplayManager private readonly IImageEncoder _imageEncoder; private readonly IDbContextFactory _dbProvider; private readonly IApplicationPaths _appPaths; + private readonly IPathManager _pathManager; private static readonly AsyncNonKeyedLocker _resourcePool = new(1); - private static readonly string[] _trickplayImgExtensions = { ".jpg" }; + private static readonly string[] _trickplayImgExtensions = [".jpg"]; /// /// Initializes a new instance of the class. @@ -53,6 +55,7 @@ public class TrickplayManager : ITrickplayManager /// The image encoder. /// The database provider. /// The application paths. + /// The path manager. public TrickplayManager( ILogger logger, IMediaEncoder mediaEncoder, @@ -62,7 +65,8 @@ public class TrickplayManager : ITrickplayManager IServerConfigurationManager config, IImageEncoder imageEncoder, IDbContextFactory dbProvider, - IApplicationPaths appPaths) + IApplicationPaths appPaths, + IPathManager pathManager) { _logger = logger; _mediaEncoder = mediaEncoder; @@ -73,6 +77,7 @@ public class TrickplayManager : ITrickplayManager _imageEncoder = imageEncoder; _dbProvider = dbProvider; _appPaths = appPaths; + _pathManager = pathManager; } /// @@ -610,12 +615,7 @@ public class TrickplayManager : ITrickplayManager /// public string GetTrickplayDirectory(BaseItem item, int tileWidth, int tileHeight, int width, bool saveWithMedia = false) { - var basePath = _config.ApplicationPaths.TrickplayPath; - var idString = item.Id.ToString("N", CultureInfo.InvariantCulture); - var path = saveWithMedia - ? Path.Combine(item.ContainingFolderPath, Path.ChangeExtension(item.Path, ".trickplay")) - : Path.Combine(basePath, idString); - + var path = _pathManager.GetTrickplayDirectory(item, saveWithMedia); var subdirectory = string.Format( CultureInfo.InvariantCulture, "{0} - {1}x{2}", diff --git a/MediaBrowser.Controller/IO/IPathManager.cs b/MediaBrowser.Controller/IO/IPathManager.cs new file mode 100644 index 0000000000..0368898102 --- /dev/null +++ b/MediaBrowser.Controller/IO/IPathManager.cs @@ -0,0 +1,17 @@ +using MediaBrowser.Controller.Entities; + +namespace MediaBrowser.Controller.IO; + +/// +/// Interface ITrickplayManager. +/// +public interface IPathManager +{ + /// + /// Gets the path to the trickplay image base folder. + /// + /// The item. + /// Whether or not the tile should be saved next to the media file. + /// The absolute path. + public string GetTrickplayDirectory(BaseItem item, bool saveWithMedia = false); +} From dfb485d1f205c8eda95bc3b79d341f3c3aef7ec4 Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sun, 23 Mar 2025 17:05:40 +0100 Subject: [PATCH 54/56] Rework season folder parsing (#11748) --- Emby.Naming/TV/SeasonPathParser.cs | 108 ++++++++---------- .../Library/LibraryManager.cs | 7 +- .../Library/Resolvers/TV/SeasonResolver.cs | 2 +- .../Library/Resolvers/TV/SeriesResolver.cs | 7 +- MediaBrowser.Controller/Entities/TV/Season.cs | 2 +- .../Library/ILibraryManager.cs | 3 +- .../TV/SeasonPathParserTests.cs | 68 +++++++---- 7 files changed, 105 insertions(+), 92 deletions(-) diff --git a/Emby.Naming/TV/SeasonPathParser.cs b/Emby.Naming/TV/SeasonPathParser.cs index 45b91971bf..98ee1e4b8f 100644 --- a/Emby.Naming/TV/SeasonPathParser.cs +++ b/Emby.Naming/TV/SeasonPathParser.cs @@ -1,43 +1,35 @@ using System; using System.Globalization; using System.IO; +using System.Text.RegularExpressions; namespace Emby.Naming.TV { /// /// Class to parse season paths. /// - public static class SeasonPathParser + public static partial class SeasonPathParser { - /// - /// A season folder must contain one of these somewhere in the name. - /// - private static readonly string[] _seasonFolderNames = - { - "season", - "sæson", - "temporada", - "saison", - "staffel", - "series", - "сезон", - "stagione" - }; + [GeneratedRegex(@"^\s*((?(?>\d+))(?:st|nd|rd|th|\.)*(?!\s*[Ee]\d+))\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?.*)$")] + private static partial Regex ProcessPre(); - private static readonly char[] _splitChars = ['.', '_', ' ', '-']; + [GeneratedRegex(@"^\s*(?:[[시즌]*|[シーズン]*|[sS](?:eason|æson|aison|taffel|eries|tagione|äsong|eizoen|easong|ezon|ezona|ezóna|ezonul)*|[tT](?:emporada)*|[kK](?:ausi)*|[Сс](?:езон)*)\s*(?(?>\d+)(?!\s*[Ee]\d+))(?.*)$")] + private static partial Regex ProcessPost(); /// /// Attempts to parse season number from path. /// /// Path to season. + /// Folder name of the parent. /// Support special aliases when parsing. /// Support numeric season folders when parsing. /// Returns object. - public static SeasonPathParserResult Parse(string path, bool supportSpecialAliases, bool supportNumericSeasonFolders) + public static SeasonPathParserResult Parse(string path, string? parentPath, bool supportSpecialAliases, bool supportNumericSeasonFolders) { var result = new SeasonPathParserResult(); + var parentFolderName = parentPath is null ? null : new DirectoryInfo(parentPath).Name; - var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, supportSpecialAliases, supportNumericSeasonFolders); + var (seasonNumber, isSeasonFolder) = GetSeasonNumberFromPath(path, parentFolderName, supportSpecialAliases, supportNumericSeasonFolders); result.SeasonNumber = seasonNumber; @@ -54,15 +46,24 @@ namespace Emby.Naming.TV /// Gets the season number from path. /// /// The path. + /// The parent folder name. /// if set to true [support special aliases]. /// if set to true [support numeric season folders]. /// System.Nullable{System.Int32}. private static (int? SeasonNumber, bool IsSeasonFolder) GetSeasonNumberFromPath( string path, + string? parentFolderName, bool supportSpecialAliases, bool supportNumericSeasonFolders) { string filename = Path.GetFileName(path); + filename = Regex.Replace(filename, "[ ._-]", string.Empty); + + if (parentFolderName is not null) + { + parentFolderName = Regex.Replace(parentFolderName, "[ ._-]", string.Empty); + filename = filename.Replace(parentFolderName, string.Empty, StringComparison.OrdinalIgnoreCase); + } if (supportSpecialAliases) { @@ -85,53 +86,38 @@ namespace Emby.Naming.TV } } - if (TryGetSeasonNumberFromPart(filename, out int seasonNumber)) + if (filename.StartsWith('s')) { + var testFilename = filename.AsSpan()[1..]; + + if (int.TryParse(testFilename, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val)) + { + return (val, true); + } + } + + var preMatch = ProcessPre().Match(filename); + if (preMatch.Success) + { + return CheckMatch(preMatch); + } + else + { + var postMatch = ProcessPost().Match(filename); + return CheckMatch(postMatch); + } + } + + private static (int? SeasonNumber, bool IsSeasonFolder) CheckMatch(Match match) + { + var numberString = match.Groups["seasonnumber"]; + if (numberString.Success) + { + var seasonNumber = int.Parse(numberString.Value, CultureInfo.InvariantCulture); return (seasonNumber, true); } - // Look for one of the season folder names - foreach (var name in _seasonFolderNames) - { - if (filename.Contains(name, StringComparison.OrdinalIgnoreCase)) - { - var result = GetSeasonNumberFromPathSubstring(filename.Replace(name, " ", StringComparison.OrdinalIgnoreCase)); - if (result.SeasonNumber.HasValue) - { - return result; - } - - break; - } - } - - var parts = filename.Split(_splitChars, StringSplitOptions.RemoveEmptyEntries); - foreach (var part in parts) - { - if (TryGetSeasonNumberFromPart(part, out seasonNumber)) - { - return (seasonNumber, true); - } - } - - return (null, true); - } - - private static bool TryGetSeasonNumberFromPart(ReadOnlySpan part, out int seasonNumber) - { - seasonNumber = 0; - if (part.Length < 2 || !part.StartsWith("s", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - if (int.TryParse(part.Slice(1), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) - { - seasonNumber = value; - return true; - } - - return false; + return (null, false); } /// diff --git a/Emby.Server.Implementations/Library/LibraryManager.cs b/Emby.Server.Implementations/Library/LibraryManager.cs index 3432aa3222..b810ad4de1 100644 --- a/Emby.Server.Implementations/Library/LibraryManager.cs +++ b/Emby.Server.Implementations/Library/LibraryManager.cs @@ -2512,8 +2512,11 @@ namespace Emby.Server.Implementations.Library } /// - public int? GetSeasonNumberFromPath(string path) - => SeasonPathParser.Parse(path, true, true).SeasonNumber; + public int? GetSeasonNumberFromPath(string path, Guid? parentId) + { + var parentPath = parentId.HasValue ? GetItemById(parentId.Value)?.ContainingFolderPath : null; + return SeasonPathParser.Parse(path, parentPath, true, true).SeasonNumber; + } /// public bool FillMissingEpisodeNumbersFromPath(Episode episode, bool forceRefresh) diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs index abf2d01159..6cb63a28a2 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeasonResolver.cs @@ -48,7 +48,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV var path = args.Path; - var seasonParserResult = SeasonPathParser.Parse(path, true, true); + var seasonParserResult = SeasonPathParser.Parse(path, series.ContainingFolderPath, true, true); var season = new Season { diff --git a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs index fb48d7bf17..c81a0adb89 100644 --- a/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs +++ b/Emby.Server.Implementations/Library/Resolvers/TV/SeriesResolver.cs @@ -118,7 +118,7 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV { if (child.IsDirectory) { - if (IsSeasonFolder(child.FullName, isTvContentType)) + if (IsSeasonFolder(child.FullName, path, isTvContentType)) { _logger.LogDebug("{Path} is a series because of season folder {Dir}.", path, child.FullName); return true; @@ -155,11 +155,12 @@ namespace Emby.Server.Implementations.Library.Resolvers.TV /// Determines whether [is season folder] [the specified path]. /// /// The path. + /// The parentpath. /// if set to true [is tv content type]. /// true if [is season folder] [the specified path]; otherwise, false. - private static bool IsSeasonFolder(string path, bool isTvContentType) + private static bool IsSeasonFolder(string path, string parentPath, bool isTvContentType) { - var seasonNumber = SeasonPathParser.Parse(path, isTvContentType, isTvContentType).SeasonNumber; + var seasonNumber = SeasonPathParser.Parse(path, parentPath, isTvContentType, isTvContentType).SeasonNumber; return seasonNumber.HasValue; } diff --git a/MediaBrowser.Controller/Entities/TV/Season.cs b/MediaBrowser.Controller/Entities/TV/Season.cs index e3fbe8e4d6..9dbac1e920 100644 --- a/MediaBrowser.Controller/Entities/TV/Season.cs +++ b/MediaBrowser.Controller/Entities/TV/Season.cs @@ -257,7 +257,7 @@ namespace MediaBrowser.Controller.Entities.TV if (!IndexNumber.HasValue && !string.IsNullOrEmpty(Path)) { - IndexNumber ??= LibraryManager.GetSeasonNumberFromPath(Path); + IndexNumber ??= LibraryManager.GetSeasonNumberFromPath(Path, ParentId); // If a change was made record it if (IndexNumber.HasValue) diff --git a/MediaBrowser.Controller/Library/ILibraryManager.cs b/MediaBrowser.Controller/Library/ILibraryManager.cs index 03a28fd8c0..e4490bca3b 100644 --- a/MediaBrowser.Controller/Library/ILibraryManager.cs +++ b/MediaBrowser.Controller/Library/ILibraryManager.cs @@ -426,8 +426,9 @@ namespace MediaBrowser.Controller.Library /// Gets the season number from path. /// /// The path. + /// The parent id. /// System.Nullable<System.Int32>. - int? GetSeasonNumberFromPath(string path); + int? GetSeasonNumberFromPath(string path, Guid? parentId); /// /// Fills the missing episode numbers from path. diff --git a/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs index 3a042df683..4c8ba58d04 100644 --- a/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs +++ b/tests/Jellyfin.Naming.Tests/TV/SeasonPathParserTests.cs @@ -6,32 +6,54 @@ 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) + [InlineData("/Drive/Season 1", "/Drive", 1, true)] + [InlineData("/Drive/Staffel 1", "/Drive", 1, true)] + [InlineData("/Drive/Stagione 1", "/Drive", 1, true)] + [InlineData("/Drive/sæson 1", "/Drive", 1, true)] + [InlineData("/Drive/Temporada 1", "/Drive", 1, true)] + [InlineData("/Drive/series 1", "/Drive", 1, true)] + [InlineData("/Drive/Kausi 1", "/Drive", 1, true)] + [InlineData("/Drive/Säsong 1", "/Drive", 1, true)] + [InlineData("/Drive/Seizoen 1", "/Drive", 1, true)] + [InlineData("/Drive/Seasong 1", "/Drive", 1, true)] + [InlineData("/Drive/Sezon 1", "/Drive", 1, true)] + [InlineData("/Drive/sezona 1", "/Drive", 1, true)] + [InlineData("/Drive/sezóna 1", "/Drive", 1, true)] + [InlineData("/Drive/Sezonul 1", "/Drive", 1, true)] + [InlineData("/Drive/시즌 1", "/Drive", 1, true)] + [InlineData("/Drive/シーズン 1", "/Drive", 1, true)] + [InlineData("/Drive/сезон 1", "/Drive", 1, true)] + [InlineData("/Drive/Сезон 1", "/Drive", 1, true)] + [InlineData("/Drive/Season 10", "/Drive", 10, true)] + [InlineData("/Drive/Season 100", "/Drive", 100, true)] + [InlineData("/Drive/s1", "/Drive", 1, true)] + [InlineData("/Drive/S1", "/Drive", 1, true)] + [InlineData("/Drive/Season 2", "/Drive", 2, true)] + [InlineData("/Drive/Season 02", "/Drive", 2, true)] + [InlineData("/Drive/Seinfeld/S02", "/Seinfeld", 2, true)] + [InlineData("/Drive/Seinfeld/2", "/Seinfeld", 2, true)] + [InlineData("/Drive/Seinfeld Season 2", "/Drive", null, false)] + [InlineData("/Drive/Season 2009", "/Drive", 2009, true)] + [InlineData("/Drive/Season1", "/Drive", 1, true)] + [InlineData("The Wonder Years/The.Wonder.Years.S04.PDTV.x264-JCH", "/The Wonder Years", 4, true)] + [InlineData("/Drive/Season 7 (2016)", "/Drive", 7, true)] + [InlineData("/Drive/Staffel 7 (2016)", "/Drive", 7, true)] + [InlineData("/Drive/Stagione 7 (2016)", "/Drive", 7, true)] + [InlineData("/Drive/Stargate SG-1/Season 1", "/Drive/Stargate SG-1", 1, true)] + [InlineData("/Drive/Stargate SG-1/Stargate SG-1 Season 1", "/Drive/Stargate SG-1", 1, true)] + [InlineData("/Drive/Season (8)", "/Drive", null, false)] + [InlineData("/Drive/3.Staffel", "/Drive", 3, true)] + [InlineData("/Drive/s06e05", "/Drive", null, false)] + [InlineData("/Drive/The.Legend.of.Condor.Heroes.2017.V2.web-dl.1080p.h264.aac-hdctv", "/Drive", null, false)] + [InlineData("/Drive/extras", "/Drive", 0, true)] + [InlineData("/Drive/specials", "/Drive", 0, true)] + [InlineData("/Drive/Episode 1 Season 2", "/Drive", null, false)] + public void GetSeasonNumberFromPathTest(string path, string? parentPath, int? seasonNumber, bool isSeasonDirectory) { - var result = SeasonPathParser.Parse(path, true, true); + var result = SeasonPathParser.Parse(path, parentPath, true, true); Assert.Equal(result.SeasonNumber is not null, result.Success); - Assert.Equal(result.SeasonNumber, seasonNumber); + Assert.Equal(seasonNumber, result.SeasonNumber); Assert.Equal(isSeasonDirectory, result.IsSeasonFolder); } } From ea6130b354d263777b303ff9d79cab9aa4f22fb7 Mon Sep 17 00:00:00 2001 From: Shadowghost Date: Sun, 23 Mar 2025 21:55:26 +0100 Subject: [PATCH 55/56] Add missing singleton --- Emby.Server.Implementations/ApplicationHost.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Emby.Server.Implementations/ApplicationHost.cs b/Emby.Server.Implementations/ApplicationHost.cs index 29967c6df5..4fe1d2b17e 100644 --- a/Emby.Server.Implementations/ApplicationHost.cs +++ b/Emby.Server.Implementations/ApplicationHost.cs @@ -57,6 +57,7 @@ using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Drawing; using MediaBrowser.Controller.Dto; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.LiveTv; using MediaBrowser.Controller.Lyrics; @@ -508,6 +509,7 @@ namespace Emby.Server.Implementations serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); // TODO: Refactor to eliminate the circular dependencies here so that Lazy isn't required serviceCollection.AddTransient(provider => new Lazy(provider.GetRequiredService)); From 671d801d9f734665d0acbd441246712ad2e3d91f Mon Sep 17 00:00:00 2001 From: JPVenson Date: Mon, 24 Mar 2025 02:52:34 +0100 Subject: [PATCH 56/56] #13540 Fixed (#13757) #13508 Partially fixed Co-authored-by: JPVenson --- .../Item/BaseItemRepository.cs | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs index 630a169cba..bea69b2820 100644 --- a/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs +++ b/Jellyfin.Server.Implementations/Item/BaseItemRepository.cs @@ -963,25 +963,11 @@ public sealed class BaseItemRepository using var context = _dbProvider.CreateDbContext(); - var innerQuery = new InternalItemsQuery(filter.User) - { - ExcludeItemTypes = filter.ExcludeItemTypes, - IncludeItemTypes = filter.IncludeItemTypes, - MediaTypes = filter.MediaTypes, - AncestorIds = filter.AncestorIds, - ItemIds = filter.ItemIds, - TopParentIds = filter.TopParentIds, - ParentId = filter.ParentId, - IsAiring = filter.IsAiring, - IsMovie = filter.IsMovie, - IsSports = filter.IsSports, - IsKids = filter.IsKids, - IsNews = filter.IsNews, - IsSeries = filter.IsSeries - }; - var query = TranslateQuery(context.BaseItems.AsNoTracking(), context, innerQuery); + var query = TranslateQuery(context.BaseItems.AsNoTracking(), context, filter); - query = query.Where(e => e.Type == returnType && e.ItemValues!.Any(f => e.CleanName == f.ItemValue.CleanValue && itemValueTypes.Any(w => (ItemValueType)w == f.ItemValue.Type))); + query = query.Where(e => e.Type == returnType); + // this does not seem to be nesseary but it does not make any sense why this isn't working. + // && e.ItemValues!.Any(f => e.CleanName == f.ItemValue.CleanValue && itemValueTypes.Any(w => (ItemValueType)w == f.ItemValue.Type))); if (filter.OrderBy.Count != 0 || !string.IsNullOrEmpty(filter.SearchTerm))