This commit is contained in:
2026-02-13 20:47:31 +01:00
commit 1aebd48fbb
8 changed files with 979 additions and 0 deletions

44
.gitignore vendored Normal file
View File

@@ -0,0 +1,44 @@
# Build results
[Dd]ebug/
[Rr]elease/
x64/
x86/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# NuGet packages
*.nupkg
*.snupkg
.nuget/
packages/
# Visual Studio
.vs/
*.suo
*.user
*.userosscache
*.sln.docstates
# JetBrains Rider
.idea/
*.sln.iml
# VS Code
.vscode/
# Build artifacts
*.dll
*.exe
*.pdb
*.cache
# Test results
TestResults/
coverage/
# OS files
.DS_Store
Thumbs.db

22
LICENSE Normal file
View File

@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2024
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

22
Lavalink4NET.Jellyfin.sln Normal file
View File

@@ -0,0 +1,22 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lavalink4NET.Jellyfin", "Lavalink4NET.Jellyfin\Lavalink4NET.Jellyfin.csproj", "{90868A8B-38C0-4602-9A91-884A2BF62A43}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{90868A8B-38C0-4602-9A91-884A2BF62A43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{90868A8B-38C0-4602-9A91-884A2BF62A43}.Debug|Any CPU.Build.0 = Debug|Any CPU
{90868A8B-38C0-4602-9A91-884A2BF62A43}.Release|Any CPU.ActiveCfg = Release|Any CPU
{90868A8B-38C0-4602-9A91-884A2BF62A43}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,39 @@
using Lavalink4NET.Rest.Entities.Tracks;
namespace Lavalink4NET.Jellyfin;
/// <summary>
/// Provides additional <see cref="TrackSearchMode"/> values for Jellyfin and other custom sources.
/// </summary>
public static class JellyfinSearchMode
{
/// <summary>
/// Jellyfin search mode using the <c>jfsearch:</c> prefix.
/// Use this when searching your Jellyfin library via the Jellylink Lavalink plugin.
/// </summary>
/// <example>
/// <code>
/// var options = new TrackLoadOptions { SearchMode = JellyfinSearchMode.Jellyfin };
/// var tracks = await audioService.Tracks.LoadTracksAsync("Bohemian Rhapsody", options);
/// </code>
/// </example>
public static TrackSearchMode Jellyfin { get; } = new("jfsearch");
/// <summary>
/// Creates a custom search mode with the specified prefix.
/// </summary>
/// <param name="prefix">The search prefix without the colon (e.g., "mysearch" for "mysearch:").</param>
/// <returns>A new <see cref="TrackSearchMode"/> instance configured with the specified prefix.</returns>
/// <example>
/// <code>
/// var mySearchMode = JellyfinSearchMode.Custom("mycustom");
/// var options = new TrackLoadOptions { SearchMode = mySearchMode };
/// </code>
/// </example>
public static TrackSearchMode Custom(string prefix)
{
ArgumentException.ThrowIfNullOrWhiteSpace(prefix);
return new TrackSearchMode(prefix);
}
}

View File

@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<!-- NuGet Package Metadata -->
<PackageId>Lavalink4NET.Jellyfin</PackageId>
<Version>1.0.0</Version>
<Authors>YourName</Authors>
<Company>YourCompany</Company>
<Description>Extends Lavalink4NET with Jellyfin search support (jfsearch:) and utilities for custom search modes. Works with the Jellylink Lavalink plugin.</Description>
<PackageTags>lavalink;lavalink4net;jellyfin;discord;music;bot;jfsearch</PackageTags>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageProjectUrl>https://github.com/YourUsername/Lavalink4NET.Jellyfin</PackageProjectUrl>
<RepositoryUrl>https://github.com/YourUsername/Lavalink4NET.Jellyfin</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageReadmeFile>README.md</PackageReadmeFile>
<!-- Generate XML documentation -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);CS1591</NoWarn>
</PropertyGroup>
<ItemGroup>
<None Include="../README.md" Pack="true" PackagePath="/" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Lavalink4NET.Rest" Version="4.*" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,306 @@
using Lavalink4NET.Rest.Entities.Tracks;
namespace Lavalink4NET.Jellyfin;
/// <summary>
/// Identifies the source/platform for a search query.
/// </summary>
public enum SearchSource
{
/// <summary>No specific source (direct URL or unknown).</summary>
None,
/// <summary>YouTube search.</summary>
YouTube,
/// <summary>YouTube Music search.</summary>
YouTubeMusic,
/// <summary>SoundCloud search.</summary>
SoundCloud,
/// <summary>Spotify search.</summary>
Spotify,
/// <summary>Apple Music search.</summary>
AppleMusic,
/// <summary>Deezer search.</summary>
Deezer,
/// <summary>Yandex Music search.</summary>
YandexMusic,
/// <summary>Jellyfin search.</summary>
Jellyfin,
/// <summary>Custom/user-defined source.</summary>
Custom,
}
/// <summary>
/// Provides extension methods and utilities for parsing search queries with custom prefixes.
/// </summary>
public static class SearchQueryParser
{
private static readonly Dictionary<string, (TrackSearchMode Mode, SearchSource Source)> RegisteredPrefixes = new(StringComparer.OrdinalIgnoreCase)
{
["jfsearch"] = (JellyfinSearchMode.Jellyfin, SearchSource.Jellyfin),
["ytsearch"] = (TrackSearchMode.YouTube, SearchSource.YouTube),
["ytmsearch"] = (TrackSearchMode.YouTubeMusic, SearchSource.YouTubeMusic),
["scsearch"] = (TrackSearchMode.SoundCloud, SearchSource.SoundCloud),
["spsearch"] = (TrackSearchMode.Spotify, SearchSource.Spotify),
["amsearch"] = (TrackSearchMode.AppleMusic, SearchSource.AppleMusic),
["dzsearch"] = (TrackSearchMode.Deezer, SearchSource.Deezer),
["ymsearch"] = (TrackSearchMode.YandexMusic, SearchSource.YandexMusic),
};
/// <summary>
/// Parses a query string and extracts the search mode and clean query.
/// If the query contains a known prefix (e.g., "jfsearch:song name"), returns the appropriate
/// <see cref="TrackSearchMode"/> and the query without the prefix.
/// </summary>
/// <param name="query">The original query string (may contain a prefix like "jfsearch:song").</param>
/// <param name="defaultMode">The default search mode to use if no prefix is found. Defaults to <see cref="TrackSearchMode.YouTube"/>.</param>
/// <returns>A <see cref="ParsedSearchQuery"/> containing the search mode and clean query string.</returns>
public static ParsedSearchQuery Parse(string query, TrackSearchMode defaultMode = default)
{
var result = ParseExtended(query, defaultMode);
return new ParsedSearchQuery(result.SearchMode, result.Query);
}
/// <summary>
/// Parses a query string and returns detailed information about the search source.
/// </summary>
/// <param name="query">The original query string (may contain a prefix like "jfsearch:song").</param>
/// <param name="defaultMode">The default search mode to use if no prefix is found. Defaults to <see cref="TrackSearchMode.YouTube"/>.</param>
/// <returns>A <see cref="SearchQueryResult"/> containing detailed information about the parsed query.</returns>
/// <example>
/// <code>
/// var result = SearchQueryParser.ParseExtended("jfsearch:Bohemian Rhapsody");
/// // result.Source == SearchSource.Jellyfin
/// // result.SearchMode == JellyfinSearchMode.Jellyfin
/// // result.Query == "Bohemian Rhapsody"
/// // result.OriginalQuery == "jfsearch:Bohemian Rhapsody"
/// // result.DetectedPrefix == "jfsearch"
/// // result.IsUrl == false
/// // result.HasPrefix == true
/// </code>
/// </example>
public static SearchQueryResult ParseExtended(string query, TrackSearchMode defaultMode = default)
{
if (string.IsNullOrWhiteSpace(query))
{
var fallback = defaultMode.Equals(default) ? TrackSearchMode.YouTube : defaultMode;
var fallbackSource = GetSourceFromMode(fallback);
return new SearchQueryResult(
SearchMode: fallback,
Source: fallbackSource,
Query: query ?? string.Empty,
OriginalQuery: query ?? string.Empty,
DetectedPrefix: null,
IsUrl: false,
HasPrefix: false
);
}
// Check for known prefixes first (before URL check, as prefixes can look like URIs)
var colonIndex = query.IndexOf(':');
if (colonIndex > 0)
{
var prefix = query[..colonIndex];
if (RegisteredPrefixes.TryGetValue(prefix, out var registered))
{
var cleanQuery = query[(colonIndex + 1)..].TrimStart();
return new SearchQueryResult(
SearchMode: registered.Mode,
Source: registered.Source,
Query: cleanQuery,
OriginalQuery: query,
DetectedPrefix: prefix,
IsUrl: false,
HasPrefix: true
);
}
}
// Check if it's a URL - only consider http(s) URLs
if ((query.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
query.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) &&
Uri.IsWellFormedUriString(query, UriKind.Absolute))
{
return new SearchQueryResult(
SearchMode: TrackSearchMode.None,
Source: SearchSource.None,
Query: query,
OriginalQuery: query,
DetectedPrefix: null,
IsUrl: true,
HasPrefix: false
);
}
// No known prefix found, use default
var defaultFallback = defaultMode.Equals(default) ? TrackSearchMode.YouTube : defaultMode;
var defaultSource = GetSourceFromMode(defaultFallback);
return new SearchQueryResult(
SearchMode: defaultFallback,
Source: defaultSource,
Query: query,
OriginalQuery: query,
DetectedPrefix: null,
IsUrl: false,
HasPrefix: false
);
}
private static SearchSource GetSourceFromMode(TrackSearchMode mode)
{
foreach (var kvp in RegisteredPrefixes)
{
if (kvp.Value.Mode.Equals(mode))
return kvp.Value.Source;
}
return SearchSource.None;
}
/// <summary>
/// Registers a custom search prefix that can be recognized by <see cref="Parse"/> and <see cref="ParseExtended"/>.
/// </summary>
/// <param name="prefix">The prefix to register (without the colon, e.g., "mysearch").</param>
/// <param name="searchMode">The search mode to associate with this prefix.</param>
/// <param name="source">The source type for this prefix. Defaults to <see cref="SearchSource.Custom"/>.</param>
public static void RegisterPrefix(string prefix, TrackSearchMode searchMode, SearchSource source = SearchSource.Custom)
{
ArgumentException.ThrowIfNullOrWhiteSpace(prefix);
RegisteredPrefixes[prefix] = (searchMode, source);
}
/// <summary>
/// Unregisters a custom search prefix.
/// </summary>
/// <param name="prefix">The prefix to unregister.</param>
/// <returns>True if the prefix was found and removed; otherwise, false.</returns>
public static bool UnregisterPrefix(string prefix)
{
return RegisteredPrefixes.Remove(prefix);
}
/// <summary>
/// Checks if the given prefix is registered as a known search mode.
/// </summary>
/// <param name="prefix">The prefix to check (without the colon).</param>
/// <returns>True if the prefix is registered; otherwise, false.</returns>
public static bool IsPrefixRegistered(string prefix)
{
return RegisteredPrefixes.ContainsKey(prefix);
}
/// <summary>
/// Gets all registered search prefixes.
/// </summary>
/// <returns>A read-only collection of registered prefixes.</returns>
public static IReadOnlyCollection<string> GetRegisteredPrefixes()
{
return RegisteredPrefixes.Keys.ToList().AsReadOnly();
}
/// <summary>
/// Tries to get the search mode and source associated with a prefix.
/// </summary>
/// <param name="prefix">The prefix to look up.</param>
/// <param name="searchMode">When this method returns, contains the search mode if found.</param>
/// <param name="source">When this method returns, contains the source if found.</param>
/// <returns>True if the prefix was found; otherwise, false.</returns>
public static bool TryGetSearchMode(string prefix, out TrackSearchMode searchMode, out SearchSource source)
{
if (RegisteredPrefixes.TryGetValue(prefix, out var registered))
{
searchMode = registered.Mode;
source = registered.Source;
return true;
}
searchMode = default;
source = SearchSource.None;
return false;
}
/// <summary>
/// Tries to get the search mode associated with a prefix.
/// </summary>
/// <param name="prefix">The prefix to look up.</param>
/// <param name="searchMode">When this method returns, contains the search mode if found.</param>
/// <returns>True if the prefix was found; otherwise, false.</returns>
public static bool TryGetSearchMode(string prefix, out TrackSearchMode searchMode)
{
return TryGetSearchMode(prefix, out searchMode, out _);
}
}
/// <summary>
/// Represents the detailed result of parsing a search query.
/// </summary>
/// <param name="SearchMode">The Lavalink4NET search mode to use.</param>
/// <param name="Source">The identified search source/platform.</param>
/// <param name="Query">The clean query string without the prefix.</param>
/// <param name="OriginalQuery">The original query string as provided.</param>
/// <param name="DetectedPrefix">The prefix that was detected, or null if none.</param>
/// <param name="IsUrl">Whether the query is a direct URL.</param>
/// <param name="HasPrefix">Whether a known prefix was detected in the query.</param>
public readonly record struct SearchQueryResult(
TrackSearchMode SearchMode,
SearchSource Source,
string Query,
string OriginalQuery,
string? DetectedPrefix,
bool IsUrl,
bool HasPrefix
)
{
/// <summary>
/// Deconstructs to just the search mode and query for simple usage.
/// </summary>
public void Deconstruct(out TrackSearchMode searchMode, out string query)
{
searchMode = SearchMode;
query = Query;
}
/// <summary>
/// Gets whether this result represents a Jellyfin search.
/// </summary>
public bool IsJellyfin => Source == SearchSource.Jellyfin;
/// <summary>
/// Gets whether this result represents a YouTube search.
/// </summary>
public bool IsYouTube => Source == SearchSource.YouTube || Source == SearchSource.YouTubeMusic;
/// <summary>
/// Gets a human-readable name for the search source.
/// </summary>
public string SourceName => Source switch
{
SearchSource.Jellyfin => "Jellyfin",
SearchSource.YouTube => "YouTube",
SearchSource.YouTubeMusic => "YouTube Music",
SearchSource.SoundCloud => "SoundCloud",
SearchSource.Spotify => "Spotify",
SearchSource.AppleMusic => "Apple Music",
SearchSource.Deezer => "Deezer",
SearchSource.YandexMusic => "Yandex Music",
SearchSource.Custom => "Custom",
_ => "Direct"
};
}
/// <summary>
/// Represents the result of parsing a search query, containing the detected search mode and clean query.
/// </summary>
/// <param name="SearchMode">The detected or default search mode.</param>
/// <param name="Query">The clean query string without the prefix.</param>
public readonly record struct ParsedSearchQuery(TrackSearchMode SearchMode, string Query)
{
/// <summary>
/// Deconstructs the parsed query into its components.
/// </summary>
/// <param name="searchMode">The search mode.</param>
/// <param name="query">The clean query.</param>
public void Deconstruct(out TrackSearchMode searchMode, out string query)
{
searchMode = SearchMode;
query = Query;
}
}

