Initial implementation of trovo.live notifications support for .stadd

This commit is contained in:
Kwoth
2022-01-31 00:47:37 +01:00
parent cd812304f7
commit 0f36242597
19 changed files with 318 additions and 84 deletions

View File

@@ -6,7 +6,8 @@ public static class CommandNameLoadHelper
{
private static readonly IDeserializer _deserializer = new Deserializer();
public static Lazy<Dictionary<string, string[]>> LazyCommandAliases = new(() => LoadAliases());
private static readonly Lazy<Dictionary<string, string[]>> _lazyCommandAliases
= new(() => LoadAliases());
public static Dictionary<string, string[]> LoadAliases(string aliasesFilePath = "data/aliases.yml")
{
@@ -15,14 +16,14 @@ public static class CommandNameLoadHelper
}
public static string[] GetAliasesFor(string methodName)
=> LazyCommandAliases.Value.TryGetValue(methodName.ToLowerInvariant(), out var aliases) && aliases.Length > 1
=> _lazyCommandAliases.Value.TryGetValue(methodName.ToLowerInvariant(), out var aliases) && aliases.Length > 1
? aliases.Skip(1).ToArray()
: Array.Empty<string>();
public static string GetCommandNameFor(string methodName)
{
methodName = methodName.ToLowerInvariant();
var toReturn = LazyCommandAliases.Value.TryGetValue(methodName, out var aliases) && aliases.Length > 0
var toReturn = _lazyCommandAliases.Value.TryGetValue(methodName, out var aliases) && aliases.Length > 0
? aliases[0]
: methodName;
return toReturn;

View File

@@ -7,7 +7,7 @@ public sealed class OwnerOnlyAttribute : PreconditionAttribute
{
public override Task<PreconditionResult> CheckPermissionsAsync(
ICommandContext context,
CommandInfo executingCommand,
CommandInfo command,
IServiceProvider services)
{
var creds = services.GetRequiredService<IBotCredsProvider>().GetCreds();

View File

@@ -1,5 +1,6 @@
#nullable disable
#pragma warning disable all
#pragma warning disable
#pragma warning disable *
// License MIT
// Source: https://github.com/i3arnon/ConcurrentHashSet
@@ -332,16 +333,16 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
/// <returns>true if the <see cref="ConcurrentHashSet{T}" /> contains the item; otherwise, false.</returns>
public bool Contains(T item)
{
var hashcode = _comparer.GetHashCode(item);
var hashcode = _comparer.GetHashCode(item!);
// We must capture the _buckets field in a local variable. It is set to a new table on each table resize.
var tables = this.tables;
var localTables = this.tables;
var bucketNo = GetBucket(hashcode, tables.Buckets.Length);
var bucketNo = GetBucket(hashcode, localTables.Buckets.Length);
// We can get away w/out a lock here.
// The Volatile.Read ensures that the load of the fields of 'n' doesn't move before the load from buckets[i].
var current = Volatile.Read(ref tables.Buckets[bucketNo]);
var current = Volatile.Read(ref localTables.Buckets[bucketNo]);
while (current is not null)
{
@@ -444,29 +445,29 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
var hashcode = _comparer.GetHashCode(item);
while (true)
{
var tables = this.tables;
var localTables = this.tables;
GetBucketAndLockNo(hashcode, out var bucketNo, out var lockNo, tables.Buckets.Length, tables.Locks.Length);
GetBucketAndLockNo(hashcode, out var bucketNo, out var lockNo, localTables.Buckets.Length, localTables.Locks.Length);
lock (tables.Locks[lockNo])
lock (localTables.Locks[lockNo])
{
// If the table just got resized, we may not be holding the right lock, and must retry.
// This should be a rare occurrence.
if (tables != this.tables) continue;
if (localTables != this.tables) continue;
Node previous = null;
for (var current = tables.Buckets[bucketNo]; current is not null; current = current.Next)
for (var current = localTables.Buckets[bucketNo]; current is not null; current = current.Next)
{
Debug.Assert((previous is null && current == tables.Buckets[bucketNo]) || previous.Next == current);
Debug.Assert((previous is null && current == localTables.Buckets[bucketNo]) || previous!.Next == current);
if (hashcode == current.Hashcode && _comparer.Equals(current.Item, item))
{
if (previous is null)
Volatile.Write(ref tables.Buckets[bucketNo], current.Next);
Volatile.Write(ref localTables.Buckets[bucketNo], current.Next);
else
previous.Next = current.Next;
tables.CountPerLock[lockNo]--;
localTables.CountPerLock[lockNo]--;
return true;
}
@@ -489,25 +490,25 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
{
while (true)
{
var tables = this.tables;
GetBucketAndLockNo(hashcode, out var bucketNo, out var lockNo, tables.Buckets.Length, tables.Locks.Length);
var localTables = this.tables;
GetBucketAndLockNo(hashcode, out var bucketNo, out var lockNo, localTables.Buckets.Length, localTables.Locks.Length);
var resizeDesired = false;
var lockTaken = false;
try
{
if (acquireLock)
Monitor.Enter(tables.Locks[lockNo], ref lockTaken);
Monitor.Enter(localTables.Locks[lockNo], ref lockTaken);
// If the table just got resized, we may not be holding the right lock, and must retry.
// This should be a rare occurrence.
if (tables != this.tables) continue;
if (localTables != this.tables) continue;
// Try to find this item in the bucket
Node previous = null;
for (var current = tables.Buckets[bucketNo]; current is not null; current = current.Next)
for (var current = localTables.Buckets[bucketNo]; current is not null; current = current.Next)
{
Debug.Assert((previous is null && current == tables.Buckets[bucketNo]) || previous.Next == current);
Debug.Assert((previous is null && current == localTables.Buckets[bucketNo]) || previous!.Next == current);
if (hashcode == current.Hashcode && _comparer.Equals(current.Item, item))
return false;
@@ -515,10 +516,10 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
}
// The item was not found in the bucket. Insert the new item.
Volatile.Write(ref tables.Buckets[bucketNo], new(item, hashcode, tables.Buckets[bucketNo]));
Volatile.Write(ref localTables.Buckets[bucketNo], new(item, hashcode, localTables.Buckets[bucketNo]));
checked
{
tables.CountPerLock[lockNo]++;
localTables.CountPerLock[lockNo]++;
}
//
@@ -526,12 +527,12 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
// It is also possible that GrowTable will increase the budget but won't resize the bucket table.
// That happens if the bucket table is found to be poorly utilized due to a bad hash function.
//
if (tables.CountPerLock[lockNo] > budget) resizeDesired = true;
if (localTables.CountPerLock[lockNo] > budget) resizeDesired = true;
}
finally
{
if (lockTaken)
Monitor.Exit(tables.Locks[lockNo]);
Monitor.Exit(localTables.Locks[lockNo]);
}
//
@@ -542,7 +543,7 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
// - As a result, it is possible that GrowTable will be called unnecessarily. But, GrowTable will obtain lock 0
// and then verify that the table we passed to it as the argument is still the current table.
//
if (resizeDesired) GrowTable(tables);
if (resizeDesired) GrowTable(localTables);
return true;
}
@@ -569,7 +570,7 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
Debug.Assert(lockNo >= 0 && lockNo < lockCount);
}
private void GrowTable(Tables tables)
private void GrowTable(Tables localTables)
{
const int maxArrayLength = 0X7FEFFFFF;
var locksAcquired = 0;
@@ -579,7 +580,7 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
AcquireLocks(0, 1, ref locksAcquired);
// Make sure nobody resized the table while we were waiting for lock 0:
if (tables != this.tables)
if (localTables != this.tables)
// We assume that since the table reference is different, it was already resized (or the budget
// was adjusted). If we ever decide to do table shrinking, or replace the table for other reasons,
// we will have to revisit this logic.
@@ -587,12 +588,12 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
// Compute the (approx.) total size. Use an Int64 accumulation variable to avoid an overflow.
long approxCount = 0;
for (var i = 0; i < tables.CountPerLock.Length; i++) approxCount += tables.CountPerLock[i];
for (var i = 0; i < localTables.CountPerLock.Length; i++) approxCount += localTables.CountPerLock[i];
//
// If the bucket array is too empty, double the budget instead of resizing the table
//
if (approxCount < tables.Buckets.Length / 4)
if (approxCount < localTables.Buckets.Length / 4)
{
budget = 2 * budget;
if (budget < 0) budget = int.MaxValue;
@@ -609,7 +610,7 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
checked
{
// Double the size of the buckets table and add one, so that we have an odd integer.
newLength = (tables.Buckets.Length * 2) + 1;
newLength = (localTables.Buckets.Length * 2) + 1;
// Now, we only need to check odd integers, and find the first that is not divisible
// by 3, 5 or 7.
@@ -638,25 +639,25 @@ public sealed class ConcurrentHashSet<T> : IReadOnlyCollection<T>, ICollection<T
}
// Now acquire all other locks for the table
AcquireLocks(1, tables.Locks.Length, ref locksAcquired);
AcquireLocks(1, localTables.Locks.Length, ref locksAcquired);
var newLocks = tables.Locks;
var newLocks = localTables.Locks;
// Add more locks
if (_growLockArray && tables.Locks.Length < MAX_LOCK_NUMBER)
if (_growLockArray && localTables.Locks.Length < MAX_LOCK_NUMBER)
{
newLocks = new object[tables.Locks.Length * 2];
Array.Copy(tables.Locks, 0, newLocks, 0, tables.Locks.Length);
for (var i = tables.Locks.Length; i < newLocks.Length; i++) newLocks[i] = new();
newLocks = new object[localTables.Locks.Length * 2];
Array.Copy(localTables.Locks, 0, newLocks, 0, localTables.Locks.Length);
for (var i = localTables.Locks.Length; i < newLocks.Length; i++) newLocks[i] = new();
}
var newBuckets = new Node[newLength];
var newCountPerLock = new int[newLocks.Length];
// Copy all data into a new table, creating new nodes for all elements
for (var i = 0; i < tables.Buckets.Length; i++)
for (var i = 0; i < localTables.Buckets.Length; i++)
{
var current = tables.Buckets[i];
var current = localTables.Buckets[i];
while (current is not null)
{
var next = current.Next;

View File

@@ -42,8 +42,8 @@ public class IndexedCollection<T> : IList<T>
}
}
public int IndexOf([NotNull] T item)
=> item.Index;
public int IndexOf(T item)
=> item?.Index ?? -1;
public IEnumerator<T> GetEnumerator()
=> Source.GetEnumerator();

View File

@@ -67,6 +67,10 @@ Used for cryptocurrency related commands.")]
[Comment(@"Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api")]
public string OsuApiKey { get; set; }
[Comment(@"Optional Trovo client id.
You should only use this if Trovo notifications stopped working or you're getting ratelimit errors.")]
public string TrovoClientId { get; set; }
[Comment(@"Command and args which will be used to restart the bot.
Only used if bot is executed directly (NOT through the coordinator)
placeholders:
@@ -188,11 +192,11 @@ This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsett
public int TotalShards { get; set; } = 1;
public string PatreonAccessToken { get; set; } = string.Empty;
public string PatreonCampaignId { get; set; } = "334038";
public RestartConfig RestartCommand { get; set; } = null;
public RestartConfig RestartCommand { get; set; }
public string ShardRunCommand { get; set; } = string.Empty;
public string ShardRunArguments { get; set; } = string.Empty;
public int? ShardRunPort { get; set; } = null;
public int? ShardRunPort { get; set; }
public string MiningProxyUrl { get; set; } = string.Empty;
public string MiningProxyCreds { get; set; } = string.Empty;

View File

@@ -20,6 +20,7 @@ public interface IBotCredentials
string LocationIqApiKey { get; }
string TimezoneDbApiKey { get; }
string CoinmarketcapApiKey { get; }
string TrovoClientId { get; }
string CoordinatorUrl { get; set; }
}

View File

@@ -11,7 +11,8 @@ public class FollowedStream : DbEntity
Twitch = 0,
Picarto = 3,
Youtube = 4,
Facebook = 5
Facebook = 5,
Trovo = 6
}
public ulong GuildId { get; set; }

View File

@@ -314,12 +314,10 @@ public partial class Administration : NadekoModule<AdministrationService>
}
if (time is null)
{
await msg.DeleteAsync();
}
else if (time.Time <= TimeSpan.FromDays(7))
{
_= Task.Run(async () =>
_ = Task.Run(async () =>
{
await Task.Delay(time.Time);
await msg.DeleteAsync();

View File

@@ -39,7 +39,7 @@ public sealed class StreamNotificationService : INService
DiscordSocketClient client,
IBotStrings strings,
ConnectionMultiplexer redis,
IBotCredentials creds,
IBotCredsProvider creds,
IHttpClientFactory httpFactory,
Bot bot,
IPubSub pubSub,
@@ -50,7 +50,7 @@ public sealed class StreamNotificationService : INService
_strings = strings;
_pubSub = pubSub;
_eb = eb;
_streamTracker = new(httpFactory, redis, creds.RedisKey(), client.ShardId == 0);
_streamTracker = new(httpFactory, creds, redis, creds.GetCreds().RedisKey(), client.ShardId == 0);
_streamsOnlineKey = new("streams.online");
_streamsOfflineKey = new("streams.offline");

View File

@@ -0,0 +1,61 @@
#nullable disable
using System.Text.Json.Serialization;
namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
public class TrovoGetUsersResponse
{
[JsonPropertyName("is_live")]
public bool IsLive { get; set; }
[JsonPropertyName("category_id")]
public string CategoryId { get; set; }
[JsonPropertyName("category_name")]
public string CategoryName { get; set; }
[JsonPropertyName("live_title")]
public string LiveTitle { get; set; }
[JsonPropertyName("audi_type")]
public string AudiType { get; set; }
[JsonPropertyName("language_code")]
public string LanguageCode { get; set; }
[JsonPropertyName("thumbnail")]
public string Thumbnail { get; set; }
[JsonPropertyName("current_viewers")]
public int CurrentViewers { get; set; }
[JsonPropertyName("followers")]
public int Followers { get; set; }
[JsonPropertyName("streamer_info")]
public string StreamerInfo { get; set; }
[JsonPropertyName("profile_pic")]
public string ProfilePic { get; set; }
[JsonPropertyName("channel_url")]
public string ChannelUrl { get; set; }
[JsonPropertyName("created_at")]
public string CreatedAt { get; set; }
[JsonPropertyName("subscriber_num")]
public int SubscriberNum { get; set; }
[JsonPropertyName("username")]
public string Username { get; set; }
[JsonPropertyName("social_links")]
public List<TrovoSocialLink> SocialLinks { get; set; }
[JsonPropertyName("started_at")]
public string StartedAt { get; set; }
[JsonPropertyName("ended_at")]
public string EndedAt { get; set; }
}

View File

@@ -0,0 +1,10 @@
#nullable disable
using System.Text.Json.Serialization;
namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
public class TrovoRequestData
{
[JsonPropertyName("channel_id")]
public string ChannelId { get; set; }
}

View File

@@ -0,0 +1,13 @@
#nullable disable
using System.Text.Json.Serialization;
namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
public class TrovoSocialLink
{
[JsonPropertyName("type")]
public string Type { get; set; }
[JsonPropertyName("url")]
public string Url { get; set; }
}

View File

@@ -17,6 +17,7 @@ public class NotifChecker
public NotifChecker(
IHttpClientFactory httpClientFactory,
IBotCredsProvider credsProvider,
ConnectionMultiplexer multi,
string uniqueCacheKey,
bool isMaster)
@@ -26,7 +27,8 @@ public class NotifChecker
_streamProviders = new()
{
{ FollowedStream.FType.Twitch, new TwitchProvider(httpClientFactory) },
{ FollowedStream.FType.Picarto, new PicartoProvider(httpClientFactory) }
{ FollowedStream.FType.Picarto, new PicartoProvider(httpClientFactory) },
{ FollowedStream.FType.Trovo, new TrovoProvider(httpClientFactory, credsProvider) }
};
_offlineBuffer = new();
if (isMaster) CacheClearAllData();
@@ -35,17 +37,20 @@ public class NotifChecker
// gets all streams which have been failing for more than the provided timespan
public IEnumerable<StreamDataKey> GetFailingStreams(TimeSpan duration, bool remove = false)
{
var toReturn = _streamProviders.SelectMany(prov => prov.Value
.FailingStreams
.Where(fs => DateTime.UtcNow - fs.ErroringSince
> duration)
.Select(fs => new StreamDataKey(prov.Value.Platform,
fs.Item1)))
.ToList();
var toReturn = _streamProviders
.SelectMany(prov => prov.Value
.FailingStreams
.Where(fs => DateTime.UtcNow - fs.Value > duration)
.Select(fs => new StreamDataKey(prov.Value.Platform, fs.Key)))
.ToList();
if (remove)
{
foreach (var toBeRemoved in toReturn)
{
_streamProviders[toBeRemoved.Type].ClearErrorsFor(toBeRemoved.Name);
}
}
return toReturn;
}
@@ -54,6 +59,7 @@ public class NotifChecker
=> Task.Run(async () =>
{
while (true)
{
try
{
var allStreamData = CacheGetAllData();
@@ -65,19 +71,21 @@ public class NotifChecker
entry => entry.AsEnumerable()
.ToDictionary(x => x.Key.Name, x => x.Value));
var newStreamData = await oldStreamDataDict.Select(x =>
{
// get all stream data for the streams of this type
if (_streamProviders.TryGetValue(x.Key,
out var provider))
return provider.GetStreamDataAsync(x.Value
.Select(entry => entry.Key)
.ToList());
var newStreamData = await oldStreamDataDict
.Select(x =>
{
// get all stream data for the streams of this type
if (_streamProviders.TryGetValue(x.Key,
out var provider))
return provider.GetStreamDataAsync(x.Value
.Select(entry => entry.Key)
.ToList());
// this means there's no provider for this stream data, (and there was before?)
return Task.FromResult(new List<StreamData>());
})
.WhenAll();
// this means there's no provider for this stream data, (and there was before?)
return Task.FromResult<IReadOnlyCollection<StreamData>>(
new List<StreamData>());
})
.WhenAll();
var newlyOnline = new List<StreamData>();
var newlyOffline = new List<StreamData>();
@@ -124,7 +132,10 @@ public class NotifChecker
}
}
var tasks = new List<Task> { Task.Delay(30_000) };
var tasks = new List<Task>
{
Task.Delay(30_000)
};
if (newlyOnline.Count > 0) tasks.Add(OnStreamsOnline(newlyOnline));
@@ -136,6 +147,7 @@ public class NotifChecker
{
Log.Error(ex, "Error getting stream notifications: {ErrorMessage}", ex.Message);
}
}
});
public bool CacheAddData(StreamDataKey key, StreamData? data, bool replace)

View File

@@ -46,10 +46,10 @@ public class PicartoProvider : Provider
return data.FirstOrDefault();
}
public override async Task<List<StreamData>> GetStreamDataAsync(List<string> logins)
public override async Task<IReadOnlyCollection<StreamData>> GetStreamDataAsync(List<string> logins)
{
if (logins.Count == 0)
return new();
return new List<StreamData>();
using var http = _httpClientFactory.CreateClient();
var toReturn = new List<StreamData>();

View File

@@ -15,9 +15,10 @@ public abstract class Provider
/// <summary>
/// Gets the stream usernames which fail to execute due to an error, and when they started throwing errors.
/// This can happen if stream name is invalid, or if the stream doesn't exist anymore.
/// Override to provide a custom implementation
/// </summary>
public IEnumerable<(string Login, DateTime ErroringSince)> FailingStreams
=> _failingStreams.Select(entry => (entry.Key, entry.Value)).ToList();
public virtual IReadOnlyDictionary<string, DateTime> FailingStreams
=> _failingStreams;
/// <summary>
/// When was the first time the stream continually had errors while being retrieved
@@ -50,8 +51,13 @@ public abstract class Provider
/// </summary>
/// <param name="usernames">List of ids/usernames</param>
/// <returns><see cref="StreamData" /> of all users, in the same order. Null for every id/user not found.</returns>
public abstract Task<List<StreamData>> GetStreamDataAsync(List<string> usernames);
public abstract Task<IReadOnlyCollection<StreamData>> GetStreamDataAsync(List<string> usernames);
public void ClearErrorsFor(string login)
=> _failingStreams.TryRemove(login, out _);
/// <summary>
/// Unmark the stream as errored. You should override this method
/// if you've overridden the <see cref="FailingStreams"/> property.
/// </summary>
/// <param name="login"></param>
public virtual void ClearErrorsFor(string login)
=> _failingStreams.Clear();
}

View File

@@ -0,0 +1,106 @@
using NadekoBot.Db.Models;
using System.Net.Http.Json;
using System.Text.RegularExpressions;
namespace NadekoBot.Modules.Searches.Common.StreamNotifications.Providers;
public class TrovoProvider : Provider
{
private readonly IHttpClientFactory _httpClientFactory;
public override FollowedStream.FType Platform
=> FollowedStream.FType.Trovo;
private readonly Regex _urlRegex
= new Regex(@"trovo.live\/(?<channel>[\w\d\-_]+)/?", RegexOptions.Compiled);
private readonly IBotCredsProvider _creds;
public TrovoProvider(IHttpClientFactory httpClientFactory, IBotCredsProvider creds)
=> (_httpClientFactory, _creds) = (httpClientFactory, creds);
public override Task<bool> IsValidUrl(string url)
=> Task.FromResult(_urlRegex.IsMatch(url));
public override Task<StreamData?> GetStreamDataByUrlAsync(string url)
{
var match = _urlRegex.Match(url);
if (match.Length == 0)
return Task.FromResult(default(StreamData?));
return GetStreamDataAsync(match.Groups["channel"].Value);
}
public override async Task<StreamData?> GetStreamDataAsync(string id)
{
using var http = _httpClientFactory.CreateClient();
var trovoClientId = _creds.GetCreds().TrovoClientId;
if (string.IsNullOrWhiteSpace(trovoClientId))
trovoClientId = "waiting for key";
http.DefaultRequestHeaders.Clear();
http.DefaultRequestHeaders.Add("Accept", "application/json");
http.DefaultRequestHeaders.Add("Client-ID", trovoClientId);
// trovo ratelimit is very generous (1200 per minute)
// so there is no need for ratelimit checks atm
try
{
var res = await http.PostAsJsonAsync(
$"https://open-api.trovo.live/openplatform/channels/id",
new TrovoRequestData()
{
ChannelId = id
});
res.EnsureSuccessStatusCode();
var data = await res.Content.ReadFromJsonAsync<TrovoGetUsersResponse>();
if (data is null)
{
Log.Warning("An empty response received while retrieving stream data for trovo.live/{TrovoId}", id);
_failingStreams.TryAdd(id, DateTime.UtcNow);
return null;
}
return new()
{
IsLive = data.IsLive,
Game = data.CategoryName,
Name = data.Username,
Title = data.LiveTitle,
Viewers = data.CurrentViewers,
AvatarUrl = data.ProfilePic,
StreamType = FollowedStream.FType.Picarto,
StreamUrl = data.ChannelUrl,
UniqueName = data.Username,
Preview = data.Thumbnail,
};
}
catch (Exception ex)
{
Log.Warning(ex, "Error retrieving stream data for trovo.live/{TrovoId}", id);
_failingStreams.TryAdd(id, DateTime.UtcNow);
return null;
}
}
public override async Task<IReadOnlyCollection<StreamData>> GetStreamDataAsync(List<string> usernames)
{
var results = new List<StreamData>(usernames.Count);
foreach (var chunk in usernames.Chunk(10)
.Select(x => x.Select(GetStreamDataAsync)))
{
var chunkResults = await Task.WhenAll(chunk);
results.AddRange(chunkResults.Where(x => x is not null)!);
await Task.Delay(1000);
}
return results;
}
}

View File

@@ -12,11 +12,17 @@ public class TwitchProvider : Provider
public override FollowedStream.FType Platform
=> FollowedStream.FType.Twitch;
public override IReadOnlyDictionary<string, DateTime> FailingStreams
=> _failingStreams;
private readonly IHttpClientFactory _httpClientFactory;
public TwitchProvider(IHttpClientFactory httpClientFactory)
=> _httpClientFactory = httpClientFactory;
public override void ClearErrorsFor(string login)
=> _failingStreams.TryRemove(login, out _);
public override Task<bool> IsValidUrl(string url)
{
var match = Regex.Match(url);
@@ -41,15 +47,18 @@ public class TwitchProvider : Provider
public override async Task<StreamData?> GetStreamDataAsync(string id)
{
var data = await GetStreamDataAsync(new List<string> { id });
var data = await GetStreamDataAsync(new List<string>
{
id
});
return data.FirstOrDefault();
}
public override async Task<List<StreamData>> GetStreamDataAsync(List<string> logins)
public override async Task<IReadOnlyCollection<StreamData>> GetStreamDataAsync(List<string> logins)
{
if (logins.Count == 0)
return new();
return new List<StreamData>();
using var http = _httpClientFactory.CreateClient();
http.DefaultRequestHeaders.Add("Client-Id", "67w6z9i09xv2uoojdm9l0wsyph4hxo6");
@@ -70,7 +79,11 @@ public class TwitchProvider : Provider
// get stream data
var str = await http.GetStringAsync($"https://api.twitch.tv/kraken/streams/{user.Id}");
var resObj = JsonConvert.DeserializeAnonymousType(str, new { Stream = new TwitchResponseV5.Stream() });
var resObj = JsonConvert.DeserializeAnonymousType(str,
new
{
Stream = new TwitchResponseV5.Stream()
});
// if stream is null, user is not streaming
if (resObj?.Stream is null)

View File

@@ -9,7 +9,8 @@
<RunWorkingDirectory>$(MSBuildProjectDirectory)</RunWorkingDirectory>
<OutputType>exe</OutputType>
<ApplicationIcon>nadeko_icon.ico</ApplicationIcon>
<NoWarn>CS1066</NoWarn>
<NoWarn>CS1066,CA1069</NoWarn>
<AnalysisMode>Recommended</AnalysisMode>
</PropertyGroup>
<ItemGroup>

View File

@@ -119,6 +119,12 @@ public sealed class BotCredsProvider : IBotCredsProvider
var jsonCredentialsFileText = File.ReadAllText(OldCredsJsonPath);
var oldCreds = JsonConvert.DeserializeObject<Creds.Old>(jsonCredentialsFileText);
if (oldCreds is null)
{
Log.Error("Error while reading old credentials file. Make sure that the file is formatted correctly");
return;
}
var creds = new Creds
{
Version = 1,