Stream notifications have more data now, and user icons
This commit is contained in:
parent
26fb2cbfef
commit
5d14c3cbcf
@ -2,17 +2,39 @@
|
|||||||
|
|
||||||
namespace NadekoBot.Modules.Searches.Common
|
namespace NadekoBot.Modules.Searches.Common
|
||||||
{
|
{
|
||||||
public class HitboxResponse
|
public interface IStreamResponse
|
||||||
{
|
{
|
||||||
public bool Success { get; set; } = true;
|
int Viewers { get; }
|
||||||
[JsonProperty("media_is_live")]
|
string Title { get; }
|
||||||
public string MediaIsLive { get; set; }
|
bool Live { get; }
|
||||||
public bool IsLive => MediaIsLive == "1";
|
string Game { get; }
|
||||||
[JsonProperty("media_views")]
|
int FollowerCount { get; }
|
||||||
public string Views { get; set; }
|
string Url { get; }
|
||||||
|
string Icon { get; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class TwitchResponse
|
public class SmashcastResponse : IStreamResponse
|
||||||
|
{
|
||||||
|
public bool Success { get; set; } = true;
|
||||||
|
public int Followers { get; set; }
|
||||||
|
[JsonProperty("user_logo")]
|
||||||
|
public string UserLogo { get; set; }
|
||||||
|
[JsonProperty("is_live")]
|
||||||
|
public string IsLive { get; set; }
|
||||||
|
|
||||||
|
public int Viewers => 0;
|
||||||
|
public string Title => "";
|
||||||
|
public bool Live => IsLive == "1";
|
||||||
|
public string Game => "";
|
||||||
|
public int FollowerCount => Followers;
|
||||||
|
public string Icon => !string.IsNullOrWhiteSpace(UserLogo)
|
||||||
|
? "https://edge.sf.hitbox.tv" + UserLogo
|
||||||
|
: "";
|
||||||
|
|
||||||
|
public string Url { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TwitchResponse : IStreamResponse
|
||||||
{
|
{
|
||||||
public string Error { get; set; } = null;
|
public string Error { get; set; } = null;
|
||||||
public bool IsLive => Stream != null;
|
public bool IsLive => Stream != null;
|
||||||
@ -21,15 +43,53 @@ namespace NadekoBot.Modules.Searches.Common
|
|||||||
public class StreamInfo
|
public class StreamInfo
|
||||||
{
|
{
|
||||||
public int Viewers { get; set; }
|
public int Viewers { get; set; }
|
||||||
|
public string Game { get; set; }
|
||||||
|
public ChannelInfo Channel { get; set; }
|
||||||
|
public int Followers { get; set; }
|
||||||
|
|
||||||
|
public class ChannelInfo
|
||||||
|
{
|
||||||
|
public string Status { get; set; }
|
||||||
|
public string Logo { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int Viewers => Stream?.Viewers ?? 0;
|
||||||
|
public string Title => Stream?.Channel?.Status;
|
||||||
|
public bool Live => IsLive;
|
||||||
|
public string Game => Stream?.Game;
|
||||||
|
public int FollowerCount => Stream?.Followers ?? 0;
|
||||||
|
public string Url { get; set; }
|
||||||
|
public string Icon => Stream?.Channel?.Logo;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BeamResponse
|
public class MixerResponse : IStreamResponse
|
||||||
{
|
{
|
||||||
|
public class MixerType
|
||||||
|
{
|
||||||
|
public string Parent { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
}
|
||||||
|
public class MixerThumbnail
|
||||||
|
{
|
||||||
|
public string Url { get; set; }
|
||||||
|
}
|
||||||
|
public string Url { get; set; }
|
||||||
public string Error { get; set; } = null;
|
public string Error { get; set; } = null;
|
||||||
|
|
||||||
[JsonProperty("online")]
|
[JsonProperty("online")]
|
||||||
public bool IsLive { get; set; }
|
public bool IsLive { get; set; }
|
||||||
public int ViewersCurrent { get; set; }
|
public int ViewersCurrent { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int NumFollowers { get; set; }
|
||||||
|
public MixerType Type { get; set; }
|
||||||
|
public MixerThumbnail Thumbnail { get; set; }
|
||||||
|
|
||||||
|
public int Viewers => ViewersCurrent;
|
||||||
|
public string Title => Name;
|
||||||
|
public bool Live => IsLive;
|
||||||
|
public string Game => Type?.Name ?? "";
|
||||||
|
public int FollowerCount => NumFollowers;
|
||||||
|
public string Icon => Thumbnail?.Url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -229,11 +229,4 @@ namespace NadekoBot.Modules.Searches.Services
|
|||||||
public ulong UserId { get; set; }
|
public ulong UserId { get; set; }
|
||||||
public ulong ChannelId { get; set; }
|
public ulong ChannelId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class StreamStatus
|
|
||||||
{
|
|
||||||
public bool IsLive { get; set; }
|
|
||||||
public string ApiLink { get; set; }
|
|
||||||
public string Views { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -21,20 +21,21 @@ namespace NadekoBot.Modules.Searches.Services
|
|||||||
{
|
{
|
||||||
private readonly Timer _streamCheckTimer;
|
private readonly Timer _streamCheckTimer;
|
||||||
private bool firstStreamNotifPass { get; set; } = true;
|
private bool firstStreamNotifPass { get; set; } = true;
|
||||||
private readonly ConcurrentDictionary<string, StreamStatus> _cachedStatuses = new ConcurrentDictionary<string, StreamStatus>();
|
private readonly ConcurrentDictionary<string, IStreamResponse> _cachedStatuses = new ConcurrentDictionary<string, IStreamResponse>();
|
||||||
|
|
||||||
private readonly DbService _db;
|
private readonly DbService _db;
|
||||||
private readonly DiscordSocketClient _client;
|
private readonly DiscordSocketClient _client;
|
||||||
private readonly NadekoStrings _strings;
|
private readonly NadekoStrings _strings;
|
||||||
|
private readonly HttpClient _http;
|
||||||
|
|
||||||
public StreamNotificationService(DbService db, DiscordSocketClient client, NadekoStrings strings)
|
public StreamNotificationService(DbService db, DiscordSocketClient client, NadekoStrings strings)
|
||||||
{
|
{
|
||||||
_db = db;
|
_db = db;
|
||||||
_client = client;
|
_client = client;
|
||||||
_strings = strings;
|
_strings = strings;
|
||||||
|
_http = new HttpClient();
|
||||||
_streamCheckTimer = new Timer(async (state) =>
|
_streamCheckTimer = new Timer(async (state) =>
|
||||||
{
|
{
|
||||||
var oldCachedStatuses = new ConcurrentDictionary<string, StreamStatus>(_cachedStatuses);
|
var oldCachedStatuses = new ConcurrentDictionary<string, IStreamResponse>(_cachedStatuses);
|
||||||
_cachedStatuses.Clear();
|
_cachedStatuses.Clear();
|
||||||
IEnumerable<FollowedStream> streams;
|
IEnumerable<FollowedStream> streams;
|
||||||
using (var uow = _db.UnitOfWork)
|
using (var uow = _db.UnitOfWork)
|
||||||
@ -52,9 +53,9 @@ namespace NadekoBot.Modules.Searches.Services
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamStatus oldStatus;
|
IStreamResponse oldResponse;
|
||||||
if (oldCachedStatuses.TryGetValue(newStatus.ApiLink, out oldStatus) &&
|
if (oldCachedStatuses.TryGetValue(newStatus.Url, out oldResponse) &&
|
||||||
oldStatus.IsLive != newStatus.IsLive)
|
oldResponse.Live != newStatus.Live)
|
||||||
{
|
{
|
||||||
var server = _client.GetGuild(fs.GuildId);
|
var server = _client.GetGuild(fs.GuildId);
|
||||||
var channel = server?.GetTextChannel(fs.ChannelId);
|
var channel = server?.GetTextChannel(fs.ChannelId);
|
||||||
@ -80,92 +81,85 @@ namespace NadekoBot.Modules.Searches.Services
|
|||||||
}, null, TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60));
|
}, null, TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(60));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<StreamStatus> GetStreamStatus(FollowedStream stream, bool checkCache = true)
|
public async Task<IStreamResponse> GetStreamStatus(FollowedStream stream, bool checkCache = true)
|
||||||
{
|
{
|
||||||
string response;
|
string response;
|
||||||
StreamStatus result;
|
IStreamResponse result;
|
||||||
switch (stream.Type)
|
switch (stream.Type)
|
||||||
{
|
{
|
||||||
case FollowedStream.FollowedStreamType.Smashcast:
|
case FollowedStream.FollowedStreamType.Smashcast:
|
||||||
var hitboxUrl = $"https://api.hitbox.tv/media/status/{stream.Username.ToLowerInvariant()}";
|
var smashcastUrl = $"https://api.smashcast.tv/user/{stream.Username.ToLowerInvariant()}";
|
||||||
if (checkCache && _cachedStatuses.TryGetValue(hitboxUrl, out result))
|
if (checkCache && _cachedStatuses.TryGetValue(smashcastUrl, out result))
|
||||||
return result;
|
return result;
|
||||||
using (var http = new HttpClient())
|
response = await _http.GetStringAsync(smashcastUrl).ConfigureAwait(false);
|
||||||
{
|
|
||||||
response = await http.GetStringAsync(hitboxUrl).ConfigureAwait(false);
|
var scData = JsonConvert.DeserializeObject<SmashcastResponse>(response);
|
||||||
}
|
if (!scData.Success)
|
||||||
var hbData = JsonConvert.DeserializeObject<HitboxResponse>(response);
|
|
||||||
if (!hbData.Success)
|
|
||||||
throw new StreamNotFoundException($"{stream.Username} [{stream.Type}]");
|
throw new StreamNotFoundException($"{stream.Username} [{stream.Type}]");
|
||||||
result = new StreamStatus()
|
scData.Url = smashcastUrl;
|
||||||
{
|
_cachedStatuses.AddOrUpdate(smashcastUrl, scData, (key, old) => scData);
|
||||||
IsLive = hbData.IsLive,
|
return scData;
|
||||||
ApiLink = hitboxUrl,
|
|
||||||
Views = hbData.Views
|
|
||||||
};
|
|
||||||
_cachedStatuses.AddOrUpdate(hitboxUrl, result, (key, old) => result);
|
|
||||||
return result;
|
|
||||||
case FollowedStream.FollowedStreamType.Twitch:
|
case FollowedStream.FollowedStreamType.Twitch:
|
||||||
var twitchUrl = $"https://api.twitch.tv/kraken/streams/{Uri.EscapeUriString(stream.Username.ToLowerInvariant())}?client_id=67w6z9i09xv2uoojdm9l0wsyph4hxo6";
|
var twitchUrl = $"https://api.twitch.tv/kraken/streams/{Uri.EscapeUriString(stream.Username.ToLowerInvariant())}?client_id=67w6z9i09xv2uoojdm9l0wsyph4hxo6";
|
||||||
if (checkCache && _cachedStatuses.TryGetValue(twitchUrl, out result))
|
if (checkCache && _cachedStatuses.TryGetValue(twitchUrl, out result))
|
||||||
return result;
|
return result;
|
||||||
using (var http = new HttpClient())
|
response = await _http.GetStringAsync(twitchUrl).ConfigureAwait(false);
|
||||||
{
|
|
||||||
response = await http.GetStringAsync(twitchUrl).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
var twData = JsonConvert.DeserializeObject<TwitchResponse>(response);
|
var twData = JsonConvert.DeserializeObject<TwitchResponse>(response);
|
||||||
if (twData.Error != null)
|
if (twData.Error != null)
|
||||||
{
|
{
|
||||||
throw new StreamNotFoundException($"{stream.Username} [{stream.Type}]");
|
throw new StreamNotFoundException($"{stream.Username} [{stream.Type}]");
|
||||||
}
|
}
|
||||||
result = new StreamStatus()
|
twData.Url = twitchUrl;
|
||||||
{
|
_cachedStatuses.AddOrUpdate(twitchUrl, twData, (key, old) => twData);
|
||||||
IsLive = twData.IsLive,
|
return twData;
|
||||||
ApiLink = twitchUrl,
|
|
||||||
Views = twData.Stream?.Viewers.ToString() ?? "0"
|
|
||||||
};
|
|
||||||
_cachedStatuses.AddOrUpdate(twitchUrl, result, (key, old) => result);
|
|
||||||
return result;
|
|
||||||
case FollowedStream.FollowedStreamType.Mixer:
|
case FollowedStream.FollowedStreamType.Mixer:
|
||||||
var beamUrl = $"https://mixer.com/api/v1/channels/{stream.Username.ToLowerInvariant()}";
|
var beamUrl = $"https://mixer.com/api/v1/channels/{stream.Username.ToLowerInvariant()}";
|
||||||
if (checkCache && _cachedStatuses.TryGetValue(beamUrl, out result))
|
if (checkCache && _cachedStatuses.TryGetValue(beamUrl, out result))
|
||||||
return result;
|
return result;
|
||||||
using (var http = new HttpClient())
|
response = await _http.GetStringAsync(beamUrl).ConfigureAwait(false);
|
||||||
{
|
|
||||||
response = await http.GetStringAsync(beamUrl).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
var bmData = JsonConvert.DeserializeObject<BeamResponse>(response);
|
|
||||||
|
var bmData = JsonConvert.DeserializeObject<MixerResponse>(response);
|
||||||
if (bmData.Error != null)
|
if (bmData.Error != null)
|
||||||
throw new StreamNotFoundException($"{stream.Username} [{stream.Type}]");
|
throw new StreamNotFoundException($"{stream.Username} [{stream.Type}]");
|
||||||
result = new StreamStatus()
|
bmData.Url = beamUrl;
|
||||||
{
|
_cachedStatuses.AddOrUpdate(beamUrl, bmData, (key, old) => bmData);
|
||||||
IsLive = bmData.IsLive,
|
return bmData;
|
||||||
ApiLink = beamUrl,
|
|
||||||
Views = bmData.ViewersCurrent.ToString()
|
|
||||||
};
|
|
||||||
_cachedStatuses.AddOrUpdate(beamUrl, result, (key, old) => result);
|
|
||||||
return result;
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EmbedBuilder GetEmbed(FollowedStream fs, StreamStatus status, ulong guildId)
|
public EmbedBuilder GetEmbed(FollowedStream fs, IStreamResponse status, ulong guildId)
|
||||||
{
|
{
|
||||||
var embed = new EmbedBuilder().WithTitle(fs.Username)
|
var embed = new EmbedBuilder()
|
||||||
.WithUrl(GetLink(fs))
|
.WithTitle(fs.Username)
|
||||||
.AddField(efb => efb.WithName(GetText(fs, "status"))
|
.WithUrl(GetLink(fs))
|
||||||
.WithValue(status.IsLive ? "Online" : "Offline")
|
.WithDescription(GetLink(fs))
|
||||||
.WithIsInline(true))
|
.AddField(efb => efb.WithName(GetText(fs, "status"))
|
||||||
.AddField(efb => efb.WithName(GetText(fs, "viewers"))
|
.WithValue(status.Live ? "Online" : "Offline")
|
||||||
.WithValue(status.IsLive ? status.Views : "-")
|
.WithIsInline(true))
|
||||||
.WithIsInline(true))
|
.AddField(efb => efb.WithName(GetText(fs, "viewers"))
|
||||||
.AddField(efb => efb.WithName(GetText(fs, "platform"))
|
.WithValue(status.Live ? status.Viewers.ToString() : "-")
|
||||||
.WithValue(fs.Type.ToString())
|
.WithIsInline(true))
|
||||||
.WithIsInline(true))
|
.WithColor(status.Live ? NadekoBot.OkColor : NadekoBot.ErrorColor);
|
||||||
.WithColor(status.IsLive ? NadekoBot.OkColor : NadekoBot.ErrorColor);
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(status.Title))
|
||||||
|
embed.WithAuthor(status.Title);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(status.Game))
|
||||||
|
embed.AddField(GetText(fs, "streaming"),
|
||||||
|
status.Game,
|
||||||
|
true);
|
||||||
|
|
||||||
|
embed.AddField(GetText(fs, "followers"),
|
||||||
|
status.FollowerCount.ToString(),
|
||||||
|
true);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(status.Icon))
|
||||||
|
embed.WithThumbnailUrl(status.Icon);
|
||||||
|
|
||||||
return embed;
|
return embed;
|
||||||
}
|
}
|
||||||
@ -179,7 +173,7 @@ namespace NadekoBot.Modules.Searches.Services
|
|||||||
public string GetLink(FollowedStream fs)
|
public string GetLink(FollowedStream fs)
|
||||||
{
|
{
|
||||||
if (fs.Type == FollowedStream.FollowedStreamType.Smashcast)
|
if (fs.Type == FollowedStream.FollowedStreamType.Smashcast)
|
||||||
return $"https://www.hitbox.tv/{fs.Username}/";
|
return $"https://www.smashcast.tv/{fs.Username}/";
|
||||||
if (fs.Type == FollowedStream.FollowedStreamType.Twitch)
|
if (fs.Type == FollowedStream.FollowedStreamType.Twitch)
|
||||||
return $"https://www.twitch.tv/{fs.Username}/";
|
return $"https://www.twitch.tv/{fs.Username}/";
|
||||||
if (fs.Type == FollowedStream.FollowedStreamType.Mixer)
|
if (fs.Type == FollowedStream.FollowedStreamType.Mixer)
|
||||||
|
@ -9,6 +9,7 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using NadekoBot.Common.Attributes;
|
using NadekoBot.Common.Attributes;
|
||||||
using NadekoBot.Extensions;
|
using NadekoBot.Extensions;
|
||||||
using NadekoBot.Modules.Searches.Services;
|
using NadekoBot.Modules.Searches.Services;
|
||||||
|
using NadekoBot.Modules.Searches.Common;
|
||||||
|
|
||||||
namespace NadekoBot.Modules.Searches
|
namespace NadekoBot.Modules.Searches
|
||||||
{
|
{
|
||||||
@ -124,11 +125,11 @@ namespace NadekoBot.Modules.Searches
|
|||||||
Username = stream,
|
Username = stream,
|
||||||
Type = platform,
|
Type = platform,
|
||||||
}));
|
}));
|
||||||
if (streamStatus.IsLive)
|
if (streamStatus.Live)
|
||||||
{
|
{
|
||||||
await ReplyConfirmLocalized("streamer_online",
|
await ReplyConfirmLocalized("streamer_online",
|
||||||
username,
|
username,
|
||||||
streamStatus.Views)
|
streamStatus.Viewers)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -154,7 +155,7 @@ namespace NadekoBot.Modules.Searches
|
|||||||
Type = type,
|
Type = type,
|
||||||
};
|
};
|
||||||
|
|
||||||
StreamStatus status;
|
IStreamResponse status;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
status = await _service.GetStreamStatus(fs).ConfigureAwait(false);
|
status = await _service.GetStreamStatus(fs).ConfigureAwait(false);
|
||||||
|
@ -20,7 +20,7 @@ namespace NadekoBot.Services.Impl
|
|||||||
private readonly IBotCredentials _creds;
|
private readonly IBotCredentials _creds;
|
||||||
private readonly DateTime _started;
|
private readonly DateTime _started;
|
||||||
|
|
||||||
public const string BotVersion = "1.9.3";
|
public const string BotVersion = "1.10.0";
|
||||||
|
|
||||||
public string Author => "Kwoth#2560";
|
public string Author => "Kwoth#2560";
|
||||||
public string Library => "Discord.Net";
|
public string Library => "Discord.Net";
|
||||||
|
@ -882,5 +882,7 @@
|
|||||||
"searches_feed_no_feed": "You haven't subscribed to any feeds on this server.",
|
"searches_feed_no_feed": "You haven't subscribed to any feeds on this server.",
|
||||||
"administration_restart_fail": "You must setup RestartCommand in your credentials.json",
|
"administration_restart_fail": "You must setup RestartCommand in your credentials.json",
|
||||||
"administration_restarting": "Restarting.",
|
"administration_restarting": "Restarting.",
|
||||||
"customreactions_edit_fail": "Custom reaction with that ID does not exist."
|
"customreactions_edit_fail": "Custom reaction with that ID does not exist.",
|
||||||
|
"searches_streaming": "Streaming",
|
||||||
|
"searches_followers": "Followers"
|
||||||
}
|
}
|
Loading…
Reference in New Issue
Block a user