diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs index 57d0c26b9c..6eaecff0f5 100644 --- a/Emby.Server.Implementations/Library/PathExtensions.cs +++ b/Emby.Server.Implementations/Library/PathExtensions.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Text.RegularExpressions; +using MediaBrowser.Common.Providers; namespace Emby.Server.Implementations.Library { @@ -43,8 +44,8 @@ namespace Emby.Server.Implementations.Library // for imdbid we also accept pattern matching if (string.Equals(attribute, "imdbid", StringComparison.OrdinalIgnoreCase)) { - var m = Regex.Match(str, "tt([0-9]{7,8})", RegexOptions.IgnoreCase); - return m.Success ? m.Value : null; + var match = ProviderIdParsers.TryFindImdbId(str, out var imdbId); + return match ? imdbId.ToString() : null; } return null; diff --git a/MediaBrowser.Common/Providers/ProviderIdParsers.cs b/MediaBrowser.Common/Providers/ProviderIdParsers.cs new file mode 100644 index 0000000000..64c2e19766 --- /dev/null +++ b/MediaBrowser.Common/Providers/ProviderIdParsers.cs @@ -0,0 +1,125 @@ +#nullable enable + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace MediaBrowser.Common.Providers +{ + /// + /// Parsers for provider ids. + /// + public static class ProviderIdParsers + { + private const int ImdbMinNumbers = 7; + private const int ImdbMaxNumbers = 8; + private const string ImdbPrefix = "tt"; + + /// + /// Parses an IMDb id from a string. + /// + /// The text to parse. + /// The parsed IMDb id. + /// True if parsing was successful, false otherwise. + public static bool TryFindImdbId(ReadOnlySpan text, [NotNullWhen(true)] out ReadOnlySpan imdbId) + { + // imdb id is at least 9 chars (tt + 7 numbers) + while (text.Length >= 2 + ImdbMinNumbers) + { + var ttPos = text.IndexOf(ImdbPrefix); + if (ttPos == -1) + { + imdbId = default; + return false; + } + + text = text.Slice(ttPos); + var i = 2; + var limit = Math.Min(text.Length, ImdbMaxNumbers + 2); + for (; i < limit; i++) + { + var c = text[i]; + if (!IsDigit(c)) + { + break; + } + } + + // skip if more than 8 digits + 2 chars for tt + if (i <= ImdbMaxNumbers + 2 && i >= ImdbMinNumbers + 2) + { + imdbId = text.Slice(0, i); + return true; + } + + text = text.Slice(i); + } + + imdbId = default; + return false; + } + + /// + /// Parses an TMDb id from a movie url. + /// + /// The text with the url to parse. + /// The parsed TMDb id. + /// True if parsing was successful, false otherwise. + public static bool TryFindTmdbMovieId(ReadOnlySpan text, [NotNullWhen(true)] out ReadOnlySpan tmdbId) + => TryFindProviderId(text, "themoviedb.org/movie/", out tmdbId); + + /// + /// Parses an TMDb id from a series url. + /// + /// The text with the url to parse. + /// The parsed TMDb id. + /// True if parsing was successful, false otherwise. + public static bool TryFindTmdbSeriesId(ReadOnlySpan text, [NotNullWhen(true)] out ReadOnlySpan tmdbId) + => TryFindProviderId(text, "themoviedb.org/tv/", out tmdbId); + + /// + /// Parses an TVDb id from a url. + /// + /// The text with the url to parse. + /// The parsed TVDb id. + /// True if parsing was successful, false otherwise. + public static bool TryFindTvdbId(ReadOnlySpan text, [NotNullWhen(true)] out ReadOnlySpan tvdbId) + => TryFindProviderId(text, "thetvdb.com/?tab=series&id=", out tvdbId); + + private static bool TryFindProviderId(ReadOnlySpan text, ReadOnlySpan searchString, [NotNullWhen(true)] out ReadOnlySpan providerId) + { + var searchPos = text.IndexOf(searchString); + if (searchPos == -1) + { + providerId = default; + return false; + } + + text = text.Slice(searchPos + searchString.Length); + + int i = 0; + for (; i < text.Length; i++) + { + var c = text[i]; + + if (!IsDigit(c)) + { + break; + } + } + + if (i >= 1) + { + providerId = text.Slice(0, i); + return true; + } + + providerId = default; + return false; + } + + private static bool IsDigit(char c) + { + return c >= '0' && c <= '9'; + } + } +} diff --git a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs index 3a13f38889..ff9f11eabd 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/BaseNfoParser.cs @@ -6,11 +6,12 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text; -using System.Text.RegularExpressions; using System.Threading; using System.Xml; using MediaBrowser.Common.Configuration; +using MediaBrowser.Common.Providers; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Movies; using MediaBrowser.Controller.Entities.TV; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Providers; @@ -67,8 +68,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers protected virtual bool SupportsUrlAfterClosingXmlTag => false; - protected virtual string MovieDbParserSearchString => "themoviedb.org/movie/"; - /// /// Fetches metadata for an item from one xml file. /// @@ -185,8 +184,7 @@ namespace MediaBrowser.XbmcMetadata.Parsers } else { - // If the file is just an Imdb url, handle that - + // If the file is just provider urls, handle that ParseProviderLinks(item.Item, xml); return; @@ -225,50 +223,29 @@ namespace MediaBrowser.XbmcMetadata.Parsers protected void ParseProviderLinks(T item, string xml) { - // Look for a match for the Regex pattern "tt" followed by 7 or 8 digits - var m = Regex.Match(xml, "tt([0-9]{7,8})", RegexOptions.IgnoreCase); - if (m.Success) + if (ProviderIdParsers.TryFindImdbId(xml, out var imdbId)) { - item.SetProviderId(MetadataProvider.Imdb, m.Value); + item.SetProviderId(MetadataProvider.Imdb, imdbId.ToString()); } - // Support Tmdb - // https://www.themoviedb.org/movie/30287-fallo - var srch = MovieDbParserSearchString; - var index = xml.IndexOf(srch, StringComparison.OrdinalIgnoreCase); - - if (index != -1) + if (item is Movie) { - var tmdbId = xml.AsSpan().Slice(index + srch.Length).TrimEnd('/'); - index = tmdbId.IndexOf('-'); - if (index != -1) - { - tmdbId = tmdbId.Slice(0, index); - } - - if (!tmdbId.IsEmpty - && !tmdbId.IsWhiteSpace() - && int.TryParse(tmdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) + if (ProviderIdParsers.TryFindTmdbMovieId(xml, out var tmdbId)) { - item.SetProviderId(MetadataProvider.Tmdb, value.ToString(UsCulture)); + item.SetProviderId(MetadataProvider.Tmdb, tmdbId.ToString()); } } if (item is Series) { - srch = "thetvdb.com/?tab=series&id="; - - index = xml.IndexOf(srch, StringComparison.OrdinalIgnoreCase); + if (ProviderIdParsers.TryFindTmdbSeriesId(xml, out var tmdbId)) + { + item.SetProviderId(MetadataProvider.Tmdb, tmdbId.ToString()); + } - if (index != -1) + if (ProviderIdParsers.TryFindTvdbId(xml, out var tvdbId)) { - var tvdbId = xml.AsSpan().Slice(index + srch.Length).TrimEnd('/'); - if (!tvdbId.IsEmpty - && !tvdbId.IsWhiteSpace() - && int.TryParse(tvdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)) - { - item.SetProviderId(MetadataProvider.Tvdb, value.ToString(UsCulture)); - } + item.SetProviderId(MetadataProvider.Tvdb, tvdbId.ToString()); } } } diff --git a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs index b466fa25ae..e510557253 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs @@ -49,12 +49,19 @@ namespace MediaBrowser.XbmcMetadata.Parsers { case "id": { + // get ids from attributes string? imdbId = reader.GetAttribute("IMDB"); string? tmdbId = reader.GetAttribute("TMDB"); - if (string.IsNullOrWhiteSpace(imdbId)) + // read id from content + var contentId = reader.ReadElementContentAsString(); + if (contentId.Contains("tt", StringComparison.Ordinal) && string.IsNullOrEmpty(imdbId)) { - imdbId = reader.ReadElementContentAsString(); + imdbId = contentId; + } + else if (string.IsNullOrEmpty(tmdbId)) + { + tmdbId = contentId; } if (!string.IsNullOrWhiteSpace(imdbId)) diff --git a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs index f6574c365c..2c893ac9f0 100644 --- a/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs +++ b/MediaBrowser.XbmcMetadata/Parsers/SeriesNfoParser.cs @@ -37,9 +37,6 @@ namespace MediaBrowser.XbmcMetadata.Parsers /// protected override bool SupportsUrlAfterClosingXmlTag => true; - /// - protected override string MovieDbParserSearchString => "themoviedb.org/tv/"; - /// protected override void FetchDataFromXmlNode(XmlReader reader, MetadataResult itemResult) { diff --git a/tests/Jellyfin.Common.Tests/Providers/ProviderIdParserTests.cs b/tests/Jellyfin.Common.Tests/Providers/ProviderIdParserTests.cs new file mode 100644 index 0000000000..ef9d31cc19 --- /dev/null +++ b/tests/Jellyfin.Common.Tests/Providers/ProviderIdParserTests.cs @@ -0,0 +1,85 @@ +using System; +using MediaBrowser.Common.Providers; +using Xunit; + +namespace Jellyfin.Common.Tests.Providers +{ + public class ProviderIdParserTests + { + [Theory] + [InlineData("tt1234567", "tt1234567")] + [InlineData("tt12345678", "tt12345678")] + [InlineData("https://www.imdb.com/title/tt1234567", "tt1234567")] + [InlineData("https://www.imdb.com/title/tt12345678", "tt12345678")] + [InlineData(@"multiline\nhttps://www.imdb.com/title/tt1234567", "tt1234567")] + [InlineData(@"multiline\nhttps://www.imdb.com/title/tt12345678", "tt12345678")] + [InlineData("tt1234567tt7654321", "tt1234567")] + [InlineData("tt12345678tt7654321", "tt12345678")] + [InlineData("tt123456789", "tt12345678")] + public void FindImdbId_Valid_Success(string text, string expected) + { + Assert.True(ProviderIdParsers.TryFindImdbId(text, out ReadOnlySpan parsedId)); + Assert.Equal(expected, parsedId.ToString()); + } + + [Theory] + [InlineData("tt123456")] + [InlineData("https://www.imdb.com/title/tt123456")] + [InlineData("Jellyfin")] + public void FindImdbId_Invalid_Success(string text) + { + Assert.False(ProviderIdParsers.TryFindImdbId(text, out _)); + } + + [Theory] + [InlineData("https://www.themoviedb.org/movie/30287-fallo", "30287")] + [InlineData("themoviedb.org/movie/30287", "30287")] + public void FindTmdbMovieId_Valid_Success(string text, string expected) + { + Assert.True(ProviderIdParsers.TryFindTmdbMovieId(text, out ReadOnlySpan parsedId)); + Assert.Equal(expected, parsedId.ToString()); + } + + [Theory] + [InlineData("https://www.themoviedb.org/movie/fallo-30287")] + [InlineData("https://www.themoviedb.org/tv/1668-friends")] + public void FindTmdbMovieId_Invalid_Success(string text) + { + Assert.False(ProviderIdParsers.TryFindTmdbMovieId(text, out _)); + } + + [Theory] + [InlineData("https://www.themoviedb.org/tv/1668-friends", "1668")] + [InlineData("themoviedb.org/tv/1668", "1668")] + public void FindTmdbSeriesId_Valid_Success(string text, string expected) + { + Assert.True(ProviderIdParsers.TryFindTmdbSeriesId(text, out ReadOnlySpan parsedId)); + Assert.Equal(expected, parsedId.ToString()); + } + + [Theory] + [InlineData("https://www.themoviedb.org/tv/friends-1668")] + [InlineData("https://www.themoviedb.org/movie/30287-fallo")] + public void FindTmdbSeriesId_Invalid_Success(string text) + { + Assert.False(ProviderIdParsers.TryFindTmdbSeriesId(text, out _)); + } + + [Theory] + [InlineData("https://www.thetvdb.com/?tab=series&id=121361", "121361")] + [InlineData("thetvdb.com/?tab=series&id=121361", "121361")] + public void FindTvdbId_Valid_Success(string text, string expected) + { + Assert.True(ProviderIdParsers.TryFindTvdbId(text, out ReadOnlySpan parsedId)); + Assert.Equal(expected, parsedId.ToString()); + } + + [Theory] + [InlineData("thetvdb.com/?tab=series&id=Jellyfin121361")] + [InlineData("https://www.themoviedb.org/tv/1668-friends")] + public void FindTvdbId_Invalid_Success(string text) + { + Assert.False(ProviderIdParsers.TryFindTvdbId(text, out _)); + } + } +} diff --git a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs index d4ce741323..b58151b3b4 100644 --- a/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs +++ b/tests/Jellyfin.XbmcMetadata.Tests/Parsers/MovieNfoParserTests.cs @@ -203,6 +203,21 @@ namespace Jellyfin.XbmcMetadata.Tests.Parsers Assert.Equal(id, item.ProviderIds[provider]); } + [Fact] + public void Parse_RadarrUrlFile_Success() + { + var result = new MetadataResult