mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-12 10:18:27 -04:00
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:
@@ -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; }
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
@@ -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);
|
||||
}
|
@@ -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>();
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
Reference in New Issue
Block a user