diff --git a/Emby.Server.Implementations/Library/PathExtensions.cs b/Emby.Server.Implementations/Library/PathExtensions.cs
index 57d0c26b9..6eaecff0f 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 000000000..64c2e1976
--- /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 3a13f3888..ff9f11eab 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)
+ if (ProviderIdParsers.TryFindTmdbMovieId(xml, out var tmdbId))
{
- tmdbId = tmdbId.Slice(0, index);
- }
-
- if (!tmdbId.IsEmpty
- && !tmdbId.IsWhiteSpace()
- && int.TryParse(tmdbId, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
- {
- 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 (index != -1)
+ if (ProviderIdParsers.TryFindTmdbSeriesId(xml, out var tmdbId))
{
- 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.Tmdb, tmdbId.ToString());
+ }
+
+ if (ProviderIdParsers.TryFindTvdbId(xml, out var tvdbId))
+ {
+ item.SetProviderId(MetadataProvider.Tvdb, tvdbId.ToString());
}
}
}
diff --git a/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs b/MediaBrowser.XbmcMetadata/Parsers/MovieNfoParser.cs
index b466fa25a..e51055725 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 f6574c365..2c893ac9f 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 000000000..ef9d31cc1
--- /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 d4ce74132..b58151b3b 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