View File

@@ -0,0 +1,67 @@
using Lavalink4NET.Rest.Entities.Tracks;
namespace Lavalink4NET.Jellyfin;
/// <summary>
/// Extension methods for <see cref="TrackLoadOptions"/> to simplify working with Jellyfin and custom search modes.
/// </summary>
public static class TrackLoadOptionsExtensions
{
/// <summary>
/// Creates <see cref="TrackLoadOptions"/> configured for Jellyfin search.
/// </summary>
/// <returns>A new <see cref="TrackLoadOptions"/> instance with Jellyfin search mode.</returns>
/// <example>
/// <code>
/// var options = TrackLoadOptionsExtensions.ForJellyfin();
/// var tracks = await audioService.Tracks.LoadTracksAsync("Bohemian Rhapsody", options);
/// </code>
/// </example>
public static TrackLoadOptions ForJellyfin()
{
return new TrackLoadOptions
{
SearchMode = JellyfinSearchMode.Jellyfin
};
}
/// <summary>
/// Creates <see cref="TrackLoadOptions"/> from a parsed search query.
/// </summary>
/// <param name="parsedQuery">The parsed search query containing the search mode.</param>
/// <returns>A new <see cref="TrackLoadOptions"/> instance.</returns>
/// <example>
/// <code>
/// var parsed = SearchQueryParser.Parse("jfsearch:Bohemian Rhapsody");
/// var options = parsed.ToTrackLoadOptions();
/// var tracks = await audioService.Tracks.LoadTracksAsync(parsed.Query, options);
/// </code>
/// </example>
public static TrackLoadOptions ToTrackLoadOptions(this ParsedSearchQuery parsedQuery)
{
return new TrackLoadOptions
{
SearchMode = parsedQuery.SearchMode
};
}
/// <summary>
/// Creates <see cref="TrackLoadOptions"/> with a custom search mode.
/// </summary>
/// <param name="searchMode">The search mode to use.</param>
/// <returns>A new <see cref="TrackLoadOptions"/> instance.</returns>
/// <example>
/// <code>
/// var options = JellyfinSearchMode.Jellyfin.ToTrackLoadOptions();
/// var tracks = await audioService.Tracks.LoadTracksAsync("Bohemian Rhapsody", options);
/// </code>
/// </example>
public static TrackLoadOptions ToTrackLoadOptions(this TrackSearchMode searchMode)
{
return new TrackLoadOptions
{
SearchMode = searchMode
};
}
}

