Skip to content

Commit

Permalink
Merge pull request #139 from angelobreuer/feature/angelobreuer/improv…
Browse files Browse the repository at this point in the history
…ed-strictsearch

refactor!: Improve strict search
  • Loading branch information
angelobreuer authored Jan 27, 2024
2 parents 81a98a3 + 01cd33a commit 68a11c6
Show file tree
Hide file tree
Showing 5 changed files with 334 additions and 53 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
run: dotnet restore src/Lavalink4NET.sln

- name: Build
run: dotnet build src/Lavalink4NET.sln --no-restore --configuration ${{ matrix.configuration }} /property:Version=4.0.8
run: dotnet build src/Lavalink4NET.sln --no-restore --configuration ${{ matrix.configuration }} /property:Version=4.0.9

- name: Run tests
working-directory: ci
Expand Down
124 changes: 124 additions & 0 deletions src/Lavalink4NET.Rest/Entities/Tracks/StrictSearchBehavior.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
namespace Lavalink4NET.Rest.Entities.Tracks;

public enum StrictSearchBehavior : byte
{
/// <summary>
/// Denotes that strict search will throw if a query is found that might be ambiguous.
/// </summary>
/// <remarks>
/// <example>
/// The following queries would throw an exception:
/// <list type="bullet">
/// <item>`ytsearch:abc`</item>
/// <item>`spsearch:abc`</item>
/// </list>
///
/// The following query would not throw an exception:
/// <list type="bullet">
/// <item>`abc`</item>
/// <item>`https://example.com/test.mp3`</item>
/// <item>`https://youtube.com/watch?v=[...]`</item>
/// <item>`https://open.spotify.com/playlist/[...]`</item>
/// </list>
/// </example>
///
/// <example>
/// The following queries will be converted to resolve as the following queries:
/// <list type="bullet">
/// <item>(Search Mode: `ytsearch`) `abc` -> `ytsearch:abc`;</item>
/// <item>(Search Mode: `&lt;none&gt;`) `abc` -> `abc`;</item>
/// </list>
/// </example>
/// </remarks>
Throw,

/// <summary>
/// Denotes that if strict search would be triggered, the query will be prefixed with
/// the specified search mode to avoid ambiguous results.
/// </summary>
/// <remarks>
/// <example>
/// The following queries will be converted to resolve as the following queries (Search Mode here is ytsearch):
/// <list type="bullet">
/// <item>`abc` -> `ytsearch:abc`;</item>
/// <item>`PR: abc` -> `ytsearch:PR: abc`;</item>
/// <item>`ytsearch:abc` -> `ytsearch:ytsearch:abc`</item>
/// <item>`spsearch:abc` -> `ytsearch:spsearch:abc`</item>
/// <item>`https://example.com/test.mp3` -> `https://example.com/test.mp3`</item>
/// <item>`https://youtube.com/watch?v=[...]` -> `https://youtube.com/watch?v=[...]`</item>
/// <item>`https://open.spotify.com/playlist/[...]` -> `https://open.spotify.com/playlist/[...]`</item>
/// </list>
/// </example>
/// </remarks>
Resolve,

/// <summary>
/// Denotes that strict search will throw if a query is found that might be not using the specified search mode.
/// <see cref="Implicit"/> always requires an explicit search mode to be specified.
/// </summary>
/// <remarks>
/// <example>
/// The following queries would throw an exception:
/// <list type="bullet">
/// <item>`ytsearch:abc`</item>
/// <item>`spsearch:abc`</item>
/// <item>`a:abc`</item>
/// <item>`https://example.com/test.mp3`</item>
/// <item>`https://youtube.com/watch?v=[...]`</item>
/// <item>`https://open.spotify.com/playlist/[...]`</item>
/// </list>
///
/// The following queries would not throw an exception:
/// <list type="bullet">
/// <item>`abc`</item>
/// <item>`def`</item>
/// </list>
/// </example>
/// </remarks>
Implicit,

/// <summary>
/// Denotes that if strict search would be triggered, the query will be prefixed with
/// the specified search mode to avoid ambiguous results.
/// </summary>
/// <remarks>
/// <example>
/// The following queries will be converted to resolve as the following queries (Search Mode here is `ytsearch`):
/// <list type="bullet">
/// <item>`abc` -> `ytsearch:abc`;</item>
/// <item>`PR: abc` -> `ytsearch:PR: abc`;</item>
/// <item>`ytsearch:abc` -> `ytsearch:ytsearch:abc`</item>
/// <item>`spsearch:abc` -> `ytsearch:spsearch:abc`</item>
/// <item>`https://example.com/test.mp3` -> `ytsearch:https://example.com/test.mp3`</item>
/// <item>`https://youtube.com/watch?v=[...]` -> `ytsearch:https://youtube.com/watch?v=[...]`</item>
/// <item>`https://youtube.com/watch?v=[...]` -> `ytsearch:https://youtube.com/watch?v=[...]`</item>
/// <item>`https://open.spotify.com/playlist/[...]` -> `ytsearch:https://open.spotify.com/playlist/[...]`</item>
/// </list>
/// </example>
/// </remarks>
Explicit,

/// <summary>
/// Denotes that strict search will be disabled. If no search mode is specified, the query will be passed through. If a search
/// mode is specified and none set in the query, the query will be prefixed with the specified search mode, in case the query is not a direct URI.
/// </summary>
/// <remarks>
/// If you want to have full control over your queries, choose <see cref="Passthrough"/> and specify no search mode, in
/// that way Lavalink4NET will not process your query in any way.
///
/// <example>
/// The following queries will be converted to resolve as the following queries (Search Mode here is `ytsearch`):
/// <list type="bullet">
/// <item>`abc` -> `ytsearch:abc`;</item>
/// <item>`PR: abc` -> `PR: abc`;</item>
/// <item>`ytsearch:abc` -> `ytsearch:abc`</item>
/// <item>`spsearch:abc` -> `spsearch:abc`</item>
/// <item>`https://example.com/test.mp3` -> `https://example.com/test.mp3`</item>
/// <item>`https://youtube.com/watch?v=[...]` -> `https://youtube.com/watch?v=[...]`</item>
/// <item>`https://youtube.com/watch?v=[...]` -> `https://youtube.com/watch?v=[...]`</item>
/// <item>`https://open.spotify.com/playlist/[...]` -> `https://open.spotify.com/playlist/[...]`</item>
/// </list>
/// </example>
/// </remarks>
Passthrough,
}
17 changes: 15 additions & 2 deletions src/Lavalink4NET.Rest/Entities/Tracks/TrackLoadOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,18 @@

