diff --git a/src/EllieBot/EllieBot.csproj b/src/EllieBot/EllieBot.csproj
index ebcd4b2..9bfce54 100644
--- a/src/EllieBot/EllieBot.csproj
+++ b/src/EllieBot/EllieBot.csproj
@@ -37,6 +37,7 @@
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/src/EllieBot/Modules/Gambling/GamblingConfig.cs b/src/EllieBot/Modules/Gambling/GamblingConfig.cs
index 2571890..7b6582c 100644
--- a/src/EllieBot/Modules/Gambling/GamblingConfig.cs
+++ b/src/EllieBot/Modules/Gambling/GamblingConfig.cs
@@ -274,8 +274,8 @@ public sealed partial class WaifuConfig
public class WaifuDecayConfig
{
[Comment("""
- Percentage (0 - 100) of the waifu value to reduce.
- Set 0 to disable
+ Unclaimed waifus will decay by this percentage (0 - 100).
+ Default is 0 (disabled)
For example if a waifu has a price of 500$, setting this value to 10 would reduce the waifu value by 10% (50$)
""")]
public int UnclaimedDecayPercent { get; set; } = 0;
@@ -283,6 +283,7 @@ public sealed partial class WaifuConfig
[Comment("""
Claimed waifus will decay by this percentage (0 - 100).
Default is 0 (disabled)
+ For example if a waifu has a price of $500, setting this value to 10 would reduce the waifu by 10% ($50)
""")]
public int ClaimedDecayPercent { get; set; } = 0;
diff --git a/src/EllieBot/Modules/Music/PlaylistCommands.cs b/src/EllieBot/Modules/Music/PlaylistCommands.cs
index 0e4137b..d9cc5d4 100644
--- a/src/EllieBot/Modules/Music/PlaylistCommands.cs
+++ b/src/EllieBot/Modules/Music/PlaylistCommands.cs
@@ -130,13 +130,14 @@ public sealed partial class Music
return;
}
+ // todo check locally queued songs
var songs = mp.GetQueuedTracks()
.Select(s => new PlaylistSong
{
Provider = s.Platform.ToString(),
ProviderType = (MusicType)s.Platform,
Title = s.Title,
- Query = s.Platform == MusicPlatform.Local ? s.GetStreamUrl().Result!.Trim('"') : s.Url
+ Query = s.Url
})
.ToList();
diff --git a/src/EllieBot/Modules/Music/Services/MusicService.cs b/src/EllieBot/Modules/Music/Services/MusicService.cs
index 8495211..89c848d 100644
--- a/src/EllieBot/Modules/Music/Services/MusicService.cs
+++ b/src/EllieBot/Modules/Music/Services/MusicService.cs
@@ -1,4 +1,5 @@
using EllieBot.Db.Models;
+using EllieBot.Modules.Music.Resolvers;
using System.Diagnostics.CodeAnalysis;
namespace EllieBot.Modules.Music.Services;
@@ -8,7 +9,7 @@ public sealed class MusicService : IMusicService, IPlaceholderProvider
private readonly AyuVoiceStateService _voiceStateService;
private readonly ITrackResolveProvider _trackResolveProvider;
private readonly DbService _db;
- private readonly IYoutubeResolver _ytResolver;
+ private readonly IYoutubeResolverFactory _ytResolver;
private readonly ILocalTrackResolver _localResolver;
private readonly DiscordSocketClient _client;
private readonly IBotStrings _strings;
@@ -24,7 +25,7 @@ public sealed class MusicService : IMusicService, IPlaceholderProvider
AyuVoiceStateService voiceStateService,
ITrackResolveProvider trackResolveProvider,
DbService db,
- IYoutubeResolver ytResolver,
+ IYoutubeResolverFactory ytResolver,
ILocalTrackResolver localResolver,
DiscordSocketClient client,
IBotStrings strings,
@@ -93,7 +94,7 @@ public sealed class MusicService : IMusicService, IPlaceholderProvider
public async Task EnqueueYoutubePlaylistAsync(IMusicPlayer mp, string query, string queuer)
{
var count = 0;
- await foreach (var track in _ytResolver.ResolveTracksFromPlaylistAsync(query))
+ await foreach (var track in _ytResolver.GetYoutubeResolver().ResolveTracksFromPlaylistAsync(query))
{
if (mp.IsKilled)
break;
@@ -139,6 +140,7 @@ public sealed class MusicService : IMusicService, IPlaceholderProvider
var mp = new MusicPlayer(queue,
resolver,
+ _ytResolver,
proxy,
_googleApiService,
settings.QualityPreset,
diff --git a/src/EllieBot/Modules/Music/_common/ITrackInfo.cs b/src/EllieBot/Modules/Music/_common/ITrackInfo.cs
index 347e8fa..3525b59 100644
--- a/src/EllieBot/Modules/Music/_common/ITrackInfo.cs
+++ b/src/EllieBot/Modules/Music/_common/ITrackInfo.cs
@@ -8,5 +8,4 @@ public interface ITrackInfo
public string Thumbnail { get; }
public TimeSpan Duration { get; }
public MusicPlatform Platform { get; }
- public ValueTask GetStreamUrl();
}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Music/_common/IYoutubeResolver.cs b/src/EllieBot/Modules/Music/_common/IYoutubeResolver.cs
index 433012d..e4c2f53 100644
--- a/src/EllieBot/Modules/Music/_common/IYoutubeResolver.cs
+++ b/src/EllieBot/Modules/Music/_common/IYoutubeResolver.cs
@@ -4,8 +4,8 @@ namespace EllieBot.Modules.Music;
public interface IYoutubeResolver : IPlatformQueryResolver
{
- public Regex YtVideoIdRegex { get; }
public Task ResolveByIdAsync(string id);
IAsyncEnumerable ResolveTracksFromPlaylistAsync(string query);
Task ResolveByQueryAsync(string query, bool tryExtractingId);
+ Task GetStreamUrl(string query);
}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Music/_common/Impl/MusicPlayer.cs b/src/EllieBot/Modules/Music/_common/Impl/MusicPlayer.cs
index 6819d4e..0edd3cf 100644
--- a/src/EllieBot/Modules/Music/_common/Impl/MusicPlayer.cs
+++ b/src/EllieBot/Modules/Music/_common/Impl/MusicPlayer.cs
@@ -1,5 +1,6 @@
using EllieBot.Voice;
using EllieBot.Db.Models;
+using EllieBot.Modules.Music.Resolvers;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.CompilerServices;
@@ -27,6 +28,7 @@ public sealed class MusicPlayer : IMusicPlayer
private readonly IMusicQueue _queue;
private readonly ITrackResolveProvider _trackResolveProvider;
+ private readonly IYoutubeResolverFactory _ytResolverFactory;
private readonly IVoiceProxy _proxy;
private readonly IGoogleApiService _googleApiService;
private readonly ISongBuffer _songBuffer;
@@ -41,6 +43,7 @@ public sealed class MusicPlayer : IMusicPlayer
public MusicPlayer(
IMusicQueue queue,
ITrackResolveProvider trackResolveProvider,
+ IYoutubeResolverFactory ytResolverFactory,
IVoiceProxy proxy,
IGoogleApiService googleApiService,
QualityPreset qualityPreset,
@@ -48,6 +51,7 @@ public sealed class MusicPlayer : IMusicPlayer
{
_queue = queue;
_trackResolveProvider = trackResolveProvider;
+ _ytResolverFactory = ytResolverFactory;
_proxy = proxy;
_googleApiService = googleApiService;
AutoPlay = autoPlay;
@@ -118,7 +122,7 @@ public sealed class MusicPlayer : IMusicPlayer
// make sure song buffer is ready to be (re)used
_songBuffer.Reset();
- var streamUrl = await track.GetStreamUrl();
+ var streamUrl = await GetStreamUrl(track);
// start up the data source
using var source = FfmpegTrackDataSource.CreateAsync(
_vc.BitDepth,
@@ -256,6 +260,7 @@ public sealed class MusicPlayer : IMusicPlayer
IsStopped = true;
Log.Error("Please install ffmpeg and make sure it's added to your "
+ "PATH environment variable before trying again");
+
}
catch (OperationCanceledException)
{
@@ -264,6 +269,7 @@ public sealed class MusicPlayer : IMusicPlayer
catch (Exception ex)
{
Log.Error(ex, "Unknown error in music loop: {ErrorMessage}", ex.Message);
+ await Task.Delay(3_000);
}
finally
{
@@ -303,6 +309,14 @@ public sealed class MusicPlayer : IMusicPlayer
}
}
+ private async Task GetStreamUrl(IQueuedTrackInfo track)
+ {
+ if (track.TrackInfo is SimpleTrackInfo sti)
+ return sti.StreamUrl;
+
+ return await _ytResolverFactory.GetYoutubeResolver().GetStreamUrl(track.TrackInfo.Id);
+ }
+
private bool? CopyChunkToOutput(ISongBuffer sb, VoiceClient vc)
{
var data = sb.Read(vc.InputLength, out var length);
@@ -406,19 +420,19 @@ public sealed class MusicPlayer : IMusicPlayer
break;
await chunk.Select(async data =>
+ {
+ var (query, platform) = data;
+ try
{
- var (query, platform) = data;
- try
- {
- await TryEnqueueTrackAsync(query, queuer, false, platform);
- errorCount = 0;
- }
- catch (Exception ex)
- {
- Log.Warning(ex, "Error resolving {MusicPlatform} Track {TrackQuery}", platform, query);
- ++errorCount;
- }
- })
+ await TryEnqueueTrackAsync(query, queuer, false, platform);
+ errorCount = 0;
+ }
+ catch (Exception ex)
+ {
+ Log.Warning(ex, "Error resolving {MusicPlatform} Track {TrackQuery}", platform, query);
+ ++errorCount;
+ }
+ })
.WhenAll();
await Task.Delay(1000);
diff --git a/src/EllieBot/Modules/Music/_common/Impl/MusicQueue.cs b/src/EllieBot/Modules/Music/_common/Impl/MusicQueue.cs
index 1b1ce9c..9b1c7aa 100644
--- a/src/EllieBot/Modules/Music/_common/Impl/MusicQueue.cs
+++ b/src/EllieBot/Modules/Music/_common/Impl/MusicQueue.cs
@@ -28,9 +28,6 @@ public sealed partial class MusicQueue
TrackInfo = trackInfo;
Queuer = queuer;
}
-
- public ValueTask GetStreamUrl()
- => TrackInfo.GetStreamUrl();
}
}
diff --git a/src/EllieBot/Modules/Music/_common/Impl/RemoteTrackInfo.cs b/src/EllieBot/Modules/Music/_common/Impl/RemoteTrackInfo.cs
index b002779..c6fbf10 100644
--- a/src/EllieBot/Modules/Music/_common/Impl/RemoteTrackInfo.cs
+++ b/src/EllieBot/Modules/Music/_common/Impl/RemoteTrackInfo.cs
@@ -6,11 +6,5 @@ public sealed record RemoteTrackInfo(
string Url,
string Thumbnail,
TimeSpan Duration,
- MusicPlatform Platform,
- Func> _streamFactory) : ITrackInfo
-{
- private readonly Func> _streamFactory = _streamFactory;
- public async ValueTask GetStreamUrl()
- => await _streamFactory();
-}
\ No newline at end of file
+ MusicPlatform Platform) : ITrackInfo;
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Music/_common/Impl/SimpleTrackInfo.cs b/src/EllieBot/Modules/Music/_common/Impl/SimpleTrackInfo.cs
index 9ae1c30..c7828cc 100644
--- a/src/EllieBot/Modules/Music/_common/Impl/SimpleTrackInfo.cs
+++ b/src/EllieBot/Modules/Music/_common/Impl/SimpleTrackInfo.cs
@@ -24,7 +24,4 @@ public sealed class SimpleTrackInfo : ITrackInfo
Platform = platform;
StreamUrl = streamUrl;
}
-
- public ValueTask GetStreamUrl()
- => new(StreamUrl);
}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Music/_common/Resolvers/InvTrackInfo.cs b/src/EllieBot/Modules/Music/_common/Resolvers/InvTrackInfo.cs
new file mode 100644
index 0000000..cca2727
--- /dev/null
+++ b/src/EllieBot/Modules/Music/_common/Resolvers/InvTrackInfo.cs
@@ -0,0 +1,12 @@
+namespace EllieBot.Modules.Music;
+
+public sealed class InvTrackInfo : ITrackInfo
+{
+ public required string Id { get; init; }
+ public required string Title { get; init; }
+ public required string Url { get; init; }
+ public required string Thumbnail { get; init; }
+ public required TimeSpan Duration { get; init; }
+ public required MusicPlatform Platform { get; init; }
+ public required string? StreamUrl { get; init; }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Music/_common/Resolvers/InvidiousYoutubeResolver.cs b/src/EllieBot/Modules/Music/_common/Resolvers/InvidiousYoutubeResolver.cs
new file mode 100644
index 0000000..243fd13
--- /dev/null
+++ b/src/EllieBot/Modules/Music/_common/Resolvers/InvidiousYoutubeResolver.cs
@@ -0,0 +1,108 @@
+using EllieBot.Modules.Searches;
+using System.Net.Http.Json;
+
+namespace EllieBot.Modules.Music;
+
+public sealed class InvidiousYoutubeResolver : IYoutubeResolver
+{
+ private readonly IHttpClientFactory _httpFactory;
+ private readonly SearchesConfigService _sc;
+ private readonly EllieRandom _rng;
+
+ private string InvidiousApiUrl
+ => _sc.Data.InvidiousInstances[_rng.Next(0, _sc.Data.InvidiousInstances.Count)];
+
+ public InvidiousYoutubeResolver(IHttpClientFactory httpFactory, SearchesConfigService sc)
+ {
+ _rng = new EllieRandom();
+ _httpFactory = httpFactory;
+ _sc = sc;
+ }
+
+ public async Task ResolveByQueryAsync(string query)
+ {
+ using var http = _httpFactory.CreateClient();
+
+ var items = await http.GetFromJsonAsync>(
+ $"{InvidiousApiUrl}/api/v1/search"
+ + $"?q={query}"
+ + $"&type=video");
+
+ if (items is null || items.Count == 0)
+ return null;
+
+
+ var res = items.First();
+
+ return new InvTrackInfo()
+ {
+ Id = res.VideoId,
+ Title = res.Title,
+ Url = $"https://youtube.com/watch?v={res.VideoId}",
+ Thumbnail = res.Thumbnails?.Select(x => x.Url).FirstOrDefault() ?? string.Empty,
+ Duration = TimeSpan.FromSeconds(res.LengthSeconds),
+ Platform = MusicPlatform.Youtube,
+ StreamUrl = null,
+ };
+ }
+
+ public async Task ResolveByIdAsync(string id)
+ => await InternalResolveByIdAsync(id);
+
+ private async Task InternalResolveByIdAsync(string id)
+ {
+ using var http = _httpFactory.CreateClient();
+
+ var res = await http.GetFromJsonAsync(
+ $"{InvidiousApiUrl}/api/v1/videos/{id}");
+
+ if (res is null)
+ return null;
+
+ return new InvTrackInfo()
+ {
+ Id = res.VideoId,
+ Title = res.Title,
+ Url = $"https://youtube.com/watch?v={res.VideoId}",
+ Thumbnail = res.Thumbnails?.Select(x => x.Url).FirstOrDefault() ?? string.Empty,
+ Duration = TimeSpan.FromSeconds(res.LengthSeconds),
+ Platform = MusicPlatform.Youtube,
+ StreamUrl = res.AdaptiveFormats.FirstOrDefault(x => x.AudioQuality == "AUDIO_QUALITY_HIGH")?.Url
+ ?? res.AdaptiveFormats.FirstOrDefault(x => x.AudioQuality == "AUDIO_QUALITY_MEDIUM")?.Url
+ ?? res.AdaptiveFormats.FirstOrDefault(x => x.AudioQuality == "AUDIO_QUALITY_LOW")?.Url
+ };
+ }
+
+ public async IAsyncEnumerable ResolveTracksFromPlaylistAsync(string query)
+ {
+ using var http = _httpFactory.CreateClient();
+ var res = await http.GetFromJsonAsync(
+ $"{InvidiousApiUrl}/api/v1/search?type=video&q={query}");
+
+ if (res is null)
+ yield break;
+
+ foreach (var video in res.Videos)
+ {
+ yield return new InvTrackInfo()
+ {
+ Id = video.VideoId,
+ Title = video.Title,
+ Url = $"https://youtube.com/watch?v={video.VideoId}",
+ Thumbnail = video.Thumbnails?.Select(x => x.Url).FirstOrDefault() ?? string.Empty,
+ Duration = TimeSpan.FromSeconds(video.LengthSeconds),
+ Platform = MusicPlatform.Youtube,
+ StreamUrl = null
+ };
+ }
+ }
+
+ public Task ResolveByQueryAsync(string query, bool tryExtractingId)
+ => ResolveByQueryAsync(query);
+
+ public async Task GetStreamUrl(string videoId)
+ {
+ var video = await InternalResolveByIdAsync(videoId);
+ return video?.StreamUrl;
+ }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Music/_common/Resolvers/TrackResolveProvider.cs b/src/EllieBot/Modules/Music/_common/Resolvers/TrackResolveProvider.cs
index 642edf1..21a6adf 100644
--- a/src/EllieBot/Modules/Music/_common/Resolvers/TrackResolveProvider.cs
+++ b/src/EllieBot/Modules/Music/_common/Resolvers/TrackResolveProvider.cs
@@ -1,13 +1,15 @@
-namespace EllieBot.Modules.Music;
+using EllieBot.Modules.Music.Resolvers;
+
+namespace EllieBot.Modules.Music;
public sealed class TrackResolveProvider : ITrackResolveProvider
{
- private readonly IYoutubeResolver _ytResolver;
+ private readonly IYoutubeResolverFactory _ytResolver;
private readonly ILocalTrackResolver _localResolver;
private readonly IRadioResolver _radioResolver;
public TrackResolveProvider(
- IYoutubeResolver ytResolver,
+ IYoutubeResolverFactory ytResolver,
ILocalTrackResolver localResolver,
IRadioResolver radioResolver)
{
@@ -23,19 +25,22 @@ public sealed class TrackResolveProvider : ITrackResolveProvider
case MusicPlatform.Radio:
return _radioResolver.ResolveByQueryAsync(query);
case MusicPlatform.Youtube:
- return _ytResolver.ResolveByQueryAsync(query);
+ return _ytResolver.GetYoutubeResolver().ResolveByQueryAsync(query);
case MusicPlatform.Local:
return _localResolver.ResolveByQueryAsync(query);
case null:
- var match = _ytResolver.YtVideoIdRegex.Match(query);
+ var match = YoutubeHelpers.YtVideoIdRegex.Match(query);
+
if (match.Success)
- return _ytResolver.ResolveByIdAsync(match.Groups["id"].Value);
- else if (Uri.TryCreate(query, UriKind.Absolute, out var uri) && uri.IsFile)
+ return _ytResolver.GetYoutubeResolver().ResolveByIdAsync(match.Groups["id"].Value);
+
+ if (Uri.TryCreate(query, UriKind.Absolute, out var uri) && uri.IsFile)
return _localResolver.ResolveByQueryAsync(uri.AbsolutePath);
- else if (IsRadioLink(query))
+
+ if (IsRadioLink(query))
return _radioResolver.ResolveByQueryAsync(query);
- else
- return _ytResolver.ResolveByQueryAsync(query, false);
+
+ return _ytResolver.GetYoutubeResolver().ResolveByQueryAsync(query, false);
default:
Log.Error("Unsupported platform: {MusicPlatform}", forcePlatform);
return Task.FromResult(null);
diff --git a/src/EllieBot/Modules/Music/_common/Resolvers/YoutubeHelpers.cs b/src/EllieBot/Modules/Music/_common/Resolvers/YoutubeHelpers.cs
new file mode 100644
index 0000000..869102d
--- /dev/null
+++ b/src/EllieBot/Modules/Music/_common/Resolvers/YoutubeHelpers.cs
@@ -0,0 +1,10 @@
+using System.Text.RegularExpressions;
+
+namespace EllieBot.Modules.Music;
+
+public sealed class YoutubeHelpers
+{
+ public static Regex YtVideoIdRegex { get; } =
+ new(@"(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)(?[a-zA-Z0-9_-]{6,11})",
+ RegexOptions.Compiled);
+}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Music/_common/Resolvers/YoutubeResolverFactory.cs b/src/EllieBot/Modules/Music/_common/Resolvers/YoutubeResolverFactory.cs
new file mode 100644
index 0000000..955faca
--- /dev/null
+++ b/src/EllieBot/Modules/Music/_common/Resolvers/YoutubeResolverFactory.cs
@@ -0,0 +1,33 @@
+using Microsoft.Extensions.DependencyInjection;
+using EllieBot.Modules.Searches;
+using EllieBot.Modules.Searches.Services;
+
+namespace EllieBot.Modules.Music.Resolvers;
+
+public interface IYoutubeResolverFactory
+{
+ IYoutubeResolver GetYoutubeResolver();
+}
+
+public sealed class YoutubeResolverFactory : IYoutubeResolverFactory
+{
+ private readonly SearchesConfigService _ss;
+ private readonly IServiceProvider _services;
+
+ public YoutubeResolverFactory(SearchesConfigService ss, IServiceProvider services)
+ {
+ _ss = ss;
+ _services = services;
+ }
+
+ public IYoutubeResolver GetYoutubeResolver()
+ {
+ var conf = _ss.Data;
+ if (conf.YtProvider == YoutubeSearcher.Invidious)
+ {
+ return _services.GetRequiredService();
+ }
+
+ return _services.GetRequiredService();
+ }
+}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Music/_common/Resolvers/YtdlYoutubeResolver.cs b/src/EllieBot/Modules/Music/_common/Resolvers/YtdlYoutubeResolver.cs
index 70479d0..eeb3a1c 100644
--- a/src/EllieBot/Modules/Music/_common/Resolvers/YtdlYoutubeResolver.cs
+++ b/src/EllieBot/Modules/Music/_common/Resolvers/YtdlYoutubeResolver.cs
@@ -16,9 +16,6 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
private static readonly Regex _simplePlaylistRegex = new(@"&list=(?[\w\-]{12,})", RegexOptions.Compiled);
- public Regex YtVideoIdRegex { get; } =
- new(@"(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)(?[a-zA-Z0-9_-]{6,11})",
- RegexOptions.Compiled);
private readonly ITrackCacher _trackCacher;
@@ -32,7 +29,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
{
_trackCacher = trackCacher;
_google = google;
-
+
_ytdlPlaylistOperation = new("-4 "
+ "--geo-bypass "
@@ -46,7 +43,8 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
+ "--no-check-certificate "
+ "-i "
+ "--yes-playlist "
- + "-- \"{0}\"", scs.Data.YtProvider != YoutubeSearcher.Ytdl);
+ + "-- \"{0}\"",
+ scs.Data.YtProvider != YoutubeSearcher.Ytdl);
_ytdlIdOperation = new("-4 "
+ "--geo-bypass "
@@ -58,7 +56,8 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
+ "--get-thumbnail "
+ "--get-duration "
+ "--no-check-certificate "
- + "-- \"{0}\"", scs.Data.YtProvider != YoutubeSearcher.Ytdl);
+ + "-- \"{0}\"",
+ scs.Data.YtProvider != YoutubeSearcher.Ytdl);
_ytdlSearchOperation = new("-4 "
+ "--geo-bypass "
@@ -71,7 +70,8 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
+ "--get-duration "
+ "--no-check-certificate "
+ "--default-search "
- + "\"ytsearch:\" -- \"{0}\"", scs.Data.YtProvider != YoutubeSearcher.Ytdl);
+ + "\"ytsearch:\" -- \"{0}\"",
+ scs.Data.YtProvider != YoutubeSearcher.Ytdl);
}
private YtTrackData ResolveYtdlData(string ytdlOutputString)
@@ -102,8 +102,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
$"https://youtube.com/watch?v={trackData.Id}",
trackData.Thumbnail,
trackData.Duration,
- MusicPlatform.Youtube,
- CreateCacherFactory(trackData.Id));
+ MusicPlatform.Youtube);
private Func> CreateCacherFactory(string id)
=> () => _trackCacher.GetOrCreateStreamLink(id,
@@ -268,7 +267,7 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
{
if (tryResolving)
{
- var match = YtVideoIdRegex.Match(query);
+ var match = YoutubeHelpers.YtVideoIdRegex.Match(query);
if (match.Success)
return await ResolveByIdAsync(match.Groups["id"].Value);
}
@@ -290,6 +289,8 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
return DataToInfo(new(cachedData.Title, cachedData.Id, cachedData.Thumbnail, null, cachedData.Duration));
}
+ public Task GetStreamUrl(string videoId)
+ => CreateCacherFactory(videoId)();
private readonly struct YtTrackData
{
public readonly string Title;
diff --git a/src/EllieBot/Modules/Searches/Search/SearchCommands.cs b/src/EllieBot/Modules/Searches/Search/SearchCommands.cs
index 411600c..b155c75 100644
--- a/src/EllieBot/Modules/Searches/Search/SearchCommands.cs
+++ b/src/EllieBot/Modules/Searches/Search/SearchCommands.cs
@@ -60,21 +60,21 @@ public partial class Searches
descStr = descStr.TrimTo(4096);
var embed = _sender.CreateEmbed()
- .WithOkColor()
- .WithAuthor(ctx.User)
- .WithTitle(query.TrimTo(64)!)
- .WithDescription(descStr)
- .WithFooter(
- GetText(strs.results_in(data.Info.TotalResults, data.Info.SearchTime)),
- "https://i.imgur.com/G46fm8J.png");
+ .WithOkColor()
+ .WithAuthor(ctx.User)
+ .WithTitle(query.TrimTo(64)!)
+ .WithDescription(descStr)
+ .WithFooter(
+ GetText(strs.results_in(data.Info.TotalResults, data.Info.SearchTime)),
+ "https://i.imgur.com/G46fm8J.png");
await Response().Embed(embed).SendAsync();
}
[Cmd]
- public async Task Image([Leftover] string? query)
+ public async Task Image([Leftover] string query)
{
- query = query?.Trim();
+ query = query.Trim();
if (string.IsNullOrWhiteSpace(query))
{
@@ -99,29 +99,32 @@ public partial class Searches
EmbedBuilder CreateEmbed(IImageSearchResultEntry entry)
{
return _sender.CreateEmbed()
- .WithOkColor()
- .WithAuthor(ctx.User)
- .WithTitle(query)
- .WithUrl("https://google.com")
- .WithImageUrl(entry.Link);
+ .WithOkColor()
+ .WithAuthor(ctx.User)
+ .WithTitle(query)
+ .WithUrl("https://google.com")
+ .WithImageUrl(entry.Link);
}
- embeds.Add(CreateEmbed(data.Entries.First())
- .WithFooter(
- GetText(strs.results_in(data.Info.TotalResults, data.Info.SearchTime)),
- "https://i.imgur.com/G46fm8J.png"));
+ await Response()
+ .Paginated()
+ .Items(data.Entries)
+ .PageSize(1)
+ .AddFooter(false)
+ .Page((items, _) =>
+ {
+ var item = items.FirstOrDefault();
- var random = data.Entries.Skip(1)
- .Shuffle()
- .Take(3)
- .ToArray();
+ if (item is null)
+ return _sender.CreateEmbed()
+ .WithDescription(GetText(strs.no_search_results));
- foreach (var entry in random)
- {
- embeds.Add(CreateEmbed(entry));
- }
+ var embed = CreateEmbed(item);
+ embeds.Add(embed);
- await Response().Embeds(embeds).SendAsync();
+ return embed;
+ })
+ .SendAsync();
}
private TypedKey GetYtCacheKey(string query)
@@ -158,7 +161,7 @@ public partial class Searches
var maybeResult = await GetYoutubeUrlFromCacheAsync(query)
?? await _searchFactory.GetYoutubeSearchService().SearchAsync(query);
- if (maybeResult is not {} result || result is {Url: null})
+ if (maybeResult is not { } result || result is { Url: null })
{
await Response().Error(strs.no_results).SendAsync();
return;
diff --git a/src/EllieBot/Modules/Searches/Search/Youtube/InvidiousSearchResponse.cs b/src/EllieBot/Modules/Searches/Search/Youtube/InvidiousSearchResponse.cs
index 9951db8..9b18d0c 100644
--- a/src/EllieBot/Modules/Searches/Search/Youtube/InvidiousSearchResponse.cs
+++ b/src/EllieBot/Modules/Searches/Search/Youtube/InvidiousSearchResponse.cs
@@ -5,5 +5,59 @@ namespace EllieBot.Modules.Searches;
public sealed class InvidiousSearchResponse
{
[JsonPropertyName("videoId")]
- public string VideoId { get; set; } = null!;
+ public required string VideoId { get; init; }
+
+ [JsonPropertyName("title")]
+ public required string Title { get; init; }
+
+ [JsonPropertyName("videoThumbnails")]
+ public required List Thumbnails { get; init; }
+
+ [JsonPropertyName("lengthSeconds")]
+ public required int LengthSeconds { get; init; }
+
+ [JsonPropertyName("description")]
+ public required string Description { get; init; }
+}
+
+public sealed class InvidiousVideoResponse
+{
+ [JsonPropertyName("title")]
+ public required string Title { get; init; }
+
+ [JsonPropertyName("videoId")]
+ public required string VideoId { get; init; }
+
+ [JsonPropertyName("lengthSeconds")]
+ public required int LengthSeconds { get; init; }
+
+ [JsonPropertyName("videoThumbnails")]
+ public required List Thumbnails { get; init; }
+
+ [JsonPropertyName("adaptiveFormats")]
+ public required List AdaptiveFormats { get; init; }
+}
+
+public sealed class InvidiousAdaptiveFormat
+{
+ [JsonPropertyName("url")]
+ public required string Url { get; init; }
+
+ [JsonPropertyName("audioQuality")]
+ public string? AudioQuality { get; init; }
+}
+
+public sealed class InvidiousPlaylistResponse
+{
+ [JsonPropertyName("title")]
+ public required string Title { get; init; }
+
+ [JsonPropertyName("videos")]
+ public required List Videos { get; init; }
+}
+
+public sealed class InvidiousThumbnail
+{
+ [JsonPropertyName("url")]
+ public required string Url { get; init; }
}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Searches/Search/Youtube/InvidiousYtSearchService.cs b/src/EllieBot/Modules/Searches/Search/Youtube/InvidiousYtSearchService.cs
index 6fc8bac..17bd519 100644
--- a/src/EllieBot/Modules/Searches/Search/Youtube/InvidiousYtSearchService.cs
+++ b/src/EllieBot/Modules/Searches/Search/Youtube/InvidiousYtSearchService.cs
@@ -32,13 +32,14 @@ public sealed class InvidiousYtSearchService : IYoutubeSearchService, IEService
var instance = instances[_rng.Next(0, instances.Count)];
+ var url = $"{instance}/api/v1/search"
+ + $"?q={query}"
+ + $"&type=video";
using var http = _http.CreateClient();
var res = await http.GetFromJsonAsync>(
- $"{instance}/api/v1/search"
- + $"?q={query}"
- + $"&type=video");
+ url);
- if (res is null or {Count: 0})
+ if (res is null or { Count: 0 })
return null;
return new VideoInfo(res[0].VideoId);
diff --git a/src/EllieBot/Modules/Searches/_common/Config/SearchesConfig.cs b/src/EllieBot/Modules/Searches/_common/Config/SearchesConfig.cs
index fb64849..8cb0227 100644
--- a/src/EllieBot/Modules/Searches/_common/Config/SearchesConfig.cs
+++ b/src/EllieBot/Modules/Searches/_common/Config/SearchesConfig.cs
@@ -7,7 +7,7 @@ namespace EllieBot.Modules.Searches;
public partial class SearchesConfig : ICloneable
{
[Comment("DO NOT CHANGE")]
- public int Version { get; set; } = 0;
+ public int Version { get; set; } = 3;
[Comment("""
Which engine should .search command
@@ -26,7 +26,7 @@ public partial class SearchesConfig : ICloneable
[Comment("""
- Which search provider will be used for the `.youtube` command.
+ Which search provider will be used for the `.youtube` and `.q` commands.
- `ytDataApiv3` - uses google's official youtube data api. Requires `GoogleApiKey` set in creds and youtube data api enabled in developers console
@@ -58,7 +58,6 @@ public partial class SearchesConfig : ICloneable
[Comment("""
Set the invidious instance urls in case you want to use 'invidious' for `.youtube` search
Ellie will use a random one for each request.
- These instances may be used for music queue functionality in the future.
Use a fully qualified url. Example: https://my-invidious-instance.mydomain.com
Instances specified must have api available.
diff --git a/src/EllieBot/Modules/Searches/_common/Config/SearchesConfigService.cs b/src/EllieBot/Modules/Searches/_common/Config/SearchesConfigService.cs
index f656a5b..39ae3c6 100644
--- a/src/EllieBot/Modules/Searches/_common/Config/SearchesConfigService.cs
+++ b/src/EllieBot/Modules/Searches/_common/Config/SearchesConfigService.cs
@@ -54,5 +54,13 @@ public class SearchesConfigService : ConfigServiceBase
c.Version = 2;
});
}
+
+ if (data.Version < 3)
+ {
+ ModifyConfig(c =>
+ {
+ c.Version = 3;
+ });
+ }
}
}
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Xp/Xp.cs b/src/EllieBot/Modules/Xp/Xp.cs
index 63e59e3..e961d87 100644
--- a/src/EllieBot/Modules/Xp/Xp.cs
+++ b/src/EllieBot/Modules/Xp/Xp.cs
@@ -445,9 +445,10 @@ public partial class Xp : EllieModule
if (!string.IsNullOrWhiteSpace(item.Desc))
eb.AddField(GetText(strs.desc), item.Desc);
+#if GLOBAL_ELLIE
if (key == "default")
eb.WithDescription(GetText(strs.xpshop_website));
-
+#endif
var tier = _service.GetXpShopTierRequirement(type);
if (tier != PatronTier.None)
diff --git a/src/EllieBot/_common/Replacements/Impl/ReplacementContext.cs b/src/EllieBot/_common/Replacements/Impl/ReplacementContext.cs
index 184ba4e..4e87ce9 100644
--- a/src/EllieBot/_common/Replacements/Impl/ReplacementContext.cs
+++ b/src/EllieBot/_common/Replacements/Impl/ReplacementContext.cs
@@ -54,7 +54,6 @@ public sealed class ReplacementContext
public ReplacementContext WithOverride(string key, Func repFactory)
=> WithOverride(key, () => new ValueTask(repFactory()));
-
public ReplacementContext WithOverride(Regex regex, Func> repFactory)
{
if (_regexPatterns.Add(regex.ToString()))
diff --git a/src/EllieBot/_common/ServiceCollectionExtensions.cs b/src/EllieBot/_common/ServiceCollectionExtensions.cs
index 3a59226..c5539af 100644
--- a/src/EllieBot/_common/ServiceCollectionExtensions.cs
+++ b/src/EllieBot/_common/ServiceCollectionExtensions.cs
@@ -51,7 +51,9 @@ public static class ServiceCollectionExtensions
svcs.RegisterMany(Reuse.Singleton);
svcs.AddSingleton();
- svcs.AddSingleton();
+ svcs.AddSingleton();
+ svcs.AddSingleton();
+ svcs.AddSingleton();
svcs.AddSingleton();
svcs.AddSingleton();
svcs.AddSingleton();
diff --git a/src/EllieBot/data/searches.yml b/src/EllieBot/data/searches.yml
index 9b7ab5c..f551bae 100644
--- a/src/EllieBot/data/searches.yml
+++ b/src/EllieBot/data/searches.yml
@@ -1,5 +1,5 @@
# DO NOT CHANGE
-version: 2
+version: 3
# Which engine should .search command
# 'google_scrape' - default. Scrapes the webpage for results. May break. Requires no api keys.
# 'google' - official google api. Requires googleApiKey and google.searchId set in creds.yml
@@ -9,14 +9,14 @@ webSearchEngine: Google_Scrape
# 'google'- official google api. googleApiKey and google.imageSearchId set in creds.yml
# 'searx' requires at least one searx instance specified in the 'searxInstances' property below
imgSearchEngine: Google
-# Which search provider will be used for the `.youtube` command.
-#
+# Which search provider will be used for the `.youtube` and `.q` commands.
+#
# - `ytDataApiv3` - uses google's official youtube data api. Requires `GoogleApiKey` set in creds and youtube data api enabled in developers console
-#
+#
# - `ytdl` - default, uses youtube-dl. Requires `youtube-dl` to be installed and it's path added to env variables. Slow.
-#
+#
# - `ytdlp` - recommended easy, uses `yt-dlp`. Requires `yt-dlp` to be installed and it's path added to env variables
-#
+#
# - `invidious` - recommended advanced, uses invidious api. Requires at least one invidious instance specified in the `invidiousInstances` property
ytProvider: Ytdlp
# Set the searx instance urls in case you want to use 'searx' for either img or web search.
@@ -24,20 +24,19 @@ ytProvider: Ytdlp
# Use a fully qualified url. Example: `https://my-searx-instance.mydomain.com`
# Instances specified must support 'format=json' query parameter.
# - In case you're running your own searx instance, set
-#
+#
# search:
# formats:
# - json
-#
+#
# in 'searxng/settings.yml' on your server
-#
+#
# - If you're using a public instance, make sure that the instance you're using supports it (they usually don't)
searxInstances: []
# Set the invidious instance urls in case you want to use 'invidious' for `.youtube` search
# Ellie will use a random one for each request.
-# These instances may be used for music queue functionality in the future.
# Use a fully qualified url. Example: https://my-invidious-instance.mydomain.com
-#
+#
# Instances specified must have api available.
# You check that by opening an api endpoint in your browser. For example: https://my-invidious-instance.mydomain.com/api/v1/trending
invidiousInstances: []