mirror of
https://github.com/Myxelium/Lavalink4NET.Jellyfin.git
synced 2026-04-09 10:49:37 +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