From 65386a70cca08ed922c575a73e076efea10625ff Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 23 Mar 2025 19:55:36 -0700 Subject: [PATCH] Add XML declaration and clean up Kodi metadata generation (cherry picked from commit b103005aa23baffcf95ade6a2fa3b9923cddc167) --- src/NzbDrone.Common/Utf8StringWriter.cs | 9 + .../Metadata/Consumers/Xbmc/XbmcMetadata.cs | 417 +++++++++--------- 2 files changed, 220 insertions(+), 206 deletions(-) create mode 100644 src/NzbDrone.Common/Utf8StringWriter.cs diff --git a/src/NzbDrone.Common/Utf8StringWriter.cs b/src/NzbDrone.Common/Utf8StringWriter.cs new file mode 100644 index 000000000..f98bb8f2e --- /dev/null +++ b/src/NzbDrone.Common/Utf8StringWriter.cs @@ -0,0 +1,9 @@ +using System.IO; +using System.Text; + +namespace NzbDrone.Common; + +public class Utf8StringWriter : StringWriter +{ + public override Encoding Encoding => Encoding.UTF8; +} diff --git a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs index bfe2a4ec9..62e39ac0b 100644 --- a/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs +++ b/src/NzbDrone.Core/Extras/Metadata/Consumers/Xbmc/XbmcMetadata.cs @@ -7,6 +7,7 @@ using System.Text.RegularExpressions; using System.Xml; using System.Xml.Linq; using NLog; +using NzbDrone.Common; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Core.Extras.Metadata.Files; @@ -56,7 +57,6 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc public override string GetFilenameAfterMove(Movie movie, MovieFile movieFile, MetadataFile metadataFile) { var movieFilePath = Path.Combine(movie.Path, movieFile.RelativePath); - var metadataPath = Path.Combine(movie.Path, metadataFile.RelativePath); if (metadataFile.Type == MetadataType.MovieMetadata) { @@ -118,11 +118,12 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc public override MetadataFileResult MovieMetadata(Movie movie, MovieFile movieFile) { var xmlResult = string.Empty; + if (Settings.MovieMetadata) { _logger.Debug("Generating Movie Metadata for: {0}", Path.Combine(movie.Path, movieFile.RelativePath)); - var movieMetadataLanguage = (Settings.MovieMetadataLanguage == (int)Language.Original) ? + var movieMetadataLanguage = Settings.MovieMetadataLanguage == (int)Language.Original ? (int)movie.MovieMetadata.Value.OriginalLanguage : Settings.MovieMetadataLanguage; @@ -134,295 +135,299 @@ namespace NzbDrone.Core.Extras.Metadata.Consumers.Xbmc var watched = GetExistingWatchedStatus(movie, movieFile.RelativePath); - var sb = new StringBuilder(); - var xws = new XmlWriterSettings(); - xws.OmitXmlDeclaration = true; - xws.Indent = false; + var thumbnail = movie.MovieMetadata.Value.Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); + var posters = movie.MovieMetadata.Value.Images.Where(i => i.CoverType == MediaCoverTypes.Poster).ToList(); + var fanarts = movie.MovieMetadata.Value.Images.Where(i => i.CoverType == MediaCoverTypes.Fanart).ToList(); - using (var xw = XmlWriter.Create(sb, xws)) - { - var doc = new XDocument(); - var thumbnail = movie.MovieMetadata.Value.Images.SingleOrDefault(i => i.CoverType == MediaCoverTypes.Screenshot); - var posters = movie.MovieMetadata.Value.Images.Where(i => i.CoverType == MediaCoverTypes.Poster); - var fanarts = movie.MovieMetadata.Value.Images.Where(i => i.CoverType == MediaCoverTypes.Fanart); + var details = new XElement("movie"); - var details = new XElement("movie"); + var metadataTitle = movieTranslation?.Title ?? movie.Title; - var metadataTitle = movieTranslation?.Title ?? movie.Title; + details.Add(new XElement("title", metadataTitle)); - details.Add(new XElement("title", metadataTitle)); + details.Add(new XElement("originaltitle", movie.MovieMetadata.Value.OriginalTitle)); - details.Add(new XElement("originaltitle", movie.MovieMetadata.Value.OriginalTitle)); + details.Add(new XElement("sorttitle", Parser.Parser.NormalizeTitle(metadataTitle))); + + if (movie.MovieMetadata.Value.Ratings?.Tmdb?.Votes > 0 || movie.MovieMetadata.Value.Ratings?.Imdb?.Votes > 0 || movie.MovieMetadata.Value.Ratings?.RottenTomatoes?.Value > 0) + { + var setRating = new XElement("ratings"); - details.Add(new XElement("sorttitle", Parser.Parser.NormalizeTitle(metadataTitle))); + var defaultRatingSet = false; - if (movie.MovieMetadata.Value.Ratings?.Tmdb?.Votes > 0 || movie.MovieMetadata.Value.Ratings?.Imdb?.Votes > 0 || movie.MovieMetadata.Value.Ratings?.RottenTomatoes?.Value > 0) + if (movie.MovieMetadata.Value.Ratings?.Imdb?.Votes > 0) { - var setRating = new XElement("ratings"); + var setRateImdb = new XElement("rating", new XAttribute("name", "imdb"), new XAttribute("max", "10"), new XAttribute("default", "true")); + setRateImdb.Add(new XElement("value", movie.MovieMetadata.Value.Ratings.Imdb.Value)); + setRateImdb.Add(new XElement("votes", movie.MovieMetadata.Value.Ratings.Imdb.Votes)); - var defaultRatingSet = false; + defaultRatingSet = true; + setRating.Add(setRateImdb); + } - if (movie.MovieMetadata.Value.Ratings?.Imdb?.Votes > 0) - { - var setRateImdb = new XElement("rating", new XAttribute("name", "imdb"), new XAttribute("max", "10"), new XAttribute("default", "true")); - setRateImdb.Add(new XElement("value", movie.MovieMetadata.Value.Ratings.Imdb.Value)); - setRateImdb.Add(new XElement("votes", movie.MovieMetadata.Value.Ratings.Imdb.Votes)); + if (movie.MovieMetadata.Value.Ratings?.Tmdb?.Votes > 0) + { + var setRateTheMovieDb = new XElement("rating", new XAttribute("name", "themoviedb"), new XAttribute("max", "10")); + setRateTheMovieDb.Add(new XElement("value", movie.MovieMetadata.Value.Ratings.Tmdb.Value)); + setRateTheMovieDb.Add(new XElement("votes", movie.MovieMetadata.Value.Ratings.Tmdb.Votes)); + if (!defaultRatingSet) + { defaultRatingSet = true; - setRating.Add(setRateImdb); + setRateTheMovieDb.SetAttributeValue("default", "true"); } - if (movie.MovieMetadata.Value.Ratings?.Tmdb?.Votes > 0) - { - var setRateTheMovieDb = new XElement("rating", new XAttribute("name", "themoviedb"), new XAttribute("max", "10")); - setRateTheMovieDb.Add(new XElement("value", movie.MovieMetadata.Value.Ratings.Tmdb.Value)); - setRateTheMovieDb.Add(new XElement("votes", movie.MovieMetadata.Value.Ratings.Tmdb.Votes)); - - if (!defaultRatingSet) - { - defaultRatingSet = true; - setRateTheMovieDb.SetAttributeValue("default", "true"); - } + setRating.Add(setRateTheMovieDb); + } - setRating.Add(setRateTheMovieDb); - } + if (movie.MovieMetadata.Value.Ratings?.RottenTomatoes?.Value > 0) + { + var setRateRottenTomatoes = new XElement("rating", new XAttribute("name", "tomatometerallcritics"), new XAttribute("max", "100")); + setRateRottenTomatoes.Add(new XElement("value", movie.MovieMetadata.Value.Ratings.RottenTomatoes.Value)); - if (movie.MovieMetadata.Value.Ratings?.RottenTomatoes?.Value > 0) + if (!defaultRatingSet) { - var setRateRottenTomatoes = new XElement("rating", new XAttribute("name", "tomatometerallcritics"), new XAttribute("max", "100")); - setRateRottenTomatoes.Add(new XElement("value", movie.MovieMetadata.Value.Ratings.RottenTomatoes.Value)); - - if (!defaultRatingSet) - { - setRateRottenTomatoes.SetAttributeValue("default", "true"); - } - - setRating.Add(setRateRottenTomatoes); + setRateRottenTomatoes.SetAttributeValue("default", "true"); } - details.Add(setRating); + setRating.Add(setRateRottenTomatoes); } - if (movie.MovieMetadata.Value.Ratings?.Tmdb?.Votes > 0) - { - details.Add(new XElement("rating", movie.MovieMetadata.Value.Ratings.Tmdb.Value)); - } + details.Add(setRating); + } - if (movie.MovieMetadata.Value.Ratings?.RottenTomatoes?.Value > 0) - { - details.Add(new XElement("criticrating", movie.MovieMetadata.Value.Ratings.RottenTomatoes.Value)); - } + if (movie.MovieMetadata.Value.Ratings?.Tmdb?.Votes > 0) + { + details.Add(new XElement("rating", movie.MovieMetadata.Value.Ratings.Tmdb.Value)); + } - details.Add(new XElement("userrating")); + if (movie.MovieMetadata.Value.Ratings?.RottenTomatoes?.Value > 0) + { + details.Add(new XElement("criticrating", movie.MovieMetadata.Value.Ratings.RottenTomatoes.Value)); + } + + details.Add(new XElement("userrating")); - details.Add(new XElement("top250")); + details.Add(new XElement("top250")); - details.Add(new XElement("outline")); + details.Add(new XElement("outline")); - details.Add(new XElement("plot", movieTranslation?.Overview ?? movie.MovieMetadata.Value.Overview)); + details.Add(new XElement("plot", movieTranslation?.Overview ?? movie.MovieMetadata.Value.Overview)); - details.Add(new XElement("tagline")); + details.Add(new XElement("tagline")); - details.Add(new XElement("runtime", movie.MovieMetadata.Value.Runtime)); + details.Add(new XElement("runtime", movie.MovieMetadata.Value.Runtime)); + + if (thumbnail != null) + { + details.Add(new XElement("thumb", thumbnail.RemoteUrl)); + } - if (thumbnail != null) + foreach (var poster in posters) + { + if (poster != null && poster.RemoteUrl != null) { - details.Add(new XElement("thumb", thumbnail.RemoteUrl)); + details.Add(new XElement("thumb", new XAttribute("aspect", "poster"), new XAttribute("preview", poster.RemoteUrl), poster.RemoteUrl)); } + } - foreach (var poster in posters) + if (fanarts.Any()) + { + var fanartElement = new XElement("fanart"); + + foreach (var fanart in fanarts) { - if (poster != null && poster.RemoteUrl != null) + if (fanart != null && fanart.RemoteUrl != null) { - details.Add(new XElement("thumb", new XAttribute("aspect", "poster"), new XAttribute("preview", poster.RemoteUrl), poster.RemoteUrl)); + fanartElement.Add(new XElement("thumb", new XAttribute("preview", fanart.RemoteUrl), fanart.RemoteUrl)); } } - if (fanarts.Any()) - { - var fanartElement = new XElement("fanart"); - foreach (var fanart in fanarts) - { - if (fanart != null && fanart.RemoteUrl != null) - { - fanartElement.Add(new XElement("thumb", new XAttribute("preview", fanart.RemoteUrl), fanart.RemoteUrl)); - } - } + details.Add(fanartElement); + } - details.Add(fanartElement); - } + if (movie.MovieMetadata.Value.Certification.IsNotNullOrWhiteSpace()) + { + details.Add(new XElement("mpaa", movie.MovieMetadata.Value.Certification)); + } - if (movie.MovieMetadata.Value.Certification.IsNotNullOrWhiteSpace()) - { - details.Add(new XElement("mpaa", movie.MovieMetadata.Value.Certification)); - } + details.Add(new XElement("playcount")); - details.Add(new XElement("playcount")); + details.Add(new XElement("lastplayed")); - details.Add(new XElement("lastplayed")); + details.Add(new XElement("id", movie.TmdbId)); - details.Add(new XElement("id", movie.TmdbId)); + var uniqueId = new XElement("uniqueid", movie.TmdbId); + uniqueId.SetAttributeValue("type", "tmdb"); + uniqueId.SetAttributeValue("default", true); + details.Add(uniqueId); - var uniqueId = new XElement("uniqueid", movie.TmdbId); - uniqueId.SetAttributeValue("type", "tmdb"); - uniqueId.SetAttributeValue("default", true); - details.Add(uniqueId); + if (movie.MovieMetadata.Value.ImdbId.IsNotNullOrWhiteSpace()) + { + var imdbId = new XElement("uniqueid", movie.MovieMetadata.Value.ImdbId); + imdbId.SetAttributeValue("type", "imdb"); + details.Add(imdbId); + } - if (movie.MovieMetadata.Value.ImdbId.IsNotNullOrWhiteSpace()) - { - var imdbId = new XElement("uniqueid", movie.MovieMetadata.Value.ImdbId); - imdbId.SetAttributeValue("type", "imdb"); - details.Add(imdbId); - } + foreach (var genre in movie.MovieMetadata.Value.Genres) + { + details.Add(new XElement("genre", genre)); + } - foreach (var genre in movie.MovieMetadata.Value.Genres) - { - details.Add(new XElement("genre", genre)); - } + details.Add(new XElement("country")); - details.Add(new XElement("country")); + if (Settings.AddCollectionName && movie.MovieMetadata.Value.CollectionTitle.IsNotNullOrWhiteSpace()) + { + var setElement = new XElement("set"); - if (Settings.AddCollectionName && movie.MovieMetadata.Value.CollectionTitle.IsNotNullOrWhiteSpace()) - { - var setElement = new XElement("set"); + setElement.Add(new XElement("name", movie.MovieMetadata.Value.CollectionTitle)); + setElement.Add(new XElement("overview")); - setElement.Add(new XElement("name", movie.MovieMetadata.Value.CollectionTitle)); - setElement.Add(new XElement("overview")); + details.Add(setElement); + } - details.Add(setElement); - } + if (movie.Tags.Any()) + { + var tags = _tagRepository.GetTags(movie.Tags); - if (movie.Tags.Any()) + foreach (var tag in tags) { - var tags = _tagRepository.GetTags(movie.Tags); - - foreach (var tag in tags) - { - details.Add(new XElement("tag", tag.Label)); - } + details.Add(new XElement("tag", tag.Label)); } + } - details.Add(new XElement("status", movie.MovieMetadata.Value.Status)); + details.Add(new XElement("status", movie.MovieMetadata.Value.Status)); - foreach (var credit in credits) + foreach (var credit in credits) + { + if (credit.Name != null && credit.Job == "Screenplay") { - if (credit.Name != null && credit.Job == "Screenplay") - { - details.Add(new XElement("credits", credit.Name)); - } + details.Add(new XElement("credits", credit.Name)); } + } - foreach (var credit in credits) + foreach (var credit in credits) + { + if (credit.Name != null && credit.Job == "Director") { - if (credit.Name != null && credit.Job == "Director") - { - details.Add(new XElement("director", credit.Name)); - } + details.Add(new XElement("director", credit.Name)); } + } - if (movie.MovieMetadata.Value.InCinemas.HasValue) - { - details.Add(new XElement("premiered", movie.MovieMetadata.Value.InCinemas.Value.ToString("yyyy-MM-dd"))); - } + if (movie.MovieMetadata.Value.InCinemas.HasValue) + { + details.Add(new XElement("premiered", movie.MovieMetadata.Value.InCinemas.Value.ToString("yyyy-MM-dd"))); + } - details.Add(new XElement("year", movie.Year)); + details.Add(new XElement("year", movie.Year)); - details.Add(new XElement("studio", movie.MovieMetadata.Value.Studio)); + details.Add(new XElement("studio", movie.MovieMetadata.Value.Studio)); - details.Add(new XElement("trailer", "plugin://plugin.video.youtube/play/?video_id=" + movie.MovieMetadata.Value.YouTubeTrailerId)); + details.Add(new XElement("trailer", "plugin://plugin.video.youtube/play/?video_id=" + movie.MovieMetadata.Value.YouTubeTrailerId)); - details.Add(new XElement("watched", watched)); + details.Add(new XElement("watched", watched)); - if (movieFile.MediaInfo != null) - { - var sceneName = movieFile.GetSceneOrFileName(); + if (movieFile.MediaInfo != null) + { + var sceneName = movieFile.GetSceneOrFileName(); - var fileInfo = new XElement("fileinfo"); - var streamDetails = new XElement("streamdetails"); + var fileInfo = new XElement("fileinfo"); + var streamDetails = new XElement("streamdetails"); - var video = new XElement("video"); - video.Add(new XElement("aspect", (float)movieFile.MediaInfo.Width / (float)movieFile.MediaInfo.Height)); - video.Add(new XElement("bitrate", movieFile.MediaInfo.VideoBitrate)); - video.Add(new XElement("codec", MediaInfoFormatter.FormatVideoCodec(movieFile.MediaInfo, sceneName))); - video.Add(new XElement("framerate", movieFile.MediaInfo.VideoFps)); - video.Add(new XElement("height", movieFile.MediaInfo.Height)); - video.Add(new XElement("scantype", movieFile.MediaInfo.ScanType)); - video.Add(new XElement("width", movieFile.MediaInfo.Width)); + var video = new XElement("video"); + video.Add(new XElement("aspect", (float)movieFile.MediaInfo.Width / (float)movieFile.MediaInfo.Height)); + video.Add(new XElement("bitrate", movieFile.MediaInfo.VideoBitrate)); + video.Add(new XElement("codec", MediaInfoFormatter.FormatVideoCodec(movieFile.MediaInfo, sceneName))); + video.Add(new XElement("framerate", movieFile.MediaInfo.VideoFps)); + video.Add(new XElement("height", movieFile.MediaInfo.Height)); + video.Add(new XElement("scantype", movieFile.MediaInfo.ScanType)); + video.Add(new XElement("width", movieFile.MediaInfo.Width)); - if (movieFile.MediaInfo.RunTime != default) - { - video.Add(new XElement("duration", movieFile.MediaInfo.RunTime.TotalMinutes)); - video.Add(new XElement("durationinseconds", Math.Round(movieFile.MediaInfo.RunTime.TotalSeconds))); - } + if (movieFile.MediaInfo.RunTime != TimeSpan.Zero) + { + video.Add(new XElement("duration", movieFile.MediaInfo.RunTime.TotalMinutes)); + video.Add(new XElement("durationinseconds", Math.Round(movieFile.MediaInfo.RunTime.TotalSeconds))); + } - if (movieFile.MediaInfo.VideoHdrFormat is HdrFormat.DolbyVision or HdrFormat.DolbyVisionHdr10 or HdrFormat.DolbyVisionHdr10Plus or HdrFormat.DolbyVisionHlg or HdrFormat.DolbyVisionSdr) - { - video.Add(new XElement("hdrtype", "dolbyvision")); - } - else if (movieFile.MediaInfo.VideoHdrFormat is HdrFormat.Hdr10 or HdrFormat.Hdr10Plus or HdrFormat.Pq10) - { - video.Add(new XElement("hdrtype", "hdr10")); - } - else if (movieFile.MediaInfo.VideoHdrFormat == HdrFormat.Hlg10) - { - video.Add(new XElement("hdrtype", "hlg")); - } - else if (movieFile.MediaInfo.VideoHdrFormat == HdrFormat.None) - { - video.Add(new XElement("hdrtype", "")); - } + if (movieFile.MediaInfo.VideoHdrFormat is HdrFormat.DolbyVision or HdrFormat.DolbyVisionHdr10 or HdrFormat.DolbyVisionHdr10Plus or HdrFormat.DolbyVisionHlg or HdrFormat.DolbyVisionSdr) + { + video.Add(new XElement("hdrtype", "dolbyvision")); + } + else if (movieFile.MediaInfo.VideoHdrFormat is HdrFormat.Hdr10 or HdrFormat.Hdr10Plus or HdrFormat.Pq10) + { + video.Add(new XElement("hdrtype", "hdr10")); + } + else if (movieFile.MediaInfo.VideoHdrFormat == HdrFormat.Hlg10) + { + video.Add(new XElement("hdrtype", "hlg")); + } + else if (movieFile.MediaInfo.VideoHdrFormat == HdrFormat.None) + { + video.Add(new XElement("hdrtype", "")); + } - streamDetails.Add(video); + streamDetails.Add(video); - var audio = new XElement("audio"); - var audioChannelCount = movieFile.MediaInfo.AudioChannels; - audio.Add(new XElement("bitrate", movieFile.MediaInfo.AudioBitrate)); - audio.Add(new XElement("channels", audioChannelCount)); - audio.Add(new XElement("codec", MediaInfoFormatter.FormatAudioCodec(movieFile.MediaInfo, sceneName))); - audio.Add(new XElement("language", movieFile.MediaInfo.AudioLanguages)); - streamDetails.Add(audio); + var audio = new XElement("audio"); + var audioChannelCount = movieFile.MediaInfo.AudioChannels; + audio.Add(new XElement("bitrate", movieFile.MediaInfo.AudioBitrate)); + audio.Add(new XElement("channels", audioChannelCount)); + audio.Add(new XElement("codec", MediaInfoFormatter.FormatAudioCodec(movieFile.MediaInfo, sceneName))); + audio.Add(new XElement("language", movieFile.MediaInfo.AudioLanguages)); + streamDetails.Add(audio); - if (movieFile.MediaInfo.Subtitles != null && movieFile.MediaInfo.Subtitles.Count > 0) + if (movieFile.MediaInfo.Subtitles is { Count: > 0 }) + { + foreach (var s in movieFile.MediaInfo.Subtitles) { - foreach (var s in movieFile.MediaInfo.Subtitles) - { - var subtitle = new XElement("subtitle"); - subtitle.Add(new XElement("language", s)); - streamDetails.Add(subtitle); - } + var subtitle = new XElement("subtitle"); + subtitle.Add(new XElement("language", s)); + streamDetails.Add(subtitle); } + } - fileInfo.Add(streamDetails); - details.Add(fileInfo); + fileInfo.Add(streamDetails); + details.Add(fileInfo); - foreach (var credit in credits) + foreach (var credit in credits) + { + if (credit.Name != null && credit.Character != null) { - if (credit.Name != null && credit.Character != null) - { - var actorElement = new XElement("actor"); - - actorElement.Add(new XElement("name", credit.Name)); - actorElement.Add(new XElement("role", credit.Character)); - actorElement.Add(new XElement("order", credit.Order)); + var actorElement = new XElement("actor"); - var headshot = credit.Images.FirstOrDefault(m => m.CoverType == MediaCoverTypes.Headshot); + actorElement.Add(new XElement("name", credit.Name)); + actorElement.Add(new XElement("role", credit.Character)); + actorElement.Add(new XElement("order", credit.Order)); - if (headshot != null && headshot.RemoteUrl != null) - { - actorElement.Add(new XElement("thumb", headshot.RemoteUrl)); - } + var headshot = credit.Images.FirstOrDefault(m => m.CoverType == MediaCoverTypes.Headshot); - details.Add(actorElement); + if (headshot != null && headshot.RemoteUrl != null) + { + actorElement.Add(new XElement("thumb", headshot.RemoteUrl)); } + + details.Add(actorElement); } } + } - doc.Add(details); - doc.Save(xw); + var doc = new XDocument(details) + { + Declaration = new XDeclaration("1.0", "UTF-8", "yes"), + }; - xmlResult += doc.ToString(); - xmlResult += Environment.NewLine; - } + using var sw = new Utf8StringWriter(); + using var xw = XmlWriter.Create(sw, new XmlWriterSettings + { + Encoding = Encoding.UTF8, + Indent = true + }); + + doc.Save(xw); + xw.Flush(); + + xmlResult += sw.ToString(); + xmlResult += Environment.NewLine; } if (Settings.MovieMetadataURL)