Stream notifications have more data now, and user icons

This commit is contained in:
Master Kwoth 2017-09-28 21:01:34 +02:00
parent 26fb2cbfef
commit 5d14c3cbcf
6 changed files with 135 additions and 85 deletions

View File

@ -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;
} }
} }

View File

@ -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; }
}
} }

View File

@ -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)
@ -187,4 +181,4 @@ namespace NadekoBot.Modules.Searches.Services
return "??"; return "??";
} }
} }
} }

View File

@ -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);

View File

@ -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";

View File

@ -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"
} }