From d4eeafe53ff8ae2f287f5dca49a873cd71f4c3da Mon Sep 17 00:00:00 2001 From: TheMelmacian <76712303+TheMelmacian@users.noreply.github.com> Date: Tue, 30 Jul 2024 17:51:08 +0200 Subject: [PATCH] Fix: parsing of xbmc style multi episode nfo files (#12268) --- CONTRIBUTORS.md | 1 + .../Parsers/EpisodeNfoParser.cs | 116 +++++++++--------- .../Parsers/EpisodeNfoProviderTests.cs | 24 ++++ .../Test Data/Rising.nfo | 24 ++++ .../Stargate Atlantis S01E01-E04.nfo | 89 ++++++++++++++ 5 files changed, 195 insertions(+), 59 deletions(-) create mode 100644 tests/Jellyfin.XbmcMetadata.Tests/Test Data/Stargate Atlantis S01E01-E04.nfo diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index edbc846d63..7c2e72327a 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -185,6 +185,7 @@ - [Vedant](https://github.com/viktory36/) - [NotSaifA](https://github.com/NotSaifA) - [HonestlyWhoKnows](https://github.com/honestlywhoknows) + - [TheMelmacian](https://github.com/TheMelmacian) - [ItsAllAboutTheCode](https://github.com/ItsAllAboutTheCode) # Emby Contributors diff --git a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs index 044efb51e9..2a1a14834b 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/EpisodeNfoParser.cs @@ -59,80 +59,50 @@ namespace MediaBrowser.XbmcMetadata.Parsers try { // Extract episode details from the first episodedetails block - using (var stringReader = new StringReader(xml)) - using (var reader = XmlReader.Create(stringReader, settings)) - { - reader.MoveToContent(); - reader.Read(); - - // Loop through each element - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (reader.NodeType == XmlNodeType.Element) - { - FetchDataFromXmlNode(reader, item); - } - else - { - reader.Read(); - } - } - } + ReadEpisodeDetailsFromXml(item, xml, settings, cancellationToken); // Extract the last episode number from nfo - // Retrieves all title and plot tags from the rest of the nfo and concatenates them with the first episode + // Retrieves all additional episodedetails blocks from the rest of the nfo and concatenates the name, originalTitle and overview tags with the first episode // This is needed because XBMC metadata uses multiple episodedetails blocks instead of episodenumberend tag var name = new StringBuilder(item.Item.Name); + var originalTitle = new StringBuilder(item.Item.OriginalTitle); var overview = new StringBuilder(item.Item.Overview); while ((index = xmlFile.IndexOf(srch, StringComparison.OrdinalIgnoreCase)) != -1) { xml = xmlFile.Substring(0, index + srch.Length); xmlFile = xmlFile.Substring(index + srch.Length); - using (var stringReader = new StringReader(xml)) - using (var reader = XmlReader.Create(stringReader, settings)) + var additionalEpisode = new MetadataResult() + { + Item = new Episode() + }; + + // Extract episode details from additional episodedetails block + ReadEpisodeDetailsFromXml(additionalEpisode, xml, settings, cancellationToken); + + if (!string.IsNullOrEmpty(additionalEpisode.Item.Name)) + { + name.Append(" / ").Append(additionalEpisode.Item.Name); + } + + if (!string.IsNullOrEmpty(additionalEpisode.Item.Overview)) + { + overview.Append(" / ").Append(additionalEpisode.Item.Overview); + } + + if (!string.IsNullOrEmpty(additionalEpisode.Item.OriginalTitle)) + { + originalTitle.Append(" / ").Append(additionalEpisode.Item.OriginalTitle); + } + + if (additionalEpisode.Item.IndexNumber != null) { - reader.MoveToContent(); - - while (!reader.EOF && reader.ReadState == ReadState.Interactive) - { - cancellationToken.ThrowIfCancellationRequested(); - - if (reader.NodeType == XmlNodeType.Element) - { - switch (reader.Name) - { - case "name": - case "title": - case "localtitle": - name.Append(" / ").Append(reader.ReadElementContentAsString()); - break; - case "episode": - { - if (int.TryParse(reader.ReadElementContentAsString(), out var num)) - { - item.Item.IndexNumberEnd = Math.Max(num, item.Item.IndexNumberEnd ?? num); - } - - break; - } - - case "biography": - case "plot": - case "review": - overview.Append(" / ").Append(reader.ReadElementContentAsString()); - break; - } - } - - reader.Read(); - } + item.Item.IndexNumberEnd = Math.Max((int)additionalEpisode.Item.IndexNumber, item.Item.IndexNumberEnd ?? (int)additionalEpisode.Item.IndexNumber); } } item.Item.Name = name.ToString(); + item.Item.OriginalTitle = originalTitle.ToString(); item.Item.Overview = overview.ToString(); } catch (XmlException) @@ -200,5 +170,33 @@ namespace MediaBrowser.XbmcMetadata.Parsers break; } } + + /// + /// Reads the episode details from the given xml and saves the result in the provided result item. + /// + private void ReadEpisodeDetailsFromXml(MetadataResult item, string xml, XmlReaderSettings settings, CancellationToken cancellationToken) + { + using (var stringReader = new StringReader(xml)) + using (var reader = XmlReader.Create(stringReader, settings)) + { + reader.MoveToContent(); + reader.Read(); + + // Loop through each element + while (!reader.EOF && reader.ReadState == ReadState.Interactive) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (reader.NodeType == XmlNodeType.Element) + { + FetchDataFromXmlNode(reader, item); + } + else + { + reader.Read(); + } + } + } + } } } diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs index c0d06116b5..3721d1f7ac 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/EpisodeNfoProviderTests.cs @@ -123,6 +123,30 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers Assert.Equal(2004, item.ProductionYear); } + [Fact] + public void Fetch_Valid_MultiEpisode_With_Missing_Tags_Success() + { + var result = new MetadataResult() + { + Item = new Episode() + }; + + _parser.Fetch(result, "Test Data/Stargate Atlantis S01E01-E04.nfo", CancellationToken.None); + + var item = result.Item; + // provided for episode 1, 3 and 4 + Assert.Equal("Rising / Hide and Seek / Thirty-Eight Minutes", item.Name); + // <originaltitle> provided for all episodes + Assert.Equal("Rising (1) / Rising (2) / Hide and Seek / Thirty-Eight Minutes", item.OriginalTitle); + Assert.Equal(1, item.IndexNumber); + Assert.Equal(4, item.IndexNumberEnd); + Assert.Equal(1, item.ParentIndexNumber); + // <plot> only provided for episode 1 + Assert.Equal("A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy.", item.Overview); + Assert.Equal(new DateTime(2004, 7, 16), item.PremiereDate); + Assert.Equal(2004, item.ProductionYear); + } + [Fact] public void Parse_GivenFileWithThumbWithoutAspect_Success() { diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Rising.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Rising.nfo index 56250c09a8..e95f5002a1 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Rising.nfo +++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Rising.nfo @@ -7,6 +7,18 @@ <thumb>https://artworks.thetvdb.com/banners/episodes/70851/25333.jpg</thumb> <watched>false</watched> <rating>8.0</rating> + <actor> + <name>Joe Flanigan</name> + <role>John Sheppard</role> + <order>0</order> + <thumb>https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg</thumb> + </actor> + <actor> + <name>David Hewlett</name> + <role>Rodney McKay</role> + <order>1</order> + <thumb>https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg</thumb> + </actor> </episodedetails> <episodedetails> <title>Rising (2) @@ -17,4 +29,16 @@ https://artworks.thetvdb.com/banners/episodes/70851/25334.jpg false 7.9 + + Joe Flanigan + John Sheppard + 0 + https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg + + + David Hewlett + Rodney McKay + 1 + https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg + diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Stargate Atlantis S01E01-E04.nfo b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Stargate Atlantis S01E01-E04.nfo new file mode 100644 index 0000000000..7dee0110c4 --- /dev/null +++ b/tests/Jellyfin.XbmcMetadata.Tests/Test Data/Stargate Atlantis S01E01-E04.nfo @@ -0,0 +1,89 @@ + + Rising + Rising (1) + 1 + 1 + 2004-07-16 + A new Stargate team embarks on a dangerous mission to a distant galaxy, where they discover a mythical lost city -- and a deadly new enemy. + https://artworks.thetvdb.com/banners/episodes/70851/25333.jpg + false + 8.0 + + Joe Flanigan + John Sheppard + 0 + https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg + + + David Hewlett + Rodney McKay + 1 + https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg + + + + Rising (2) + 1 + 2 + 2004-07-16 + https://artworks.thetvdb.com/banners/episodes/70851/25334.jpg + false + 7.9 + + Joe Flanigan + John Sheppard + 0 + https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg + + + David Hewlett + Rodney McKay + 1 + https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg + + + + Hide and Seek + Hide and Seek + 1 + 3 + 2004-07-23 + https://artworks.thetvdb.com/banners/episodes/70851/25335.jpg + false + 7.5 + + Joe Flanigan + John Sheppard + 0 + https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg + + + David Hewlett + Rodney McKay + 1 + https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg + + + + Thirty-Eight Minutes + Thirty-Eight Minutes + 1 + 4 + 2004-07-23 + https://artworks.thetvdb.com/banners/episodes/70851/25336.jpg + false + 7.5 + + Joe Flanigan + John Sheppard + 0 + https://image.tmdb.org/t/p/w300_and_h450_bestv2/5AA1ORKIsnMakT6fCVy3JKlzMs6.jpg + + + David Hewlett + Rodney McKay + 1 + https://image.tmdb.org/t/p/w300_and_h450_bestv2/hUcYyssAPCqnZ4GjolhOWXHTWSa.jpg + + +