2
0
Эх сурвалжийг харах

Merge pull request #9560 from IDisposable/sort-nfo-data

Sort embedded collections in Nfo files
Joshua M. Boniface 2 сар өмнө
parent
commit
f87150bb3d

+ 2 - 3
MediaBrowser.Controller/Entities/BaseItem.cs

@@ -919,7 +919,7 @@ namespace MediaBrowser.Controller.Entities
                 // Remove from middle if surrounded by spaces
                 // Remove from middle if surrounded by spaces
                 sortable = sortable.Replace(" " + search + " ", " ", StringComparison.Ordinal);
                 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))
                 if (sortable.EndsWith(" " + search, StringComparison.Ordinal))
                 {
                 {
                     sortable = sortable.Remove(sortable.Length - (search.Length + 1));
                     sortable = sortable.Remove(sortable.Length - (search.Length + 1));
@@ -1775,7 +1775,6 @@ namespace MediaBrowser.Controller.Entities
         public void AddStudio(string name)
         public void AddStudio(string name)
         {
         {
             ArgumentException.ThrowIfNullOrEmpty(name);
             ArgumentException.ThrowIfNullOrEmpty(name);
-
             var current = Studios;
             var current = Studios;
 
 
             if (!current.Contains(name, StringComparison.OrdinalIgnoreCase))
             if (!current.Contains(name, StringComparison.OrdinalIgnoreCase))
@@ -1794,7 +1793,7 @@ namespace MediaBrowser.Controller.Entities
 
 
         public void SetStudios(IEnumerable<string> names)
         public void SetStudios(IEnumerable<string> names)
         {
         {
-            Studios = names.Distinct().ToArray();
+            Studios = names.Trimmed().Distinct().ToArray();
         }
         }
 
 
         /// <summary>
         /// <summary>

+ 2 - 0
MediaBrowser.Controller/Entities/PeopleHelper.cs

@@ -15,6 +15,8 @@ namespace MediaBrowser.Controller.Entities
             ArgumentNullException.ThrowIfNull(person);
             ArgumentNullException.ThrowIfNull(person);
             ArgumentException.ThrowIfNullOrEmpty(person.Name);
             ArgumentException.ThrowIfNullOrEmpty(person.Name);
 
 
+            person.Name = person.Name.Trim();
+
             // Normalize
             // Normalize
             if (string.Equals(person.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
             if (string.Equals(person.Role, PersonType.GuestStar, StringComparison.OrdinalIgnoreCase))
             {
             {

+ 16 - 11
MediaBrowser.LocalMetadata/Parsers/BaseItemXmlParser.cs

@@ -234,8 +234,8 @@ namespace MediaBrowser.LocalMetadata.Parsers
                     item.CustomRating = reader.ReadNormalizedString();
                     item.CustomRating = reader.ReadNormalizedString();
                     break;
                     break;
                 case "RunningTime":
                 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))
                         if (int.TryParse(runtimeText.AsSpan().LeftPart(' '), NumberStyles.Integer, CultureInfo.InvariantCulture, out var runtime))
                         {
                         {
@@ -253,7 +253,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
 
 
                     break;
                     break;
                 case "LockData":
                 case "LockData":
-                    item.IsLocked = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
+                    item.IsLocked = string.Equals(reader.ReadNormalizedString(), "true", StringComparison.OrdinalIgnoreCase);
                     break;
                     break;
                 case "Network":
                 case "Network":
                     foreach (var name in reader.GetStringArray())
                     foreach (var name in reader.GetStringArray())
@@ -331,9 +331,9 @@ namespace MediaBrowser.LocalMetadata.Parsers
                 case "Rating":
                 case "Rating":
                 case "IMDBrating":
                 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
                         // 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))
                         if (float.TryParse(rating.Replace(',', '.'), NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out var val))
@@ -449,7 +449,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
 
 
                 case "OwnerUserId":
                 case "OwnerUserId":
                 {
                 {
-                    var val = reader.ReadElementContentAsString();
+                    var val = reader.ReadNormalizedString();
 
 
                     if (Guid.TryParse(val, out var guid) && !guid.Equals(Guid.Empty))
                     if (Guid.TryParse(val, out var guid) && !guid.Equals(Guid.Empty))
                     {
                     {
@@ -464,7 +464,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
 
 
                 case "Format3D":
                 case "Format3D":
                 {
                 {
-                    var val = reader.ReadElementContentAsString();
+                    var val = reader.ReadNormalizedString();
 
 
                     if (item is Video video)
                     if (item is Video video)
                     {
                     {
@@ -498,7 +498,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
                     string readerName = reader.Name;
                     string readerName = reader.Name;
                     if (_validProviderIds!.TryGetValue(readerName, out string? providerIdValue))
                     if (_validProviderIds!.TryGetValue(readerName, out string? providerIdValue))
                     {
                     {
-                        var id = reader.ReadElementContentAsString();
+                        var id = reader.ReadNormalizedString();
                         item.TrySetProviderId(providerIdValue, id);
                         item.TrySetProviderId(providerIdValue, id);
                     }
                     }
                     else
                     else
@@ -580,7 +580,12 @@ namespace MediaBrowser.LocalMetadata.Parsers
                     switch (reader.Name)
                     switch (reader.Name)
                     {
                     {
                         case "Tagline":
                         case "Tagline":
-                            item.Tagline = reader.ReadNormalizedString();
+                            var val = reader.ReadNormalizedString();
+                            if (!string.IsNullOrEmpty(val))
+                            {
+                                item.Tagline = val;
+                            }
+
                             break;
                             break;
                         default:
                         default:
                             reader.Skip();
                             reader.Skip();
@@ -842,7 +847,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
                             userId = reader.ReadNormalizedString();
                             userId = reader.ReadNormalizedString();
                             break;
                             break;
                         case "CanEdit":
                         case "CanEdit":
-                            canEdit = string.Equals(reader.ReadElementContentAsString(), "true", StringComparison.OrdinalIgnoreCase);
+                            canEdit = string.Equals(reader.ReadNormalizedString(), "true", StringComparison.OrdinalIgnoreCase);
                             break;
                             break;
                         default:
                         default:
                             reader.Skip();
                             reader.Skip();
@@ -856,7 +861,7 @@ namespace MediaBrowser.LocalMetadata.Parsers
             }
             }
 
 
             // This is valid
             // 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);
                 return new PlaylistUserPermissions(guid, canEdit);
             }
             }

+ 18 - 15
MediaBrowser.MediaEncoding/Probing/ProbeResultNormalizer.cs

@@ -10,6 +10,7 @@ using System.Text.RegularExpressions;
 using System.Xml;
 using System.Xml;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
 using Jellyfin.Extensions;
 using Jellyfin.Extensions;
+using MediaBrowser.Controller.Extensions;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Dto;
 using MediaBrowser.Model.Entities;
 using MediaBrowser.Model.Entities;
@@ -531,42 +532,44 @@ namespace MediaBrowser.MediaEncoding.Probing
         private void ProcessPairs(string key, List<NameValuePair> pairs, MediaInfo info)
         private void ProcessPairs(string key, List<NameValuePair> pairs, MediaInfo info)
         {
         {
             List<BaseItemPerson> peoples = new List<BaseItemPerson>();
             List<BaseItemPerson> peoples = new List<BaseItemPerson>();
+            var distinctPairs = pairs.Select(p => p.Value)
+                    .Where(i => !string.IsNullOrWhiteSpace(i))
+                    .Trimmed()
+                    .Distinct(StringComparer.OrdinalIgnoreCase);
+
             if (string.Equals(key, "studio", StringComparison.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))
             else if (string.Equals(key, "screenwriters", StringComparison.OrdinalIgnoreCase))
             {
             {
-                foreach (var pair in pairs)
+                foreach (var pair in distinctPairs)
                 {
                 {
                     peoples.Add(new BaseItemPerson
                     peoples.Add(new BaseItemPerson
                     {
                     {
-                        Name = pair.Value,
+                        Name = pair,
                         Type = PersonKind.Writer
                         Type = PersonKind.Writer
                     });
                     });
                 }
                 }
             }
             }
             else if (string.Equals(key, "producers", StringComparison.OrdinalIgnoreCase))
             else if (string.Equals(key, "producers", StringComparison.OrdinalIgnoreCase))
             {
             {
-                foreach (var pair in pairs)
+                foreach (var pair in distinctPairs)
                 {
                 {
                     peoples.Add(new BaseItemPerson
                     peoples.Add(new BaseItemPerson
                     {
                     {
-                        Name = pair.Value,
+                        Name = pair,
                         Type = PersonKind.Producer
                         Type = PersonKind.Producer
                     });
                     });
                 }
                 }
             }
             }
             else if (string.Equals(key, "directors", StringComparison.OrdinalIgnoreCase))
             else if (string.Equals(key, "directors", StringComparison.OrdinalIgnoreCase))
             {
             {
-                foreach (var pair in pairs)
+                foreach (var pair in distinctPairs)
                 {
                 {
                     peoples.Add(new BaseItemPerson
                     peoples.Add(new BaseItemPerson
                     {
                     {
-                        Name = pair.Value,
+                        Name = pair,
                         Type = PersonKind.Director
                         Type = PersonKind.Director
                     });
                     });
                 }
                 }
@@ -591,10 +594,10 @@ namespace MediaBrowser.MediaEncoding.Probing
                     switch (reader.Name)
                     switch (reader.Name)
                     {
                     {
                         case "key":
                         case "key":
-                            name = reader.ReadElementContentAsString();
+                            name = reader.ReadNormalizedString();
                             break;
                             break;
                         case "string":
                         case "string":
-                            value = reader.ReadElementContentAsString();
+                            value = reader.ReadNormalizedString();
                             break;
                             break;
                         default:
                         default:
                             reader.Skip();
                             reader.Skip();
@@ -607,8 +610,8 @@ namespace MediaBrowser.MediaEncoding.Probing
                 }
                 }
             }
             }
 
 
-            if (string.IsNullOrWhiteSpace(name)
-                || string.IsNullOrWhiteSpace(value))
+            if (string.IsNullOrEmpty(name)
+                || string.IsNullOrEmpty(value))
             {
             {
                 return null;
                 return null;
             }
             }
@@ -1453,7 +1456,7 @@ namespace MediaBrowser.MediaEncoding.Probing
             var genres = new List<string>(info.Genres);
             var genres = new List<string>(info.Genres);
             foreach (var genre in Split(genreVal, true))
             foreach (var genre in Split(genreVal, true))
             {
             {
-                if (string.IsNullOrWhiteSpace(genre))
+                if (string.IsNullOrEmpty(genre))
                 {
                 {
                     continue;
                     continue;
                 }
                 }

+ 11 - 8
MediaBrowser.Providers/MediaInfo/AudioFileProber.cs

@@ -6,6 +6,7 @@ using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
 using ATL;
 using ATL;
 using Jellyfin.Data.Enums;
 using Jellyfin.Data.Enums;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Library;
 using MediaBrowser.Controller.Library;
@@ -179,8 +180,8 @@ namespace MediaBrowser.Providers.MediaInfo
             // That setter is meant for its own tag parser and external editor usage and will have unwanted side effects
             // 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.
             // 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.
             // 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 trackTitle = (string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title).Trim();
+            var trackAlbum = (string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album).Trim();
             var trackYear = track.Year is null or 0 ? mediaInfo.ProductionYear : track.Year;
             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 trackTrackNumber = track.TrackNumber is null or 0 ? mediaInfo.IndexNumber : track.TrackNumber;
             var trackDiscNumber = track.DiscNumber is null or 0 ? mediaInfo.ParentIndexNumber : track.DiscNumber;
             var trackDiscNumber = track.DiscNumber is null or 0 ? mediaInfo.ParentIndexNumber : track.DiscNumber;
@@ -197,11 +198,11 @@ namespace MediaBrowser.Providers.MediaInfo
 
 
                 foreach (var albumArtist in albumArtists)
                 foreach (var albumArtist in albumArtists)
                 {
                 {
-                    if (!string.IsNullOrEmpty(albumArtist))
+                    if (!string.IsNullOrWhiteSpace(albumArtist))
                     {
                     {
                         PeopleHelper.AddPerson(people, new PersonInfo
                         PeopleHelper.AddPerson(people, new PersonInfo
                         {
                         {
-                            Name = albumArtist,
+                            Name = albumArtist.Trim(),
                             Type = PersonKind.AlbumArtist
                             Type = PersonKind.AlbumArtist
                         });
                         });
                     }
                     }
@@ -229,11 +230,11 @@ namespace MediaBrowser.Providers.MediaInfo
 
 
                 foreach (var performer in performers)
                 foreach (var performer in performers)
                 {
                 {
-                    if (!string.IsNullOrEmpty(performer))
+                    if (!string.IsNullOrWhiteSpace(performer))
                     {
                     {
                         PeopleHelper.AddPerson(people, new PersonInfo
                         PeopleHelper.AddPerson(people, new PersonInfo
                         {
                         {
-                            Name = performer,
+                            Name = performer.Trim(),
                             Type = PersonKind.Artist
                             Type = PersonKind.Artist
                         });
                         });
                     }
                     }
@@ -241,11 +242,11 @@ namespace MediaBrowser.Providers.MediaInfo
 
 
                 foreach (var composer in track.Composer.Split(InternalValueSeparator))
                 foreach (var composer in track.Composer.Split(InternalValueSeparator))
                 {
                 {
-                    if (!string.IsNullOrEmpty(composer))
+                    if (!string.IsNullOrWhiteSpace(composer))
                     {
                     {
                         PeopleHelper.AddPerson(people, new PersonInfo
                         PeopleHelper.AddPerson(people, new PersonInfo
                         {
                         {
-                            Name = composer,
+                            Name = composer.Trim(),
                             Type = PersonKind.Composer
                             Type = PersonKind.Composer
                         });
                         });
                     }
                     }
@@ -331,6 +332,8 @@ namespace MediaBrowser.Providers.MediaInfo
                     genres = genres.SelectMany(g => SplitWithCustomDelimiter(g, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist)).ToArray();
                     genres = genres.SelectMany(g => SplitWithCustomDelimiter(g, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist)).ToArray();
                 }
                 }
 
 
+                genres = genres.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
+
                 audio.Genres = options.ReplaceAllMetadata || audio.Genres is null || audio.Genres.Length == 0
                 audio.Genres = options.ReplaceAllMetadata || audio.Genres is null || audio.Genres.Length == 0
                     ? genres
                     ? genres
                     : audio.Genres;
                     : audio.Genres;

+ 4 - 3
MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs

@@ -6,6 +6,7 @@ using System.Globalization;
 using System.Linq;
 using System.Linq;
 using System.Threading;
 using System.Threading;
 using System.Threading.Tasks;
 using System.Threading.Tasks;
+using Jellyfin.Extensions;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Common.Configuration;
 using MediaBrowser.Controller.Chapters;
 using MediaBrowser.Controller.Chapters;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
@@ -407,7 +408,7 @@ namespace MediaBrowser.Providers.MediaInfo
                 {
                 {
                     video.Genres = Array.Empty<string>();
                     video.Genres = Array.Empty<string>();
 
 
-                    foreach (var genre in data.Genres)
+                    foreach (var genre in data.Genres.Trimmed())
                     {
                     {
                         video.AddGenre(genre);
                         video.AddGenre(genre);
                     }
                     }
@@ -516,9 +517,9 @@ namespace MediaBrowser.Providers.MediaInfo
                 {
                 {
                     PeopleHelper.AddPerson(people, new PersonInfo
                     PeopleHelper.AddPerson(people, new PersonInfo
                     {
                     {
-                        Name = person.Name,
+                        Name = person.Name.Trim(),
                         Type = person.Type,
                         Type = person.Type,
-                        Role = person.Role
+                        Role = person.Role.Trim()
                     });
                     });
                 }
                 }
 
 

+ 2 - 2
MediaBrowser.Providers/Music/AlbumMetadataService.cs

@@ -187,7 +187,7 @@ namespace MediaBrowser.Providers.Music
                 {
                 {
                     PeopleHelper.AddPerson(people, new PersonInfo
                     PeopleHelper.AddPerson(people, new PersonInfo
                     {
                     {
-                        Name = albumArtist,
+                        Name = albumArtist.Trim(),
                         Type = PersonKind.AlbumArtist
                         Type = PersonKind.AlbumArtist
                     });
                     });
                 }
                 }
@@ -196,7 +196,7 @@ namespace MediaBrowser.Providers.Music
                 {
                 {
                     PeopleHelper.AddPerson(people, new PersonInfo
                     PeopleHelper.AddPerson(people, new PersonInfo
                     {
                     {
-                        Name = artist,
+                        Name = artist.Trim(),
                         Type = PersonKind.Artist
                         Type = PersonKind.Artist
                     });
                     });
                 }
                 }

+ 2 - 2
MediaBrowser.Providers/Plugins/Omdb/OmdbProvider.cs

@@ -421,7 +421,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
             {
             {
                 var person = new PersonInfo
                 var person = new PersonInfo
                 {
                 {
-                    Name = result.Director,
+                    Name = result.Director.Trim(),
                     Type = PersonKind.Director
                     Type = PersonKind.Director
                 };
                 };
 
 
@@ -432,7 +432,7 @@ namespace MediaBrowser.Providers.Plugins.Omdb
             {
             {
                 var person = new PersonInfo
                 var person = new PersonInfo
                 {
                 {
-                    Name = result.Writer,
+                    Name = result.Writer.Trim(),
                     Type = PersonKind.Writer
                     Type = PersonKind.Writer
                 };
                 };
 
 

+ 3 - 3
MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs

@@ -234,7 +234,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
 
 
             var genres = movieResult.Genres;
             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);
                 movie.AddGenre(genre);
             }
             }
@@ -254,7 +254,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
                     var personInfo = new PersonInfo
                     var personInfo = new PersonInfo
                     {
                     {
                         Name = actor.Name.Trim(),
                         Name = actor.Name.Trim(),
-                        Role = actor.Character,
+                        Role = actor.Character.Trim(),
                         Type = PersonKind.Actor,
                         Type = PersonKind.Actor,
                         SortOrder = actor.Order
                         SortOrder = actor.Order
                     };
                     };
@@ -289,7 +289,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies
                     var personInfo = new PersonInfo
                     var personInfo = new PersonInfo
                     {
                     {
                         Name = person.Name.Trim(),
                         Name = person.Name.Trim(),
-                        Role = person.Job,
+                        Role = person.Job?.Trim(),
                         Type = type
                         Type = type
                     };
                     };
 
 

+ 3 - 3
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbEpisodeProvider.cs

@@ -211,7 +211,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
                     metadataResult.AddPerson(new PersonInfo
                     metadataResult.AddPerson(new PersonInfo
                     {
                     {
                         Name = actor.Name.Trim(),
                         Name = actor.Name.Trim(),
-                        Role = actor.Character,
+                        Role = actor.Character.Trim(),
                         Type = PersonKind.Actor,
                         Type = PersonKind.Actor,
                         SortOrder = actor.Order
                         SortOrder = actor.Order
                     });
                     });
@@ -225,7 +225,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
                     metadataResult.AddPerson(new PersonInfo
                     metadataResult.AddPerson(new PersonInfo
                     {
                     {
                         Name = guest.Name.Trim(),
                         Name = guest.Name.Trim(),
-                        Role = guest.Character,
+                        Role = guest.Character.Trim(),
                         Type = PersonKind.GuestStar,
                         Type = PersonKind.GuestStar,
                         SortOrder = guest.Order
                         SortOrder = guest.Order
                     });
                     });
@@ -249,7 +249,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
                     metadataResult.AddPerson(new PersonInfo
                     metadataResult.AddPerson(new PersonInfo
                     {
                     {
                         Name = person.Name.Trim(),
                         Name = person.Name.Trim(),
-                        Role = person.Job,
+                        Role = person.Job?.Trim(),
                         Type = type
                         Type = type
                     });
                     });
                 }
                 }

+ 5 - 4
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();
                 var cast = credits.Cast.OrderBy(c => c.Order).Take(Plugin.Instance.Configuration.MaxCastMembers).ToList();
                 for (var i = 0; i < cast.Count; i++)
                 for (var i = 0; i < cast.Count; i++)
                 {
                 {
+                    var member = cast[i];
                     result.AddPerson(new PersonInfo
                     result.AddPerson(new PersonInfo
                     {
                     {
-                        Name = cast[i].Name.Trim(),
-                        Role = cast[i].Character,
+                        Name = member.Name.Trim(),
+                        Role = member.Character.Trim(),
                         Type = PersonKind.Actor,
                         Type = PersonKind.Actor,
-                        SortOrder = cast[i].Order
+                        SortOrder = member.Order
                     });
                     });
                 }
                 }
             }
             }
@@ -108,7 +109,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
                     result.AddPerson(new PersonInfo
                     result.AddPerson(new PersonInfo
                     {
                     {
                         Name = person.Name.Trim(),
                         Name = person.Name.Trim(),
-                        Role = person.Job,
+                        Role = person.Job?.Trim(),
                         Type = type
                         Type = type
                     });
                     });
                 }
                 }

+ 2 - 2
MediaBrowser.Providers/Plugins/Tmdb/TV/TmdbSeriesProvider.cs

@@ -330,7 +330,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
                     var personInfo = new PersonInfo
                     var personInfo = new PersonInfo
                     {
                     {
                         Name = actor.Name.Trim(),
                         Name = actor.Name.Trim(),
-                        Role = actor.Character,
+                        Role = actor.Character.Trim(),
                         Type = PersonKind.Actor,
                         Type = PersonKind.Actor,
                         SortOrder = actor.Order,
                         SortOrder = actor.Order,
                         ImageUrl = _tmdbClientManager.GetPosterUrl(actor.ProfilePath)
                         ImageUrl = _tmdbClientManager.GetPosterUrl(actor.ProfilePath)
@@ -368,7 +368,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.TV
                     yield return new PersonInfo
                     yield return new PersonInfo
                     {
                     {
                         Name = person.Name.Trim(),
                         Name = person.Name.Trim(),
-                        Role = person.Job,
+                        Role = person.Job?.Trim(),
                         Type = type
                         Type = type
                     };
                     };
                 }
                 }

+ 14 - 4
MediaBrowser.XbmcMetadata/Savers/AlbumNfoSaver.cs

@@ -4,6 +4,7 @@ using System.Globalization;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Xml;
 using System.Xml;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Audio;
 using MediaBrowser.Controller.Entities.Audio;
@@ -55,12 +56,12 @@ namespace MediaBrowser.XbmcMetadata.Savers
         {
         {
             var album = (MusicAlbum)item;
             var album = (MusicAlbum)item;
 
 
-            foreach (var artist in album.Artists)
+            foreach (var artist in album.Artists.Trimmed().OrderBy(artist => artist))
             {
             {
                 writer.WriteElementString("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);
                 writer.WriteElementString("albumartist", artist);
             }
             }
@@ -70,11 +71,20 @@ namespace MediaBrowser.XbmcMetadata.Savers
 
 
         private void AddTracks(IEnumerable<BaseItem> tracks, XmlWriter writer)
         private void AddTracks(IEnumerable<BaseItem> 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 => SortNameOrName(i))
+                .ThenBy(i => i.Name?.Trim()))
             {
             {
                 writer.WriteStartElement("track");
                 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));
                     writer.WriteElementString("position", track.IndexNumber.Value.ToString(CultureInfo.InvariantCulture));
                 }
                 }

+ 5 - 1
MediaBrowser.XbmcMetadata/Savers/ArtistNfoSaver.cs

@@ -1,6 +1,7 @@
 using System.Collections.Generic;
 using System.Collections.Generic;
 using System.Globalization;
 using System.Globalization;
 using System.IO;
 using System.IO;
+using System.Linq;
 using System.Xml;
 using System.Xml;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
@@ -69,7 +70,10 @@ namespace MediaBrowser.XbmcMetadata.Savers
 
 
         private void AddAlbums(IReadOnlyList<BaseItem> albums, XmlWriter writer)
         private void AddAlbums(IReadOnlyList<BaseItem> albums, XmlWriter writer)
         {
         {
-            foreach (var album in albums)
+            foreach (var album in albums
+                .OrderBy(album => album.ProductionYear ?? 0)
+                .ThenBy(album => SortNameOrName(album))
+                .ThenBy(album => album.Name?.Trim()))
             {
             {
                 writer.WriteStartElement("album");
                 writer.WriteStartElement("album");
 
 

+ 41 - 12
MediaBrowser.XbmcMetadata/Savers/BaseNfoSaver.cs

@@ -488,7 +488,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
 
 
             var directors = people
             var directors = people
                 .Where(i => i.IsType(PersonKind.Director))
                 .Where(i => i.IsType(PersonKind.Director))
-                .Select(i => i.Name)
+                .Select(i => i.Name?.Trim())
+                .Distinct(StringComparer.OrdinalIgnoreCase)
+                .OrderBy(i => i)
                 .ToList();
                 .ToList();
 
 
             foreach (var person in directors)
             foreach (var person in directors)
@@ -498,8 +500,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
 
 
             var writers = people
             var writers = people
                 .Where(i => i.IsType(PersonKind.Writer))
                 .Where(i => i.IsType(PersonKind.Writer))
-                .Select(i => i.Name)
+                .Select(i => i.Name?.Trim())
                 .Distinct(StringComparer.OrdinalIgnoreCase)
                 .Distinct(StringComparer.OrdinalIgnoreCase)
+                .OrderBy(i => i)
                 .ToList();
                 .ToList();
 
 
             foreach (var person in writers)
             foreach (var person in writers)
@@ -512,7 +515,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
                 writer.WriteElementString("credits", person);
                 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));
                 writer.WriteElementString("trailer", GetOutputTrailerUrl(trailer.Url));
             }
             }
@@ -660,22 +663,22 @@ namespace MediaBrowser.XbmcMetadata.Savers
                 writer.WriteElementString("tagline", item.Tagline);
                 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);
                 writer.WriteElementString("country", country);
             }
             }
 
 
-            foreach (var genre in item.Genres)
+            foreach (var genre in item.Genres.Trimmed().OrderBy(genre => genre))
             {
             {
                 writer.WriteElementString("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);
                 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)
                 if (item is MusicAlbum || item is MusicArtist)
                 {
                 {
@@ -752,7 +755,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
 
 
             if (item.ProviderIds is not null)
             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];
                     var providerId = item.ProviderIds[providerKey];
                     if (!string.IsNullOrEmpty(providerId) && !writtenProviderIds.Contains(providerKey))
                     if (!string.IsNullOrEmpty(providerId) && !writtenProviderIds.Contains(providerKey))
@@ -764,7 +767,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
                             XmlConvert.VerifyName(tagName);
                             XmlConvert.VerifyName(tagName);
                             Logger.LogDebug("Saving custom provider tagname {0}", tagName);
                             Logger.LogDebug("Saving custom provider tagname {0}", tagName);
 
 
-                            writer.WriteElementString(GetTagForProviderKey(providerKey), providerId);
+                            writer.WriteElementString(tagName, providerId);
                         }
                         }
                         catch (ArgumentException)
                         catch (ArgumentException)
                         {
                         {
@@ -785,7 +788,10 @@ namespace MediaBrowser.XbmcMetadata.Savers
 
 
             AddUserData(item, writer, userManager, userDataRepo, options);
             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)
             if (item is BoxSet folder)
             {
             {
@@ -797,6 +803,8 @@ namespace MediaBrowser.XbmcMetadata.Savers
         {
         {
             var items = item.LinkedChildren
             var items = item.LinkedChildren
                 .Where(i => i.Type == LinkedChildType.Manual)
                 .Where(i => i.Type == LinkedChildType.Manual)
+                .OrderBy(i => i.Path?.Trim())
+                .ThenBy(i => i.LibraryItemId?.Trim())
                 .ToList();
                 .ToList();
 
 
             foreach (var link in items)
             foreach (var link in items)
@@ -839,7 +847,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
                 writer.WriteElementString("poster", GetImagePathToSave(image, libraryManager));
                 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));
                 writer.WriteElementString("fanart", GetImagePathToSave(backdrop, libraryManager));
             }
             }
@@ -916,7 +924,9 @@ namespace MediaBrowser.XbmcMetadata.Savers
 
 
         private void AddActors(IReadOnlyList<PersonInfo> people, XmlWriter writer, ILibraryManager libraryManager, bool saveImagePath)
         private void AddActors(IReadOnlyList<PersonInfo> 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))
                 if (person.IsType(PersonKind.Director) || person.IsType(PersonKind.Writer))
                 {
                 {
@@ -1027,5 +1037,24 @@ namespace MediaBrowser.XbmcMetadata.Savers
 
 
         private string GetTagForProviderKey(string providerKey)
         private string GetTagForProviderKey(string providerKey)
             => providerKey.ToLowerInvariant() + "id";
             => 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();
+        }
     }
     }
 }
 }

+ 2 - 1
MediaBrowser.XbmcMetadata/Savers/MovieNfoSaver.cs

@@ -2,6 +2,7 @@ using System.Collections.Generic;
 using System.IO;
 using System.IO;
 using System.Linq;
 using System.Linq;
 using System.Xml;
 using System.Xml;
+using Jellyfin.Extensions;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Configuration;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities;
 using MediaBrowser.Controller.Entities.Movies;
 using MediaBrowser.Controller.Entities.Movies;
@@ -100,7 +101,7 @@ namespace MediaBrowser.XbmcMetadata.Savers
 
 
             if (item is MusicVideo musicVideo)
             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);
                     writer.WriteElementString("artist", artist);
                 }
                 }

+ 12 - 0
src/Jellyfin.Extensions/StringExtensions.cs

@@ -1,4 +1,6 @@
 using System;
 using System;
+using System.Collections.Generic;
+using System.Linq;
 using System.Text.RegularExpressions;
 using System.Text.RegularExpressions;
 using ICU4N.Text;
 using ICU4N.Text;
 
 
@@ -123,5 +125,15 @@ namespace Jellyfin.Extensions
         {
         {
             return (_transliterator.Value is null) ? text : _transliterator.Value.Transliterate(text);
             return (_transliterator.Value is null) ? text : _transliterator.Value.Transliterate(text);
         }
         }
+
+        /// <summary>
+        /// Ensures all strings are non-null and trimmed of leading an trailing blanks.
+        /// </summary>
+        /// <param name="values">The enumerable of strings to trim.</param>
+        /// <returns>The enumeration of trimmed strings.</returns>
+        public static IEnumerable<string> Trimmed(this IEnumerable<string> values)
+        {
+            return values.Select(i => (i ?? string.Empty).Trim());
+        }
     }
     }
 }
 }