add: Added .q support for invidious. If you have ytProvider set to invidious in data/searches.yml, invidious will be used to queue up songs and play them."work

This commit is contained in:
Kwoth
2024-09-18 13:26:34 +00:00
parent e4202b33f5
commit 0326e88910
25 changed files with 327 additions and 87 deletions

View File

@@ -8,5 +8,4 @@ public interface ITrackInfo
public string Thumbnail { get; }
public TimeSpan Duration { get; }
public MusicPlatform Platform { get; }
public ValueTask<string?> GetStreamUrl();
}

View File

@@ -4,8 +4,8 @@ namespace NadekoBot.Modules.Music;
public interface IYoutubeResolver : IPlatformQueryResolver
{
public Regex YtVideoIdRegex { get; }
public Task<ITrackInfo?> ResolveByIdAsync(string id);
IAsyncEnumerable<ITrackInfo> ResolveTracksFromPlaylistAsync(string query);
Task<ITrackInfo?> ResolveByQueryAsync(string query, bool tryExtractingId);
Task<string?> GetStreamUrl(string query);
}

View File

@@ -1,5 +1,6 @@
using NadekoBot.Voice;
using NadekoBot.Db.Models;
using NadekoBot.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<string?> 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);

View File

@@ -28,9 +28,6 @@ public sealed partial class MusicQueue
TrackInfo = trackInfo;
Queuer = queuer;
}
public ValueTask<string?> GetStreamUrl()
=> TrackInfo.GetStreamUrl();
}
}

View File

@@ -6,11 +6,4 @@ public sealed record RemoteTrackInfo(
string Url,
string Thumbnail,
TimeSpan Duration,
MusicPlatform Platform,
Func<Task<string?>> _streamFactory) : ITrackInfo
{
private readonly Func<Task<string?>> _streamFactory = _streamFactory;
public async ValueTask<string?> GetStreamUrl()
=> await _streamFactory();
}
MusicPlatform Platform) : ITrackInfo;

View File

@@ -24,7 +24,4 @@ public sealed class SimpleTrackInfo : ITrackInfo
Platform = platform;
StreamUrl = streamUrl;
}
public ValueTask<string?> GetStreamUrl()
=> new(StreamUrl);
}

View File

@@ -0,0 +1,12 @@
namespace NadekoBot.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; }
}

View File

@@ -0,0 +1,108 @@
using NadekoBot.Modules.Searches;
using System.Net.Http.Json;
namespace NadekoBot.Modules.Music;
public sealed class InvidiousYoutubeResolver : IYoutubeResolver
{
private readonly IHttpClientFactory _httpFactory;
private readonly SearchesConfigService _sc;
private readonly NadekoRandom _rng;
private string InvidiousApiUrl
=> _sc.Data.InvidiousInstances[_rng.Next(0, _sc.Data.InvidiousInstances.Count)];
public InvidiousYoutubeResolver(IHttpClientFactory httpFactory, SearchesConfigService sc)
{
_rng = new NadekoRandom();
_httpFactory = httpFactory;
_sc = sc;
}
public async Task<ITrackInfo?> ResolveByQueryAsync(string query)
{
using var http = _httpFactory.CreateClient();
var items = await http.GetFromJsonAsync<List<InvidiousSearchResponse>>(
$"{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<ITrackInfo?> ResolveByIdAsync(string id)
=> await InternalResolveByIdAsync(id);
private async Task<InvTrackInfo?> InternalResolveByIdAsync(string id)
{
using var http = _httpFactory.CreateClient();
var res = await http.GetFromJsonAsync<InvidiousVideoResponse>(
$"{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<ITrackInfo> ResolveTracksFromPlaylistAsync(string query)
{
using var http = _httpFactory.CreateClient();
var res = await http.GetFromJsonAsync<InvidiousPlaylistResponse>(
$"{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<ITrackInfo?> ResolveByQueryAsync(string query, bool tryExtractingId)
=> ResolveByQueryAsync(query);
public async Task<string?> GetStreamUrl(string videoId)
{
var video = await InternalResolveByIdAsync(videoId);
return video?.StreamUrl;
}
}

View File

@@ -1,13 +1,15 @@
namespace NadekoBot.Modules.Music;
using NadekoBot.Modules.Music.Resolvers;
namespace NadekoBot.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<ITrackInfo?>(null);

View File

@@ -0,0 +1,10 @@
using System.Text.RegularExpressions;
namespace NadekoBot.Modules.Music;
public sealed class YoutubeHelpers
{
public static Regex YtVideoIdRegex { get; } =
new(@"(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)(?<id>[a-zA-Z0-9_-]{6,11})",
RegexOptions.Compiled);
}

View File

@@ -0,0 +1,33 @@
using Microsoft.Extensions.DependencyInjection;
using NadekoBot.Modules.Searches;
using NadekoBot.Modules.Searches.Services;
namespace NadekoBot.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<InvidiousYoutubeResolver>();
}
return _services.GetRequiredService<YtdlYoutubeResolver>();
}
}

View File

@@ -16,9 +16,6 @@ public sealed class YtdlYoutubeResolver : IYoutubeResolver
private static readonly Regex _simplePlaylistRegex = new(@"&list=(?<id>[\w\-]{12,})", RegexOptions.Compiled);
public Regex YtVideoIdRegex { get; } =
new(@"(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)(?<id>[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<Task<string?>> 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<string?> GetStreamUrl(string videoId)
=> CreateCacherFactory(videoId)();
private readonly struct YtTrackData
{
public readonly string Title;