added youtube live support, but only if you have invidious instance (with a working api) set up in searches.yml
This commit is contained in:
parent
c8ea928de7
commit
7b4d4b1f8c
16 changed files with 345 additions and 90 deletions
src/EllieBot
Db/Models
Migrations
PostgreSql
20250315193825_fs-prettyname.sql20250315193847_init.Designer.cs20250315193847_init.csPostgreSqlContextModelSnapshot.cs
Sqlite
Modules
Searches
StreamNotification
_common
Xp
|
@ -1,4 +1,3 @@
|
||||||
#nullable disable
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
@ -18,9 +17,10 @@ public class FollowedStream
|
||||||
public int Id { get; set; }
|
public int Id { get; set; }
|
||||||
public ulong GuildId { get; set; }
|
public ulong GuildId { get; set; }
|
||||||
public ulong ChannelId { get; set; }
|
public ulong ChannelId { get; set; }
|
||||||
public string Username { get; set; }
|
public string Username { get; set; } = string.Empty;
|
||||||
|
public string? PrettyName { get; set; } = null;
|
||||||
public FType Type { get; set; }
|
public FType Type { get; set; }
|
||||||
public string Message { get; set; }
|
public string? Message { get; set; } = null;
|
||||||
|
|
||||||
protected bool Equals(FollowedStream other)
|
protected bool Equals(FollowedStream other)
|
||||||
=> ChannelId == other.ChannelId
|
=> ChannelId == other.ChannelId
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
START TRANSACTION;
|
||||||
|
ALTER TABLE discorduser DROP COLUMN notifyonlevelup;
|
||||||
|
|
||||||
|
ALTER TABLE followedstream ADD prettyname text;
|
||||||
|
|
||||||
|
INSERT INTO "__EFMigrationsHistory" (migrationid, productversion)
|
||||||
|
VALUES ('20250315193825_fs-prettyname', '9.0.1');
|
||||||
|
|
||||||
|
COMMIT;
|
|
@ -12,7 +12,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
namespace EllieBot.Migrations.PostgreSql
|
namespace EllieBot.Migrations.PostgreSql
|
||||||
{
|
{
|
||||||
[DbContext(typeof(PostgreSqlContext))]
|
[DbContext(typeof(PostgreSqlContext))]
|
||||||
[Migration("20250310143051_init")]
|
[Migration("20250315193847_init")]
|
||||||
partial class init
|
partial class init
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -837,12 +837,6 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
.HasDefaultValue(false)
|
.HasDefaultValue(false)
|
||||||
.HasColumnName("isclubadmin");
|
.HasColumnName("isclubadmin");
|
||||||
|
|
||||||
b.Property<int>("NotifyOnLevelUp")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer")
|
|
||||||
.HasDefaultValue(0)
|
|
||||||
.HasColumnName("notifyonlevelup");
|
|
||||||
|
|
||||||
b.Property<long>("TotalXp")
|
b.Property<long>("TotalXp")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("bigint")
|
.HasColumnType("bigint")
|
||||||
|
@ -1088,6 +1082,10 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
.HasColumnName("message");
|
.HasColumnName("message");
|
||||||
|
|
||||||
|
b.Property<string>("PrettyName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("prettyname");
|
||||||
|
|
||||||
b.Property<int>("Type")
|
b.Property<int>("Type")
|
||||||
.HasColumnType("integer")
|
.HasColumnType("integer")
|
||||||
.HasColumnName("type");
|
.HasColumnName("type");
|
|
@ -367,6 +367,7 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
guildid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||||
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
channelid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
|
||||||
username = table.Column<string>(type: "text", nullable: true),
|
username = table.Column<string>(type: "text", nullable: true),
|
||||||
|
prettyname = table.Column<string>(type: "text", nullable: true),
|
||||||
type = table.Column<int>(type: "integer", nullable: false),
|
type = table.Column<int>(type: "integer", nullable: false),
|
||||||
message = table.Column<string>(type: "text", nullable: true)
|
message = table.Column<string>(type: "text", nullable: true)
|
||||||
},
|
},
|
||||||
|
@ -1623,7 +1624,6 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
clubid = table.Column<int>(type: "integer", nullable: true),
|
clubid = table.Column<int>(type: "integer", nullable: true),
|
||||||
isclubadmin = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
|
isclubadmin = table.Column<bool>(type: "boolean", nullable: false, defaultValue: false),
|
||||||
totalxp = table.Column<long>(type: "bigint", nullable: false, defaultValue: 0L),
|
totalxp = table.Column<long>(type: "bigint", nullable: false, defaultValue: 0L),
|
||||||
notifyonlevelup = table.Column<int>(type: "integer", nullable: false, defaultValue: 0),
|
|
||||||
currencyamount = table.Column<long>(type: "bigint", nullable: false, defaultValue: 0L),
|
currencyamount = table.Column<long>(type: "bigint", nullable: false, defaultValue: 0L),
|
||||||
dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
|
dateadded = table.Column<DateTime>(type: "timestamp without time zone", nullable: true)
|
||||||
},
|
},
|
|
@ -834,12 +834,6 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
.HasDefaultValue(false)
|
.HasDefaultValue(false)
|
||||||
.HasColumnName("isclubadmin");
|
.HasColumnName("isclubadmin");
|
||||||
|
|
||||||
b.Property<int>("NotifyOnLevelUp")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer")
|
|
||||||
.HasDefaultValue(0)
|
|
||||||
.HasColumnName("notifyonlevelup");
|
|
||||||
|
|
||||||
b.Property<long>("TotalXp")
|
b.Property<long>("TotalXp")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("bigint")
|
.HasColumnType("bigint")
|
||||||
|
@ -1085,6 +1079,10 @@ namespace EllieBot.Migrations.PostgreSql
|
||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
.HasColumnName("message");
|
.HasColumnName("message");
|
||||||
|
|
||||||
|
b.Property<string>("PrettyName")
|
||||||
|
.HasColumnType("text")
|
||||||
|
.HasColumnName("prettyname");
|
||||||
|
|
||||||
b.Property<int>("Type")
|
b.Property<int>("Type")
|
||||||
.HasColumnType("integer")
|
.HasColumnType("integer")
|
||||||
.HasColumnName("type");
|
.HasColumnName("type");
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
BEGIN TRANSACTION;
|
||||||
|
ALTER TABLE discorduser DROP COLUMN notifyonlevelup;
|
||||||
|
|
||||||
|
ALTER TABLE "FollowedStream" ADD "PrettyName" TEXT NULL;
|
||||||
|
|
||||||
|
INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion")
|
||||||
|
VALUES ('20250315193822_fs-prettyname', '9.0.1');
|
||||||
|
|
||||||
|
COMMIT;
|
|
@ -11,7 +11,7 @@ using EllieBot.Db;
|
||||||
namespace EllieBot.Migrations.Sqlite
|
namespace EllieBot.Migrations.Sqlite
|
||||||
{
|
{
|
||||||
[DbContext(typeof(SqliteContext))]
|
[DbContext(typeof(SqliteContext))]
|
||||||
[Migration("20250310143048_init")]
|
[Migration("20250315193844_init")]
|
||||||
partial class init
|
partial class init
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -627,11 +627,6 @@ namespace EllieBot.Migrations.Sqlite
|
||||||
.HasColumnType("INTEGER")
|
.HasColumnType("INTEGER")
|
||||||
.HasDefaultValue(false);
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
b.Property<int>("NotifyOnLevelUp")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(0);
|
|
||||||
|
|
||||||
b.Property<long>("TotalXp")
|
b.Property<long>("TotalXp")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER")
|
.HasColumnType("INTEGER")
|
||||||
|
@ -812,6 +807,9 @@ namespace EllieBot.Migrations.Sqlite
|
||||||
b.Property<string>("Message")
|
b.Property<string>("Message")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PrettyName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<int>("Type")
|
b.Property<int>("Type")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
|
@ -366,6 +366,7 @@ namespace EllieBot.Migrations.Sqlite
|
||||||
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
GuildId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||||
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
ChannelId = table.Column<ulong>(type: "INTEGER", nullable: false),
|
||||||
Username = table.Column<string>(type: "TEXT", nullable: true),
|
Username = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
PrettyName = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
Type = table.Column<int>(type: "INTEGER", nullable: false),
|
Type = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
Message = table.Column<string>(type: "TEXT", nullable: true)
|
Message = table.Column<string>(type: "TEXT", nullable: true)
|
||||||
},
|
},
|
||||||
|
@ -1625,7 +1626,6 @@ namespace EllieBot.Migrations.Sqlite
|
||||||
ClubId = table.Column<int>(type: "INTEGER", nullable: true),
|
ClubId = table.Column<int>(type: "INTEGER", nullable: true),
|
||||||
IsClubAdmin = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
|
IsClubAdmin = table.Column<bool>(type: "INTEGER", nullable: false, defaultValue: false),
|
||||||
TotalXp = table.Column<long>(type: "INTEGER", nullable: false, defaultValue: 0L),
|
TotalXp = table.Column<long>(type: "INTEGER", nullable: false, defaultValue: 0L),
|
||||||
NotifyOnLevelUp = table.Column<int>(type: "INTEGER", nullable: false, defaultValue: 0),
|
|
||||||
CurrencyAmount = table.Column<long>(type: "INTEGER", nullable: false, defaultValue: 0L),
|
CurrencyAmount = table.Column<long>(type: "INTEGER", nullable: false, defaultValue: 0L),
|
||||||
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
|
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
|
||||||
},
|
},
|
|
@ -624,11 +624,6 @@ namespace EllieBot.Migrations.Sqlite
|
||||||
.HasColumnType("INTEGER")
|
.HasColumnType("INTEGER")
|
||||||
.HasDefaultValue(false);
|
.HasDefaultValue(false);
|
||||||
|
|
||||||
b.Property<int>("NotifyOnLevelUp")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("INTEGER")
|
|
||||||
.HasDefaultValue(0);
|
|
||||||
|
|
||||||
b.Property<long>("TotalXp")
|
b.Property<long>("TotalXp")
|
||||||
.ValueGeneratedOnAdd()
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("INTEGER")
|
.HasColumnType("INTEGER")
|
||||||
|
@ -809,6 +804,9 @@ namespace EllieBot.Migrations.Sqlite
|
||||||
b.Property<string>("Message")
|
b.Property<string>("Message")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("PrettyName")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<int>("Type")
|
b.Property<int>("Type")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
|
|
@ -83,7 +83,7 @@ public partial class Searches
|
||||||
for (var index = 0; index < elements.Count; index++)
|
for (var index = 0; index < elements.Count; index++)
|
||||||
{
|
{
|
||||||
var elem = elements[index];
|
var elem = elements[index];
|
||||||
eb.AddField($"**#{index + 1 + (12 * cur)}** {elem.Username.ToLower()}",
|
eb.AddField($"**#{index + 1 + (12 * cur)}** {(elem.PrettyName ?? elem.Username).ToLower()}",
|
||||||
$"【{elem.Type}】\n<#{elem.ChannelId}>\n{elem.Message?.TrimTo(50)}",
|
$"【{elem.Type}】\n<#{elem.ChannelId}>\n{elem.Message?.TrimTo(50)}",
|
||||||
true);
|
true);
|
||||||
}
|
}
|
||||||
|
@ -176,9 +176,9 @@ public partial class Searches
|
||||||
|
|
||||||
if (data.IsLive)
|
if (data.IsLive)
|
||||||
{
|
{
|
||||||
|
var embed = _service.GetEmbed(ctx.Guild.Id, data, false);
|
||||||
await Response()
|
await Response()
|
||||||
.Confirm(strs.streamer_online(Format.Bold(data.Name),
|
.Embed(embed)
|
||||||
Format.Bold(data.Viewers.ToString())))
|
|
||||||
.SendAsync();
|
.SendAsync();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
|
@ -63,7 +63,7 @@ public sealed class StreamNotificationService : IEService, IReadyExecutor
|
||||||
_repSvc = repSvc;
|
_repSvc = repSvc;
|
||||||
_shardData = shardData;
|
_shardData = shardData;
|
||||||
|
|
||||||
_streamTracker = new(httpFactory, creds);
|
_streamTracker = new(httpFactory, creds, config);
|
||||||
|
|
||||||
StreamsOnlineKey = new("streams.online");
|
StreamsOnlineKey = new("streams.online");
|
||||||
StreamsOfflineKey = new("streams.offline");
|
StreamsOfflineKey = new("streams.offline");
|
||||||
|
@ -123,11 +123,11 @@ public sealed class StreamNotificationService : IEService, IReadyExecutor
|
||||||
_shardTrackedStreams = followedStreams.GroupBy(x => new
|
_shardTrackedStreams = followedStreams.GroupBy(x => new
|
||||||
{
|
{
|
||||||
x.Type,
|
x.Type,
|
||||||
Name = x.Username.ToLower()
|
Name = x.Username
|
||||||
})
|
})
|
||||||
.ToList()
|
.ToList()
|
||||||
.ToDictionary(
|
.ToDictionary(
|
||||||
x => new StreamDataKey(x.Key.Type, x.Key.Name.ToLower()),
|
x => new StreamDataKey(x.Key.Type, x.Key.Name),
|
||||||
x => x.GroupBy(y => y.GuildId)
|
x => x.GroupBy(y => y.GuildId)
|
||||||
.ToDictionary(y => y.Key,
|
.ToDictionary(y => y.Key,
|
||||||
y => y.AsEnumerable().ToHashSet()));
|
y => y.AsEnumerable().ToHashSet()));
|
||||||
|
@ -143,7 +143,7 @@ public sealed class StreamNotificationService : IEService, IReadyExecutor
|
||||||
_trackCounter = allFollowedStreams.GroupBy(x => new
|
_trackCounter = allFollowedStreams.GroupBy(x => new
|
||||||
{
|
{
|
||||||
x.Type,
|
x.Type,
|
||||||
Name = x.Username.ToLower()
|
Name = x.Username
|
||||||
})
|
})
|
||||||
.ToDictionary(x => new StreamDataKey(x.Key.Type, x.Key.Name),
|
.ToDictionary(x => new StreamDataKey(x.Key.Type, x.Key.Name),
|
||||||
x => x.Select(fs => fs.GuildId).ToHashSet());
|
x => x.Select(fs => fs.GuildId).ToHashSet());
|
||||||
|
@ -478,6 +478,7 @@ public sealed class StreamNotificationService : IEService, IReadyExecutor
|
||||||
{
|
{
|
||||||
Type = data.StreamType,
|
Type = data.StreamType,
|
||||||
Username = data.UniqueName,
|
Username = data.UniqueName,
|
||||||
|
PrettyName = data.Name,
|
||||||
ChannelId = channelId,
|
ChannelId = channelId,
|
||||||
GuildId = guildId
|
GuildId = guildId
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,5 +5,5 @@ namespace EllieBot.Modules.Searches.Common;
|
||||||
public static class Extensions
|
public static class Extensions
|
||||||
{
|
{
|
||||||
public static StreamDataKey CreateKey(this FollowedStream fs)
|
public static StreamDataKey CreateKey(this FollowedStream fs)
|
||||||
=> new(fs.Type, fs.Username.ToLower());
|
=> new(fs.Type, fs.Username);
|
||||||
}
|
}
|
|
@ -14,13 +14,15 @@ public class NotifChecker
|
||||||
|
|
||||||
public NotifChecker(
|
public NotifChecker(
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
IBotCredsProvider credsProvider)
|
IBotCredsProvider credsProvider,
|
||||||
|
SearchesConfigService scs)
|
||||||
{
|
{
|
||||||
_streamProviders = new Dictionary<FollowedStream.FType, Provider>()
|
_streamProviders = new Dictionary<FollowedStream.FType, Provider>()
|
||||||
{
|
{
|
||||||
{ FollowedStream.FType.Twitch, new TwitchHelixProvider(httpClientFactory, credsProvider) },
|
{ FollowedStream.FType.Twitch, new TwitchHelixProvider(httpClientFactory, credsProvider) },
|
||||||
{ FollowedStream.FType.Picarto, new PicartoProvider(httpClientFactory) },
|
{ FollowedStream.FType.Picarto, new PicartoProvider(httpClientFactory) },
|
||||||
{ FollowedStream.FType.Trovo, new TrovoProvider(httpClientFactory, credsProvider) }
|
{ FollowedStream.FType.Trovo, new TrovoProvider(httpClientFactory, credsProvider) },
|
||||||
|
{ FollowedStream.FType.Youtube, new YouTubeProvider(httpClientFactory, scs) }
|
||||||
};
|
};
|
||||||
_offlineBuffer = new();
|
_offlineBuffer = new();
|
||||||
}
|
}
|
||||||
|
@ -144,6 +146,7 @@ public class NotifChecker
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Error(ex, "Error getting stream notifications: {ErrorMessage}", ex.Message);
|
Log.Error(ex, "Error getting stream notifications: {ErrorMessage}", ex.Message);
|
||||||
|
await Task.Delay(15_000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -37,6 +37,7 @@ public abstract class Provider
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="url">Url of the stream</param>
|
/// <param name="url">Url of the stream</param>
|
||||||
/// <returns><see cref="StreamData" /> of the specified stream. Null if none found</returns>
|
/// <returns><see cref="StreamData" /> of the specified stream. Null if none found</returns>
|
||||||
|
|
||||||
public abstract Task<StreamData?> GetStreamDataByUrlAsync(string url);
|
public abstract Task<StreamData?> GetStreamDataByUrlAsync(string url);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
@ -0,0 +1,230 @@
|
||||||
|
using System.Net;
|
||||||
|
using EllieBot.Db.Models;
|
||||||
|
using EllieBot.Services;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
using AngleSharp.Browser;
|
||||||
|
|
||||||
|
namespace EllieBot.Modules.Searches.Common.StreamNotifications.Providers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provider for tracking YouTube livestreams
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class YouTubeProvider : Provider
|
||||||
|
{
|
||||||
|
private readonly IHttpClientFactory _httpFactory;
|
||||||
|
private readonly SearchesConfigService _scs;
|
||||||
|
private readonly EllieRandom _rng = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Regex to match YouTube handles
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Regex</returns>
|
||||||
|
[GeneratedRegex(@"youtu(?:\.be|be\.com)\/@(?<handle>[^\/\?#]+)")]
|
||||||
|
private static partial Regex HandleRegex();
|
||||||
|
|
||||||
|
// channel id regex
|
||||||
|
[GeneratedRegex(@"youtu(?:\.be|be\.com)\/channel\/(?<channelid>[^\/\?#]+)")]
|
||||||
|
private static partial Regex ChannelIdRegex();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Type of the platform.
|
||||||
|
/// </summary>
|
||||||
|
public override FollowedStream.FType Platform
|
||||||
|
=> FollowedStream.FType.Youtube;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="YouTubeProvider"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="httpFactory">The HTTP client factory to create HTTP clients.</param>
|
||||||
|
public YouTubeProvider(
|
||||||
|
IHttpClientFactory httpFactory,
|
||||||
|
SearchesConfigService scs
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_httpFactory = httpFactory;
|
||||||
|
_scs = scs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether the specified url is a valid YouTube url.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">Url to check</param>
|
||||||
|
/// <returns>True if valid, otherwise false</returns>
|
||||||
|
public override Task<bool> IsValidUrl(string url)
|
||||||
|
{
|
||||||
|
var success = HandleRegex().IsMatch(url)
|
||||||
|
|| ChannelIdRegex().IsMatch(url);
|
||||||
|
|
||||||
|
return Task.FromResult(success);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets stream data of the stream on the specified YouTube url
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="url">Url of the stream</param>
|
||||||
|
/// <returns><see cref="StreamData"/> of the specified stream. Null if none found</returns>
|
||||||
|
public override async Task<StreamData?> GetStreamDataByUrlAsync(string url)
|
||||||
|
{
|
||||||
|
var match = ChannelIdRegex().Match(url);
|
||||||
|
var channelId = string.Empty;
|
||||||
|
if (!match.Success)
|
||||||
|
{
|
||||||
|
var handleMatch = HandleRegex().Match(url);
|
||||||
|
if (!handleMatch.Success)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var handle = handleMatch.Groups["handle"].Value;
|
||||||
|
|
||||||
|
var instances = _scs.Data.InvidiousInstances;
|
||||||
|
|
||||||
|
if (instances is not { Count: > 0 })
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var invInstance = instances[_rng.Next(0, _scs.Data.InvidiousInstances.Count)];
|
||||||
|
|
||||||
|
using var client = _httpFactory.CreateClient();
|
||||||
|
client.BaseAddress = new Uri(invInstance);
|
||||||
|
|
||||||
|
using var response = await client.GetAsync($"/@{handle}");
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
channelId = response.RequestMessage?.RequestUri?.ToString().Split("/").LastOrDefault();
|
||||||
|
|
||||||
|
if (channelId is null)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
channelId = match.Groups["channelid"].Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await GetStreamDataAsync(channelId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets stream data of the specified YouTube channel
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="channelId">Channel ID or name</param>
|
||||||
|
/// <returns><see cref="StreamData"/> of the channel. Null if none found</returns>
|
||||||
|
public override async Task<StreamData?> GetStreamDataAsync(string channelId)
|
||||||
|
{
|
||||||
|
var instances = _scs.Data.InvidiousInstances;
|
||||||
|
|
||||||
|
if (instances is not { Count: > 0 })
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var invInstance = instances[_rng.Next(0, instances.Count)];
|
||||||
|
var client = _httpFactory.CreateClient();
|
||||||
|
client.BaseAddress = new Uri(invInstance);
|
||||||
|
|
||||||
|
var channel = await client.GetFromJsonAsync<InvidiousChannelResponse>($"/api/v1/channels/{channelId}");
|
||||||
|
if (channel is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var response =
|
||||||
|
await client.GetFromJsonAsync<InvChannelStreamsResponse>($"/api/v1/channels/{channelId}/streams");
|
||||||
|
if (response is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var vid = response.Videos.FirstOrDefault(x => !x.IsUpcoming && x.LengthSeconds == 0);
|
||||||
|
var isLive = false;
|
||||||
|
if (vid is null)
|
||||||
|
{
|
||||||
|
vid = response.Videos.FirstOrDefault(x => !x.IsUpcoming);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
isLive = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var avatarUrl = channel?.AuthorThumbnails?.Select(x => x.Url).LastOrDefault();
|
||||||
|
|
||||||
|
return new StreamData()
|
||||||
|
{
|
||||||
|
Game = "Livestream",
|
||||||
|
Name = vid.Author,
|
||||||
|
Preview = vid.Thumbnails
|
||||||
|
.Skip(1)
|
||||||
|
.Select(x => "https://i.ytimg.com/" + x.Url)
|
||||||
|
.FirstOrDefault(),
|
||||||
|
Title = vid.Title,
|
||||||
|
Viewers = vid.ViewCount,
|
||||||
|
AvatarUrl = avatarUrl,
|
||||||
|
IsLive = isLive,
|
||||||
|
StreamType = FollowedStream.FType.Youtube,
|
||||||
|
StreamUrl = "https://youtube.com/watch?v=" + vid.VideoId,
|
||||||
|
UniqueName = vid.AuthorId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets stream data of all specified YouTube channels
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="channelIds">List of channel IDs or names</param>
|
||||||
|
/// <returns><see cref="StreamData"/> of all users, in the same order. Null for every ID not found.</returns>
|
||||||
|
public override async Task<IReadOnlyCollection<StreamData>> GetStreamDataAsync(List<string> channelIds)
|
||||||
|
{
|
||||||
|
var results = new List<StreamData>(channelIds.Count);
|
||||||
|
foreach (var group in channelIds.Chunk(5))
|
||||||
|
{
|
||||||
|
var streamData = await Task.WhenAll(group.Select(GetStreamDataAsync));
|
||||||
|
|
||||||
|
foreach (var data in streamData)
|
||||||
|
results.Add(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class InvidiousChannelResponse
|
||||||
|
{
|
||||||
|
[JsonPropertyName("authorId")]
|
||||||
|
public required string AuthorId { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("authorThumbnails")]
|
||||||
|
public required List<InvAuthorThumbnail> AuthorThumbnails { get; init; }
|
||||||
|
|
||||||
|
public sealed class InvAuthorThumbnail
|
||||||
|
{
|
||||||
|
[JsonPropertyName("url")]
|
||||||
|
public required string Url { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class InvChannelStreamsResponse
|
||||||
|
{
|
||||||
|
public required List<InvidiousStreamResponse> Videos { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class InvidiousStreamResponse
|
||||||
|
{
|
||||||
|
[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<InvidiousThumbnail> Thumbnails { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("author")]
|
||||||
|
public required string Author { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("authorId")]
|
||||||
|
public required string AuthorId { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("isUpcoming")]
|
||||||
|
public bool IsUpcoming { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("viewCount")]
|
||||||
|
public int ViewCount { get; set; }
|
||||||
|
}
|
|
@ -40,9 +40,19 @@ public partial class Xp : EllieModule<XpService>
|
||||||
_templateService = templateService;
|
_templateService = templateService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// [Cmd]
|
||||||
|
// [RequireContext(ContextType.Guild)]
|
||||||
|
// public async Task ExperienceText([Leftover] IUser? user = null)
|
||||||
|
// {
|
||||||
|
// user ??= ctx.User;
|
||||||
|
// var xp = await _service.GetUserStatsAsync((IGuildUser)user);
|
||||||
|
// await ctx.Channel.TriggerTypingAsync();
|
||||||
|
// await ctx.Channel.SendMessageAsync(_templateService.GetXpText(xp));
|
||||||
|
// }
|
||||||
|
|
||||||
[Cmd]
|
[Cmd]
|
||||||
[RequireContext(ContextType.Guild)]
|
[RequireContext(ContextType.Guild)]
|
||||||
public async Task Experience([Leftover] IUser user = null)
|
public async Task Experience([Leftover] IUser? user = null)
|
||||||
{
|
{
|
||||||
user ??= ctx.User;
|
user ??= ctx.User;
|
||||||
await ctx.Channel.TriggerTypingAsync();
|
await ctx.Channel.TriggerTypingAsync();
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue