Added more aliases for attributes

Adds tvdb alias for tvdbid and imdb alias for imdbid.

It also fixes an issue where tmdb alias was being ignored
if it was followed by something like "tmdbidfoo".
The same issue prevented imdb pattern matching from
working, if it was followed by something like "imdbidfoo".

It also allows for detecting the first matching occurence,
whether it was an alias or not.

Finally, it ignores attributes with values consisting of only whitespaces.
This commit is contained in:
Jakub Schmidtke
2026-07-07 04:18:02 +02:00
parent e31f168e9b
commit 1294990f4f
2 changed files with 120 additions and 28 deletions
@@ -29,17 +29,41 @@ namespace Emby.Server.Implementations.Library
throw new ArgumentException("String can't be empty.", nameof(attribute));
}
var attributeIndex = str.IndexOf(attribute, StringComparison.OrdinalIgnoreCase);
// Must be at least 3 characters after the attribute =, ], any character,
// then we offset it by 1, because we want the index and not length.
var maxIndex = str.Length - attribute.Length - 2;
while (attributeIndex > -1 && attributeIndex < maxIndex)
// Allow tmdb as an alias for tmdbid, tvdb for tvdbid, etc.
// The code below only supports aliases for attributes in the form of "<alias>id".
ReadOnlySpan<char> shortAttr = attribute switch
{
var attributeEnd = attributeIndex + attribute.Length;
_ when attribute.Equals("tmdbid", StringComparison.OrdinalIgnoreCase) => "tmdb",
_ when attribute.Equals("tvdbid", StringComparison.OrdinalIgnoreCase) => "tvdb",
_ when attribute.Equals("imdbid", StringComparison.OrdinalIgnoreCase) => "imdb",
_ => string.Empty
};
for (int strIndex = 0, attributeIndex = 0; attributeIndex > -1;)
{
// We may want to use imdbid pattern matching later, so we don't want to modify the original 'str'.
var subStr = str[strIndex..];
int attributeEnd = 0;
if (shortAttr.Length > 0)
{
// If we are using an alias it should be shorter (and a prefix), so let's search for that.
attributeIndex = subStr.IndexOf(shortAttr, StringComparison.OrdinalIgnoreCase);
attributeEnd = attributeIndex + shortAttr.Length;
}
else
{
attributeIndex = subStr.IndexOf(attribute, StringComparison.OrdinalIgnoreCase);
attributeEnd = attributeIndex + attribute.Length;
}
// The next iteration should start at the end of the attribute we just found.
// If attributeIndex < 0, the loop will end and strIndex won't be used again.
strIndex += attributeEnd;
if (attributeIndex > 0)
{
var attributeOpener = str[attributeIndex - 1];
var attributeOpener = subStr[attributeIndex - 1];
var attributeCloser = attributeOpener switch
{
'[' => ']',
@@ -47,20 +71,37 @@ namespace Emby.Server.Implementations.Library
'{' => '}',
_ => '\0'
};
if (attributeCloser != '\0' && (str[attributeEnd] == '=' || str[attributeEnd] == '-'))
{
var closingIndex = str[attributeEnd..].IndexOf(attributeCloser);
// Must be at least 1 character before the closing bracket.
if (closingIndex > 1)
if (attributeCloser != '\0')
{
if (shortAttr.Length > 0
&& attributeEnd + 1 < subStr.Length
&& (subStr[attributeEnd] is 'i' or 'I')
&& (subStr[attributeEnd + 1] is 'd' or 'D'))
{
return str[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim().ToString();
// We were searching for a shortened attribute, but it's followed by "id" - let's skip it.
attributeEnd += 2;
}
// attributeEnd points at '='.
// We need at least 1 more character and the closing bracket after that.
if (attributeEnd + 2 < subStr.Length && (subStr[attributeEnd] is '=' or '-'))
{
var closingIndex = subStr[attributeEnd..].IndexOf(attributeCloser);
// Must be at least 1 character before the closing bracket.
if (closingIndex > 1)
{
var trimmed = subStr[(attributeEnd + 1)..(attributeEnd + closingIndex)].Trim();
if (trimmed.Length > 0)
{
return trimmed.ToString();
}
}
}
}
}
str = str[attributeEnd..];
attributeIndex = str.IndexOf(attribute, StringComparison.OrdinalIgnoreCase);
}
// for imdbid we also accept pattern matching
@@ -70,16 +111,6 @@ namespace Emby.Server.Implementations.Library
return match ? imdbId.ToString() : null;
}
// Allow tmdb as an alias for tmdbid
if (attribute.Equals("tmdbid", StringComparison.OrdinalIgnoreCase))
{
var tmdbValue = str.GetAttributeValue("tmdb");
if (tmdbValue is not null)
{
return tmdbValue;
}
}
return null;
}
@@ -9,44 +9,105 @@ namespace Jellyfin.Server.Implementations.Tests.Library
{
[Theory]
[InlineData("Superman: Red Son [imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [imdb=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [imdbid-tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [imdb-tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son - tt10985510", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son {imdbid=tt10985510}", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son {imdb=tt10985510}", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son {imdbid-tt10985510}", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son {imdb-tt10985510}", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son (imdbid=tt10985510)", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son (imdb=tt10985510)", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son (imdbid-tt10985510)", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son (imdb-tt10985510)", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son", "imdbid", null)]
[InlineData("Superman: Red Son [imdbid1=tt11111111][imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [imdbid1=tt11111111][imdb=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son {imdbid1=tt11111111}(imdbid=tt10985510)", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son {imdbid1=tt11111111}(imdb=tt10985510)", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son (imdbid1-tt11111111)[imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son (imdbid1-tt11111111)[imdb=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdbid=618355][imdbid=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdbid=618355][imdb=tt10985510]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdbid-618355]{imdbid-tt10985510}", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdbid-618355]{imdb-tt10985510}", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son (tmdbid-618355)[imdbid-tt10985510]", "tmdbid", "618355")]
[InlineData("Superman: Red Son (tmdbid-618355)[imdb-tt10985510]", "tmdbid", "618355")]
[InlineData("Superman: Red Son [providera-id=1]", "providera-id", "1")]
[InlineData("Superman: Red Son [providerb-id=2]", "providerb-id", "2")]
[InlineData("Superman: Red Son [providera id=4]", "providera id", "4")]
[InlineData("Superman: Red Son [providerb id=5]", "providerb id", "5")]
[InlineData("Superman: Red Son [provider=99][providerid=5]", "providerid", "5")]
[InlineData("Superman: Red Son [tmdbid=3]", "tmdbid", "3")]
[InlineData("Superman: Red Son [tvdbid-6]", "tvdbid", "6")]
[InlineData("Superman: Red Son [tmdb=3]", "tmdbid", "3")]
[InlineData("Superman: Red Son [tmdbid-3]", "tmdbid", "3")]
[InlineData("Superman: Red Son [tmdb-3]", "tmdbid", "3")]
[InlineData("Superman: Red Son {tmdbid=3}", "tmdbid", "3")]
[InlineData("Superman: Red Son {tmdb=3}", "tmdbid", "3")]
[InlineData("Superman: Red Son {tmdbid-3}", "tmdbid", "3")]
[InlineData("Superman: Red Son {tmdb-3}", "tmdbid", "3")]
[InlineData("Superman: Red Son (tmdbid=6)", "tmdbid", "6")]
[InlineData("Superman: Red Son (tmdb=6)", "tmdbid", "6")]
[InlineData("Superman: Red Son (tmdbid-6)", "tmdbid", "6")]
[InlineData("Superman: Red Son (tmdb-6)", "tmdbid", "6")]
[InlineData("Superman: Red Son [tvdbid=6]", "tvdbid", "6")]
[InlineData("Superman: Red Son [tvdb=6]", "tvdbid", "6")]
[InlineData("Superman: Red Son [tvdbid-6]", "tvdbid", "6")]
[InlineData("Superman: Red Son [tvdb-6]", "tvdbid", "6")]
[InlineData("Superman: Red Son {tvdbid=3}", "tvdbid", "3")]
[InlineData("Superman: Red Son {tvdb=3}", "tvdbid", "3")]
[InlineData("Superman: Red Son {tvdbid-3}", "tvdbid", "3")]
[InlineData("Superman: Red Son {tvdb-3}", "tvdbid", "3")]
[InlineData("Superman: Red Son (tvdbid=6)", "tvdbid", "6")]
[InlineData("Superman: Red Son (tvdb=6)", "tvdbid", "6")]
[InlineData("Superman: Red Son (tvdbid-6)", "tvdbid", "6")]
[InlineData("Superman: Red Son (tvdb-6)", "tvdbid", "6")]
[InlineData("[tmdbid=618355]", "tmdbid", "618355")]
[InlineData("[tmdb=618355]", "tmdbid", "618355")]
[InlineData("{tmdbid=618355}", "tmdbid", "618355")]
[InlineData("{tmdb=618355}", "tmdbid", "618355")]
[InlineData("(tmdbid=618355)", "tmdbid", "618355")]
[InlineData("(tmdb=618355)", "tmdbid", "618355")]
[InlineData("[tmdbid-618355]", "tmdbid", "618355")]
[InlineData("[tmdb-618355]", "tmdbid", "618355")]
[InlineData("{tmdbid-618355)", "tmdbid", null)]
[InlineData("{tmdb-618355)", "tmdbid", null)]
[InlineData("[tmdbid-618355}", "tmdbid", null)]
[InlineData("[tmdb-618355}", "tmdbid", null)]
[InlineData("tmdbid=111111][tmdbid=618355]", "tmdbid", "618355")]
[InlineData("tmdbid=111111][tmdb=618355]", "tmdbid", "618355")]
[InlineData("[tmdbid=618355]tmdbid=111111]", "tmdbid", "618355")]
[InlineData("[tmdb=618355]tmdbid=111111]", "tmdbid", "618355")]
[InlineData("tmdbid=618355]", "tmdbid", null)]
[InlineData("tmdb=618355]", "tmdbid", null)]
[InlineData("[tmdbid=618355", "tmdbid", null)]
[InlineData("[tmdb=618355", "tmdbid", null)]
[InlineData("tmdbid=618355", "tmdbid", null)]
[InlineData("tmdb=618355", "tmdbid", null)]
[InlineData("tmdbid=", "tmdbid", null)]
[InlineData("tmdb=", "tmdbid", null)]
[InlineData("tmdbid", "tmdbid", null)]
[InlineData("tmdb", "tmdbid", null)]
[InlineData("[tmdbid= ][tmdbid=223344]", "tmdbid", "223344")]
[InlineData("[tmdb= ][tmdb=223344]", "tmdbid", "223344")]
[InlineData("[tmdbid= ][tmdb=223344]", "tmdbid", "223344")]
[InlineData("[tmdb= ][tmdbid=223344]", "tmdbid", "223344")]
[InlineData("[tmdbid=][imdbid=tt10985510]", "tmdbid", null)]
[InlineData("[tmdb=][imdbid=tt10985510]", "tmdbid", null)]
[InlineData("[tmdbid-][imdbid-tt10985510]", "tmdbid", null)]
[InlineData("[tmdb-][imdbid-tt10985510]", "tmdbid", null)]
[InlineData("Superman: Red Son [tmdbid-618355][tmdbid=1234567]", "tmdbid", "618355")]
[InlineData("Superman: Red Son [tmdb-618355][tmdbid=1234567]", "tmdbid", "618355")]
[InlineData("{tmdbid=}{imdbid=tt10985510}", "tmdbid", null)]
[InlineData("{tmdb=}{imdbid=tt10985510}", "tmdbid", null)]
[InlineData("(tmdbid-)(imdbid-tt10985510)", "tmdbid", null)]
[InlineData("(tmdb-)(imdbid-tt10985510)", "tmdbid", null)]
[InlineData("Superman: Red Son {tmdbid-618355}{tmdbid=1234567}", "tmdbid", "618355")]
[InlineData("Superman: Red Son {tmdb-618355}{tmdbid=1234567}", "tmdbid", "618355")]
[InlineData("Superman: Red Son - tt10985510 [imdbid1=tt11]", "imdbid", "tt10985510")]
[InlineData("Superman: Red Son [tmdb=618355][tmdbid1=1]", "tmdbid", "618355")]
[InlineData("Superman: Red Son [tmdb=618355][tmdbid=12345]", "tmdbid", "618355")]
public void GetAttributeValue_ValidArgs_Correct(string input, string attribute, string? expectedResult)
{
Assert.Equal(expectedResult, PathExtensions.GetAttributeValue(input, attribute));