mirror of
https://github.com/Myxelium/Lavalink4NET.Jellyfin.git
synced 2026-04-09 02:39:39 +00:00
Init
This commit is contained in:
44
.gitignore
vendored
Normal file
44
.gitignore
vendored
Normal 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
22
LICENSE
Normal 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
22
Lavalink4NET.Jellyfin.sln
Normal 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
|
||||
39
Lavalink4NET.Jellyfin/JellyfinSearchMode.cs
Normal file
39
Lavalink4NET.Jellyfin/JellyfinSearchMode.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
29
Lavalink4NET.Jellyfin/Lavalink4NET.Jellyfin.csproj
Normal file
29
Lavalink4NET.Jellyfin/Lavalink4NET.Jellyfin.csproj
Normal 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>
|
||||
306
Lavalink4NET.Jellyfin/SearchQueryParser.cs
Normal file
306
Lavalink4NET.Jellyfin/SearchQueryParser.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
67
Lavalink4NET.Jellyfin/TrackLoadOptionsExtensions.cs
Normal file
67
Lavalink4NET.Jellyfin/TrackLoadOptionsExtensions.cs
Normal 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
450
README.md
Normal file
@@ -0,0 +1,450 @@
|
||||
# Lavalink4NET.Jellyfin
|
||||
|
||||
[](https://www.nuget.org/packages/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.
|
||||
|
||||
Reference in New Issue
Block a user