450
README.md Normal file
View File

@@ -0,0 +1,450 @@
# Lavalink4NET.Jellyfin
[![NuGet](https://img.shields.io/nuget/v/Lavalink4NET.Jellyfin.svg)](https://www.nuget.org/packages/Lavalink4NET.Jellyfin)
[![License](https://img.shields.io/github/license/YourUsername/Lavalink4NET.Jellyfin)](LICENSE)
Extends [Lavalink4NET](https://github.com/angelobreuer/Lavalink4NET) with Jellyfin search support (`jfsearch:`) and utilities for custom search modes. Works with the [Jellylink](https://github.com/YourUsername/Jellylink) Lavalink plugin.
## Table of Contents
- [Installation](#installation)
- [Features](#features)
- [Quick Start](#quick-start)
- [API Reference](#api-reference)
- [JellyfinSearchMode](#jellyfinearchmode)
- [SearchQueryParser](#searchqueryparser)
- [SearchQueryResult](#searchqueryresult)
- [SearchSource Enum](#searchsource-enum)
- [Examples](#examples)
- [Basic Usage](#basic-usage)
- [Setting Default Search Provider](#setting-default-search-provider)
- [Using Extended Parse Results](#using-extended-parse-results)
- [Registering Custom Prefixes](#registering-custom-prefixes)
- [Discord Bot Integration](#discord-bot-integration)
- [Supported Prefixes](#supported-prefixes)
- [Requirements](#requirements)
- [License](#license)
---
## Installation
```bash
dotnet add package Lavalink4NET.Jellyfin
```
Or via the NuGet Package Manager:
```powershell
Install-Package Lavalink4NET.Jellyfin
```
---
## Features
- 🎵 **Jellyfin Search Mode** - Search your Jellyfin library directly via Lavalink
- 🔧 **Custom Search Modes** - Create your own search prefixes for any source
- 📝 **Smart Query Parser** - Automatically detect and parse search prefixes from user input
- 🎯 **SearchSource Enum** - Clearly identify which platform a search targets
-**Extension Methods** - Fluent API for creating `TrackLoadOptions`
- 🔗 **URL Detection** - Automatically handles direct URLs vs search queries
---
## Quick Start
```csharp
using Lavalink4NET.Jellyfin;
using Lavalink4NET.Rest.Entities.Tracks;
// Parse user input - automatically detects prefixes
var (searchMode, cleanQuery) = SearchQueryParser.Parse("jfsearch:Bohemian Rhapsody");
// Use with Lavalink4NET
var options = new TrackLoadOptions { SearchMode = searchMode };
var tracks = await audioService.Tracks.LoadTracksAsync(cleanQuery, options);
```
---
## API Reference
### JellyfinSearchMode
Static class providing Jellyfin search mode and utilities for creating custom search modes.
```csharp
public static class JellyfinSearchMode
{
// Pre-configured Jellyfin search mode (jfsearch:)
public static TrackSearchMode Jellyfin { get; }
// Create a custom search mode with any prefix
public static TrackSearchMode Custom(string prefix);
}
```
#### Examples
```csharp
// Use the built-in Jellyfin search mode
var jellyfinMode = JellyfinSearchMode.Jellyfin;
// Create a custom search mode for your own source
var myCustomMode = JellyfinSearchMode.Custom("mysource");
```
---
### SearchQueryParser
Static class for parsing search queries and detecting prefixes.
#### Methods
| Method | Description |
|--------|-------------|
| `Parse(query, defaultMode)` | Parses query and returns `ParsedSearchQuery` (simple) |
| `ParseExtended(query, defaultMode)` | Parses query and returns `SearchQueryResult` (detailed) |
| `RegisterPrefix(prefix, mode, source)` | Registers a custom prefix |
| `UnregisterPrefix(prefix)` | Removes a registered prefix |
| `IsPrefixRegistered(prefix)` | Checks if a prefix is registered |
| `GetRegisteredPrefixes()` | Gets all registered prefixes |
| `TryGetSearchMode(prefix, out mode, out source)` | Tries to get mode and source for a prefix |
#### Parse Method
```csharp
public static ParsedSearchQuery Parse(
string query,
TrackSearchMode defaultMode = default
);
```
**Parameters:**
- `query` - The user's search input (may include a prefix like `jfsearch:`)
- `defaultMode` - The search mode to use when no prefix is detected (defaults to YouTube)
**Returns:** `ParsedSearchQuery` with `SearchMode` and `Query` properties
#### ParseExtended Method
```csharp
public static SearchQueryResult ParseExtended(
string query,
TrackSearchMode defaultMode = default
);
```
**Returns:** `SearchQueryResult` with detailed information about the parsed query
---
### SearchQueryResult
A detailed result structure returned by `ParseExtended()`.
```csharp
public readonly record struct SearchQueryResult(
TrackSearchMode SearchMode, // The Lavalink4NET search mode
SearchSource Source, // The identified platform (enum)
string Query, // Clean query without prefix
string OriginalQuery, // Original input string
string? DetectedPrefix, // The prefix that was found (or null)
bool IsUrl, // True if input was a URL
bool HasPrefix // True if a known prefix was detected
)
{
// Convenience properties
bool IsJellyfin { get; } // True if Source == SearchSource.Jellyfin
bool IsYouTube { get; } // True if YouTube or YouTube Music
string SourceName { get; } // Human-readable name ("Jellyfin", "YouTube", etc.)
}
```
---
### SearchSource Enum
Identifies the search platform/source.
```csharp
public enum SearchSource
{
None, // Direct URL or unknown source
YouTube, // YouTube (ytsearch:)
YouTubeMusic, // YouTube Music (ytmsearch:)
SoundCloud, // SoundCloud (scsearch:)
Spotify, // Spotify (spsearch:)
AppleMusic, // Apple Music (amsearch:)
Deezer, // Deezer (dzsearch:)
YandexMusic, // Yandex Music (ymsearch:)
Jellyfin, // Jellyfin (jfsearch:)
Custom, // User-registered custom source
}
```
---
## Examples
### Basic Usage
```csharp
using Lavalink4NET.Jellyfin;
using Lavalink4NET.Rest.Entities.Tracks;
// User searches with a prefix
var userInput = "jfsearch:Bohemian Rhapsody";
// Parse the query - extracts the prefix and cleans the query
var (searchMode, cleanQuery) = SearchQueryParser.Parse(userInput);
// searchMode = JellyfinSearchMode.Jellyfin
// cleanQuery = "Bohemian Rhapsody"
// Load tracks using the detected search mode
var options = new TrackLoadOptions { SearchMode = searchMode };
var result = await audioService.Tracks.LoadTracksAsync(cleanQuery, options);
```
---
### Setting Default Search Provider
Set which provider to use when the user doesn't specify a prefix:
```csharp
// Default to Jellyfin when no prefix is provided
var (searchMode, query) = SearchQueryParser.Parse(
userInput,
defaultMode: JellyfinSearchMode.Jellyfin
);
// Default to YouTube when no prefix is provided
var (searchMode, query) = SearchQueryParser.Parse(
userInput,
defaultMode: TrackSearchMode.YouTube
);
// Default to SoundCloud when no prefix is provided
var (searchMode, query) = SearchQueryParser.Parse(
userInput,
defaultMode: TrackSearchMode.SoundCloud
);
```
**Example behavior with Jellyfin as default:**
| User Input | Search Mode | Query |
|------------|-------------|-------|
| `Bohemian Rhapsody` | Jellyfin | `Bohemian Rhapsody` |
| `jfsearch:Queen` | Jellyfin | `Queen` |
| `ytsearch:Never Gonna Give You Up` | YouTube | `Never Gonna Give You Up` |
| `https://youtube.com/watch?v=...` | None (URL) | `https://youtube.com/watch?v=...` |
---
### Using Extended Parse Results
Get detailed information about the parsed query:
```csharp
// User input could be anything - with or without prefix
string userInput = GetUserInput(); // e.g., "jfsearch:Queen", "ytsearch:Hello", "Bohemian Rhapsody"
var result = SearchQueryParser.ParseExtended(userInput, JellyfinSearchMode.Jellyfin);
// Access detailed information
Console.WriteLine($"Source: {result.Source}"); // e.g., SearchSource.Jellyfin
Console.WriteLine($"Source Name: {result.SourceName}"); // e.g., "Jellyfin"
Console.WriteLine($"Query: {result.Query}"); // e.g., "Queen" (prefix stripped)
Console.WriteLine($"Original: {result.OriginalQuery}"); // e.g., "jfsearch:Queen"
Console.WriteLine($"Prefix: {result.DetectedPrefix}"); // e.g., "jfsearch" or null
Console.WriteLine($"Is URL: {result.IsUrl}"); // false
Console.WriteLine($"Has Prefix: {result.HasPrefix}"); // true if prefix was detected
// Use conditional logic to show different messages based on source
if (result.IsJellyfin)
{
Console.WriteLine("🏠 Searching your personal Jellyfin library...");
}
else if (result.IsYouTube)
{
Console.WriteLine("▶️ Searching YouTube...");
}
else if (result.IsUrl)
{
Console.WriteLine("🔗 Loading from URL...");
}
else
{
Console.WriteLine($"🔍 Searching {result.SourceName}...");
}
// Still supports deconstruction for simple usage
var (searchMode, cleanQuery) = result;
```
**Example outputs:**
| User Input | Source | Message |
|------------|--------|---------|
| `jfsearch:Queen` | Jellyfin | "🏠 Searching your personal Jellyfin library..." |
| `ytsearch:Hello` | YouTube | "▶️ Searching YouTube..." |
| `Bohemian Rhapsody` | Jellyfin (default) | "🏠 Searching your personal Jellyfin library..." |
| `https://youtube.com/...` | None | "🔗 Loading from URL..." |
| `scsearch:Electronic` | SoundCloud | "🔍 Searching SoundCloud..." |
---
### Registering Custom Prefixes
Add your own search prefixes for custom sources:
```csharp
// Create a custom search mode
var navidromeMode = JellyfinSearchMode.Custom("ndsearch");
// Register it with a custom source type
SearchQueryParser.RegisterPrefix("ndsearch", navidromeMode, SearchSource.Custom);
// Now it works with Parse()
var (mode, query) = SearchQueryParser.Parse("ndsearch:My Song");
// mode = navidromeMode
// query = "My Song"
// And with ParseExtended()
var result = SearchQueryParser.ParseExtended("ndsearch:My Song");
// result.Source = SearchSource.Custom
// result.DetectedPrefix = "ndsearch"
// Check if a prefix is registered
bool isRegistered = SearchQueryParser.IsPrefixRegistered("ndsearch"); // true
// Get all registered prefixes
var allPrefixes = SearchQueryParser.GetRegisteredPrefixes();
// ["jfsearch", "ytsearch", "ytmsearch", "scsearch", "spsearch", "amsearch", "dzsearch", "ymsearch", "ndsearch"]
// Unregister a prefix
SearchQueryParser.UnregisterPrefix("ndsearch");
```
---
### Discord Bot Integration
Complete example for a Discord music bot using Discord.NET and Lavalink4NET:
```csharp
using Discord.WebSocket;
using Lavalink4NET;
using Lavalink4NET.Jellyfin;
using Lavalink4NET.Players.Queued;
using Lavalink4NET.Rest.Entities.Tracks;
public class PlayHandler
{
private readonly IAudioService _audioService;
private readonly DiscordSocketClient _client;
public PlayHandler(IAudioService audioService, DiscordSocketClient client)
{
_audioService = audioService;
_client = client;
}
public async Task HandlePlayCommand(
SocketSlashCommand command,
string searchQuery,
CancellationToken ct = default)
{
// Get or create player for the guild
var player = await GetPlayerAsync(command);
if (player is null) return;
// Parse the query to extract search mode and clean query
// Supports prefixes like jfsearch:, ytsearch:, scsearch:, etc.
// Default: Jellyfin when no prefix is specified
var (searchMode, queryToSearch) = SearchQueryParser.Parse(
searchQuery,
JellyfinSearchMode.Jellyfin
);
// Create track load options with the detected search mode
var trackLoadOptions = new TrackLoadOptions
{
SearchMode = searchMode,
};
// Load tracks
var trackCollection = await _audioService.Tracks.LoadTracksAsync(
queryToSearch,
trackLoadOptions,
cancellationToken: ct
);
// Handle the result
if (trackCollection.Track is null)
{
await command.RespondAsync("❌ No tracks found.");
return;
}
// Play the track
await player.PlayAsync(trackCollection.Track, cancellationToken: ct);
// Send confirmation with source info
var result = SearchQueryParser.ParseExtended(searchQuery, JellyfinSearchMode.Jellyfin);
await command.RespondAsync($"🎵 Now playing from **{result.SourceName}**: {trackCollection.Track.Title}");
}
private async Task<QueuedLavalinkPlayer?> GetPlayerAsync(SocketSlashCommand command)
{
// ... player retrieval logic
}
}
```
**Usage in Discord:**
| Command | Behavior |
|---------|----------|
| `/play Bohemian Rhapsody` | Searches Jellyfin (default) |
| `/play jfsearch:Queen` | Searches Jellyfin (explicit) |
| `/play ytsearch:Never Gonna Give You Up` | Searches YouTube |
| `/play scsearch:Electronic Mix` | Searches SoundCloud |
| `/play https://youtube.com/watch?v=dQw4w9WgXcQ` | Plays URL directly |
---
## Supported Prefixes
| Prefix | Platform | SearchSource |
|--------|----------|--------------|
| `jfsearch:` | Jellyfin | `SearchSource.Jellyfin` |
| `ytsearch:` | YouTube | `SearchSource.YouTube` |
| `ytmsearch:` | YouTube Music | `SearchSource.YouTubeMusic` |
| `scsearch:` | SoundCloud | `SearchSource.SoundCloud` |
| `spsearch:` | Spotify | `SearchSource.Spotify` |
| `amsearch:` | Apple Music | `SearchSource.AppleMusic` |
| `dzsearch:` | Deezer | `SearchSource.Deezer` |
| `ymsearch:` | Yandex Music | `SearchSource.YandexMusic` |
---
## Requirements
- **.NET 8.0** or later
- **Lavalink4NET 4.x**
- **Lavalink Server** with appropriate plugins:
- [Jellylink](https://github.com/YourUsername/Jellylink) for Jellyfin support
- [LavaSrc](https://github.com/topi314/LavaSrc) for Spotify, Apple Music, Deezer support
---
## License
MIT License - see [LICENSE](LICENSE) for details.