public readonly record struct TrackLoadOptions(
TrackSearchMode SearchMode = default,
bool? StrictSearch = null,
CacheMode CacheMode = CacheMode.Dynamic);
StrictSearchBehavior SearchBehavior = StrictSearchBehavior.Throw,
CacheMode CacheMode = CacheMode.Dynamic)
{
// Constructor for compatibility
public TrackLoadOptions(
TrackSearchMode SearchMode = default,
bool? StrictSearch = null,
CacheMode CacheMode = CacheMode.Dynamic) : this(
SearchMode: SearchMode,
SearchBehavior: StrictSearch.GetValueOrDefault(true) ? StrictSearchBehavior.Throw : StrictSearchBehavior.Passthrough,
CacheMode: CacheMode)
{

}
}
132 changes: 103 additions & 29 deletions src/Lavalink4NET.Rest/LavalinkApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,36 +192,9 @@ public async ValueTask<PlayerInformationModel> UpdatePlayerAsync(string sessionI

internal static string BuildIdentifier(string identifier, TrackLoadOptions loadOptions = default)
{
var strict = loadOptions.StrictSearch.GetValueOrDefault(true);

if (!strict)
{
return loadOptions.SearchMode.Prefix is null
? identifier
: $"{loadOptions.SearchMode.Prefix}:{identifier}";
}

var separatorIndex = identifier.AsSpan().IndexOfAny(':', ' ', '\t');

if (separatorIndex is -1 || identifier[separatorIndex] is not ':')
{
return loadOptions.SearchMode.Prefix is null
? identifier
: $"{loadOptions.SearchMode.Prefix}:{identifier}";
}

var currentPrefix = identifier[..separatorIndex];

if (identifier.AsSpan(separatorIndex + 1).StartsWith("//"))
{
if (currentPrefix.Equals("https", StringComparison.OrdinalIgnoreCase) ||
currentPrefix.Equals("http", StringComparison.OrdinalIgnoreCase))
{
return identifier;
}
}
ArgumentNullException.ThrowIfNull(identifier);

throw new InvalidOperationException($"The query '{identifier}' has an search mode specified while search mode is set explicitly and strict mode is enabled.");
return StrictSearchHelper.Process(loadOptions.SearchBehavior, identifier, loadOptions.SearchMode);
}

internal static (PlaylistInformation Playlist, ImmutableArray<LavalinkTrack> Tracks) CreatePlaylist(PlaylistLoadResultData loadResult)
Expand Down Expand Up @@ -422,3 +395,104 @@ internal static async ValueTask EnsureSuccessStatusCodeAsync(HttpResponseMessage
throw new HttpRequestException($"Response status code does not indicate success: {errorResponse.StatusCode} ({errorResponse.ReasonPhrase}): '{errorResponse.ErrorMessage}' at {errorResponse.RequestPath}");
}
}

internal static class StrictSearchHelper
{
public static string Process(StrictSearchBehavior searchBehavior, string identifier, TrackSearchMode searchMode) => searchBehavior switch

Check warning on line 401 in src/Lavalink4NET.Rest/LavalinkApiClient.cs

View workflow job for this annotation

GitHub Actions / build (Debug)

The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value. For example, the pattern '(Lavalink4NET.Rest.Entities.Tracks.StrictSearchBehavior)5' is not covered.

Check warning on line 401 in src/Lavalink4NET.Rest/LavalinkApiClient.cs

View workflow job for this annotation

GitHub Actions / build (Debug)

The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value. For example, the pattern '(Lavalink4NET.Rest.Entities.Tracks.StrictSearchBehavior)5' is not covered.

Check warning on line 401 in src/Lavalink4NET.Rest/LavalinkApiClient.cs

View workflow job for this annotation

GitHub Actions / build (Release)

The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value. For example, the pattern '(Lavalink4NET.Rest.Entities.Tracks.StrictSearchBehavior)5' is not covered.

Check warning on line 401 in src/Lavalink4NET.Rest/LavalinkApiClient.cs

View workflow job for this annotation

GitHub Actions / build (Release)

The switch expression does not handle some values of its input type (it is not exhaustive) involving an unnamed enum value. For example, the pattern '(Lavalink4NET.Rest.Entities.Tracks.StrictSearchBehavior)5' is not covered.
{
StrictSearchBehavior.Throw => ProcessThrow(identifier, searchMode),
StrictSearchBehavior.Resolve => ProcessResolve(identifier, searchMode),
StrictSearchBehavior.Implicit => ProcessImplicit(identifier, searchMode),
StrictSearchBehavior.Explicit => ProcessExplicit(identifier, searchMode),
StrictSearchBehavior.Passthrough => ProcessPassthrough(identifier, searchMode),
};

private static string ProcessThrow(string identifier, TrackSearchMode searchMode)
{
var separatorIndex = identifier.AsSpan().IndexOfAny(':', ' ', '\t');

if (separatorIndex is -1 || identifier[separatorIndex] is not ':')
{
return PrefixIdentifier(identifier, searchMode);
}

var currentPrefix = identifier[..separatorIndex];

if (identifier.AsSpan(separatorIndex + 1).StartsWith("//"))
{
if (currentPrefix.Equals("https", StringComparison.OrdinalIgnoreCase) ||
currentPrefix.Equals("http", StringComparison.OrdinalIgnoreCase))
{
return identifier;
}
}

throw new InvalidOperationException($"The query '{identifier}' has an search mode specified while search mode is set explicitly and strict mode is enabled (throw).");
}

private static string ProcessResolve(string identifier, TrackSearchMode searchMode)
{
var separatorIndex = identifier.AsSpan().IndexOfAny(':', ' ', '\t');

if (separatorIndex is -1 || identifier[separatorIndex] is not ':')
{
return PrefixIdentifier(identifier, searchMode);
}

var currentPrefix = identifier[..separatorIndex];

if (identifier.AsSpan(separatorIndex + 1).StartsWith("//"))
{
if (currentPrefix.Equals("https", StringComparison.OrdinalIgnoreCase) ||
currentPrefix.Equals("http", StringComparison.OrdinalIgnoreCase))
{
return identifier;
}
}

return PrefixIdentifier(identifier, searchMode);
}

private static string ProcessImplicit(string identifier, TrackSearchMode searchMode)
{
ArgumentNullException.ThrowIfNull(searchMode.Prefix);

var separatorIndex = identifier.AsSpan().IndexOfAny(':', ' ', '\t');

if (separatorIndex is -1 || identifier[separatorIndex] is not ':')
{
return PrefixIdentifier(identifier, searchMode);
}

throw new InvalidOperationException($"The query '{identifier}' has an search mode specified while search mode is set explicitly and strict mode is enabled (implicit).");
}

private static string ProcessExplicit(string identifier, TrackSearchMode searchMode)
{
return PrefixIdentifier(identifier, searchMode);
}

private static string ProcessPassthrough(string identifier, TrackSearchMode searchMode)
{
if (searchMode.Prefix is null)
{
return identifier;
}

var separatorIndex = identifier.AsSpan().IndexOfAny(':', ' ', '\t');

if (separatorIndex is -1 || identifier[separatorIndex] is not ':')
{
return PrefixIdentifier(identifier, searchMode);
}

return identifier;
}

private static string PrefixIdentifier(string identifier, TrackSearchMode searchMode)
{
return searchMode.Prefix is null
? identifier
: $"{searchMode.Prefix}:{identifier}";
}
}
Loading

0 comments on commit 68a11c6

Please sign in to comment.