mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-12 10:18:27 -04:00
Restructured folders and project names, ci should be fixed
This commit is contained in:
26
src/NadekoBot/Modules/Searches/Common/AnimeResult.cs
Normal file
26
src/NadekoBot/Modules/Searches/Common/AnimeResult.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using NadekoBot.Extensions;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common
|
||||
{
|
||||
public class AnimeResult
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string AiringStatus => AiringStatusParsed.ToTitleCase();
|
||||
[JsonProperty("airing_status")]
|
||||
public string AiringStatusParsed { get; set; }
|
||||
[JsonProperty("title_english")]
|
||||
public string TitleEnglish { get; set; }
|
||||
[JsonProperty("total_episodes")]
|
||||
public int TotalEpisodes { get; set; }
|
||||
public string Description { get; set; }
|
||||
[JsonProperty("image_url_lge")]
|
||||
public string ImageUrlLarge { get; set; }
|
||||
public string[] Genres { get; set; }
|
||||
[JsonProperty("average_score")]
|
||||
public string AverageScore { get; set; }
|
||||
|
||||
public string Link => "http://anilist.co/anime/" + Id;
|
||||
public string Synopsis => Description?.Substring(0, Description.Length > 500 ? 500 : Description.Length) + "...";
|
||||
}
|
||||
}
|
19
src/NadekoBot/Modules/Searches/Common/BibleVerses.cs
Normal file
19
src/NadekoBot/Modules/Searches/Common/BibleVerses.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Searches.Common
|
||||
{
|
||||
public class BibleVerses
|
||||
{
|
||||
public string Error { get; set; }
|
||||
public BibleVerse[] Verses { get; set; }
|
||||
}
|
||||
|
||||
public class BibleVerse
|
||||
{
|
||||
[JsonProperty("book_name")]
|
||||
public string BookName { get; set; }
|
||||
public int Chapter { get; set; }
|
||||
public int Verse { get; set; }
|
||||
public string Text { get; set; }
|
||||
}
|
||||
}
|
37
src/NadekoBot/Modules/Searches/Common/CryptoData.cs
Normal file
37
src/NadekoBot/Modules/Searches/Common/CryptoData.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Searches.Common
|
||||
{
|
||||
public class CryptoResponse
|
||||
{
|
||||
public List<CryptoResponseData> Data { get; set; }
|
||||
}
|
||||
|
||||
public class CryptoResponseData
|
||||
{
|
||||
public string Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Symbol { get; set; }
|
||||
public string Slug { get; set; }
|
||||
|
||||
[JsonProperty("cmc_rank")]
|
||||
public int Rank { get; set; }
|
||||
public CurrencyQuotes Quote { get; set; }
|
||||
}
|
||||
|
||||
public class CurrencyQuotes
|
||||
{
|
||||
public Quote Usd { get; set; }
|
||||
}
|
||||
|
||||
public class Quote
|
||||
{
|
||||
public double Price { get; set; }
|
||||
public double Market_Cap { get; set; }
|
||||
public string Percent_Change_1h { get; set; }
|
||||
public string Percent_Change_24h { get; set; }
|
||||
public string Percent_Change_7d { get; set; }
|
||||
public double? Volume_24h { get; set; }
|
||||
}
|
||||
}
|
42
src/NadekoBot/Modules/Searches/Common/DefineModel.cs
Normal file
42
src/NadekoBot/Modules/Searches/Common/DefineModel.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common
|
||||
{
|
||||
public class Audio
|
||||
{
|
||||
public string Url { get; set; }
|
||||
}
|
||||
|
||||
public class Example
|
||||
{
|
||||
public List<Audio> Audio { get; set; }
|
||||
public string Text { get; set; }
|
||||
}
|
||||
|
||||
public class GramaticalInfo
|
||||
{
|
||||
public string Type { get; set; }
|
||||
}
|
||||
|
||||
public class Sens
|
||||
{
|
||||
public object Definition { get; set; }
|
||||
public List<Example> Examples { get; set; }
|
||||
[JsonProperty("gramatical_info")]
|
||||
public GramaticalInfo GramaticalInfo { get; set; }
|
||||
}
|
||||
|
||||
public class Result
|
||||
{
|
||||
[JsonProperty("part_of_speech")]
|
||||
public string PartOfSpeech { get; set; }
|
||||
public List<Sens> Senses { get; set; }
|
||||
public string Url { get; set; }
|
||||
}
|
||||
|
||||
public class DefineModel
|
||||
{
|
||||
public List<Result> Results { get; set; }
|
||||
}
|
||||
}
|
24
src/NadekoBot/Modules/Searches/Common/E621Object.cs
Normal file
24
src/NadekoBot/Modules/Searches/Common/E621Object.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace NadekoBot.Modules.Searches.Common
|
||||
{
|
||||
public class E621Object
|
||||
{
|
||||
public class FileData
|
||||
{
|
||||
public string Url { get; set; }
|
||||
}
|
||||
|
||||
public class TagData
|
||||
{
|
||||
public string[] General { get; set; }
|
||||
}
|
||||
|
||||
public class ScoreData
|
||||
{
|
||||
public string Total { get; set; }
|
||||
}
|
||||
|
||||
public FileData File { get; set; }
|
||||
public TagData Tags { get; set; }
|
||||
public ScoreData Score { get; set; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common.Exceptions
|
||||
{
|
||||
public class StreamNotFoundException : Exception
|
||||
{
|
||||
public StreamNotFoundException()
|
||||
{
|
||||
}
|
||||
|
||||
public StreamNotFoundException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public StreamNotFoundException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
46
src/NadekoBot/Modules/Searches/Common/Gallery.cs
Normal file
46
src/NadekoBot/Modules/Searches/Common/Gallery.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Searches.Common
|
||||
{
|
||||
public sealed class Tag
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Url { get; set; }
|
||||
}
|
||||
|
||||
public sealed class Gallery
|
||||
{
|
||||
public string Id { get; }
|
||||
public string Url { get; }
|
||||
public string FullTitle { get; }
|
||||
public string Title { get; }
|
||||
public string Thumbnail { get; }
|
||||
public int PageCount { get; }
|
||||
public int Likes { get; }
|
||||
public DateTime UploadedAt { get; }
|
||||
public Tag[] Tags { get; }
|
||||
|
||||
|
||||
public Gallery(
|
||||
string id,
|
||||
string url,
|
||||
string fullTitle,
|
||||
string title,
|
||||
string thumbnail,
|
||||
int pageCount,
|
||||
int likes,
|
||||
DateTime uploadedAt,
|
||||
Tag[] tags)
|
||||
{
|
||||
Id = id;
|
||||
Url = url;
|
||||
FullTitle = fullTitle;
|
||||
Title = title;
|
||||
Thumbnail = thumbnail;
|
||||
PageCount = pageCount;
|
||||
Likes = likes;
|
||||
UploadedAt = uploadedAt;
|
||||
Tags = tags;
|
||||
}
|
||||
}
|
||||
}
|
39
src/NadekoBot/Modules/Searches/Common/GatariUserResponse.cs
Normal file
39
src/NadekoBot/Modules/Searches/Common/GatariUserResponse.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Searches.Common
|
||||
{
|
||||
public class UserData
|
||||
{
|
||||
[JsonProperty("abbr")] public object Abbr { get; set; }
|
||||
|
||||
[JsonProperty("clanid")] public object Clanid { get; set; }
|
||||
|
||||
[JsonProperty("country")] public string Country { get; set; }
|
||||
|
||||
[JsonProperty("favourite_mode")] public int FavouriteMode { get; set; }
|
||||
|
||||
[JsonProperty("followers_count")] public int FollowersCount { get; set; }
|
||||
|
||||
[JsonProperty("id")] public int Id { get; set; }
|
||||
|
||||
[JsonProperty("latest_activity")] public int LatestActivity { get; set; }
|
||||
|
||||
[JsonProperty("play_style")] public int PlayStyle { get; set; }
|
||||
|
||||
[JsonProperty("privileges")] public int Privileges { get; set; }
|
||||
|
||||
[JsonProperty("registered_on")] public int RegisteredOn { get; set; }
|
||||
|
||||
[JsonProperty("username")] public string Username { get; set; }
|
||||
|
||||
[JsonProperty("username_aka")] public string UsernameAka { get; set; }
|
||||
}
|
||||
|
||||
public class GatariUserResponse
|
||||
{
|
||||
[JsonProperty("code")] public int Code { get; set; }
|
||||
|
||||
[JsonProperty("users")] public List<UserData> Users { get; set; }
|
||||
}
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Searches.Common
|
||||
{
|
||||
public class UserStats
|
||||
{
|
||||
[JsonProperty("a_count")] public int ACount { get; set; }
|
||||
|
||||
[JsonProperty("avg_accuracy")] public double AvgAccuracy { get; set; }
|
||||
|
||||
[JsonProperty("avg_hits_play")] public double AvgHitsPlay { get; set; }
|
||||
|
||||
[JsonProperty("country_rank")] public int CountryRank { get; set; }
|
||||
|
||||
[JsonProperty("id")] public int Id { get; set; }
|
||||
|
||||
[JsonProperty("level")] public int Level { get; set; }
|
||||
|
||||
[JsonProperty("level_progress")] public int LevelProgress { get; set; }
|
||||
|
||||
[JsonProperty("max_combo")] public int MaxCombo { get; set; }
|
||||
|
||||
[JsonProperty("playcount")] public int Playcount { get; set; }
|
||||
|
||||
[JsonProperty("playtime")] public int Playtime { get; set; }
|
||||
|
||||
[JsonProperty("pp")] public int Pp { get; set; }
|
||||
|
||||
[JsonProperty("rank")] public int Rank { get; set; }
|
||||
|
||||
[JsonProperty("ranked_score")] public int RankedScore { get; set; }
|
||||
|
||||
[JsonProperty("replays_watched")] public int ReplaysWatched { get; set; }
|
||||
|
||||
[JsonProperty("s_count")] public int SCount { get; set; }
|
||||
|
||||
[JsonProperty("sh_count")] public int ShCount { get; set; }
|
||||
|
||||
[JsonProperty("total_hits")] public int TotalHits { get; set; }
|
||||
|
||||
[JsonProperty("total_score")] public long TotalScore { get; set; }
|
||||
|
||||
[JsonProperty("x_count")] public int XCount { get; set; }
|
||||
|
||||
[JsonProperty("xh_count")] public int XhCount { get; set; }
|
||||
}
|
||||
|
||||
public class GatariUserStatsResponse
|
||||
{
|
||||
[JsonProperty("code")] public int Code { get; set; }
|
||||
|
||||
[JsonProperty("stats")] public UserStats Stats { get; set; }
|
||||
}
|
||||
}
|
16
src/NadekoBot/Modules/Searches/Common/GoogleSearchResult.cs
Normal file
16
src/NadekoBot/Modules/Searches/Common/GoogleSearchResult.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace NadekoBot.Modules.Searches.Common
|
||||
{
|
||||
public sealed class GoogleSearchResult
|
||||
{
|
||||
public string Title { get; }
|
||||
public string Link { get; }
|
||||
public string Text { get; }
|
||||
|
||||
public GoogleSearchResult(string title, string link, string text)
|
||||
{
|
||||
this.Title = title;
|
||||
this.Link = link;
|
||||
this.Text = text;
|
||||
}
|
||||
}
|
||||
}
|
13
src/NadekoBot/Modules/Searches/Common/HearthstoneCardData.cs
Normal file
13
src/NadekoBot/Modules/Searches/Common/HearthstoneCardData.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace NadekoBot.Core.Modules.Searches.Common
|
||||
{
|
||||
public class HearthstoneCardData
|
||||
{
|
||||
public string Text { get; set; }
|
||||
public string Flavor { get; set; }
|
||||
public bool Collectible { get; set; }
|
||||
|
||||
public string Img { get; set; }
|
||||
public string ImgGold { get; set; }
|
||||
public string PlayerClass { get; set; }
|
||||
}
|
||||
}
|
46
src/NadekoBot/Modules/Searches/Common/ImageCacherObject.cs
Normal file
46
src/NadekoBot/Modules/Searches/Common/ImageCacherObject.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common
|
||||
{
|
||||
public class ImageCacherObject : IComparable<ImageCacherObject>
|
||||
{
|
||||
public DapiSearchType SearchType { get; }
|
||||
public string FileUrl { get; }
|
||||
public HashSet<string> Tags { get; }
|
||||
public string Rating { get; }
|
||||
|
||||
public ImageCacherObject(DapiImageObject obj, DapiSearchType type)
|
||||
{
|
||||
if (type == DapiSearchType.Danbooru && !Uri.IsWellFormedUriString(obj.FileUrl, UriKind.Absolute))
|
||||
{
|
||||
this.FileUrl = "https://danbooru.donmai.us" + obj.FileUrl;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.FileUrl = obj.FileUrl.StartsWith("http", StringComparison.InvariantCulture) ? obj.FileUrl : "https:" + obj.FileUrl;
|
||||
}
|
||||
this.SearchType = type;
|
||||
this.Rating = obj.Rating;
|
||||
this.Tags = new HashSet<string>((obj.Tags ?? obj.TagString).Split(' '));
|
||||
}
|
||||
|
||||
public ImageCacherObject(string url, DapiSearchType type, string tags, string rating)
|
||||
{
|
||||
this.SearchType = type;
|
||||
this.FileUrl = url;
|
||||
this.Tags = new HashSet<string>(tags.Split(' '));
|
||||
this.Rating = rating;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return FileUrl;
|
||||
}
|
||||
|
||||
public int CompareTo(ImageCacherObject other)
|
||||
{
|
||||
return string.Compare(FileUrl, other.FileUrl, StringComparison.InvariantCulture);
|
||||
}
|
||||
}
|
||||
}
|
8
src/NadekoBot/Modules/Searches/Common/MagicItem.cs
Normal file
8
src/NadekoBot/Modules/Searches/Common/MagicItem.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace NadekoBot.Modules.Searches.Common
|
||||
{
|
||||
public class MagicItem
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
}
|
||||
}
|
25
src/NadekoBot/Modules/Searches/Common/MangaResult.cs
Normal file
25
src/NadekoBot/Modules/Searches/Common/MangaResult.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common
|
||||
{
|
||||
public class MangaResult
|
||||
{
|
||||
public int Id { get; set; }
|
||||
[JsonProperty("publishing_status")]
|
||||
public string PublishingStatus { get; set; }
|
||||
[JsonProperty("image_url_lge")]
|
||||
public string ImageUrlLge { get; set; }
|
||||
[JsonProperty("title_english")]
|
||||
public string TitleEnglish { get; set; }
|
||||
[JsonProperty("total_chapters")]
|
||||
public int TotalChapters { get; set; }
|
||||
[JsonProperty("total_volumes")]
|
||||
public int TotalVolumes { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string[] Genres { get; set; }
|
||||
[JsonProperty("average_score")]
|
||||
public string AverageScore { get; set; }
|
||||
public string Link => "http://anilist.co/manga/" + Id;
|
||||
public string Synopsis => Description?.Substring(0, Description.Length > 500 ? 500 : Description.Length) + "...";
|
||||
}
|
||||
}
|
28
src/NadekoBot/Modules/Searches/Common/MtgData.cs
Normal file
28
src/NadekoBot/Modules/Searches/Common/MtgData.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Searches.Common
|
||||
{
|
||||
public class MtgData
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string ImageUrl { get; set; }
|
||||
public string StoreUrl { get; set; }
|
||||
public string Types { get; set; }
|
||||
public string ManaCost { get; set; }
|
||||
}
|
||||
|
||||
public class MtgResponse
|
||||
{
|
||||
public class Data
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string ManaCost { get; set; }
|
||||
public string Text { get; set; }
|
||||
public List<string> Types { get; set; }
|
||||
public string ImageUrl { get; set; }
|
||||
}
|
||||
|
||||
public List<Data> Cards { get; set; }
|
||||
}
|
||||
}
|
92
src/NadekoBot/Modules/Searches/Common/NhentaiApiModel.cs
Normal file
92
src/NadekoBot/Modules/Searches/Common/NhentaiApiModel.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Searches.Common
|
||||
{
|
||||
public static class NhentaiApiModel
|
||||
{
|
||||
public class Title
|
||||
{
|
||||
[JsonProperty("english")] public string English { get; set; }
|
||||
|
||||
[JsonProperty("japanese")] public string Japanese { get; set; }
|
||||
|
||||
[JsonProperty("pretty")] public string Pretty { get; set; }
|
||||
}
|
||||
|
||||
public class Page
|
||||
{
|
||||
[JsonProperty("t")] public string T { get; set; }
|
||||
|
||||
[JsonProperty("w")] public int W { get; set; }
|
||||
|
||||
[JsonProperty("h")] public int H { get; set; }
|
||||
}
|
||||
|
||||
public class Cover
|
||||
{
|
||||
[JsonProperty("t")] public string T { get; set; }
|
||||
|
||||
[JsonProperty("w")] public int W { get; set; }
|
||||
|
||||
[JsonProperty("h")] public int H { get; set; }
|
||||
}
|
||||
|
||||
public class Thumbnail
|
||||
{
|
||||
[JsonProperty("t")] public string T { get; set; }
|
||||
|
||||
[JsonProperty("w")] public int W { get; set; }
|
||||
|
||||
[JsonProperty("h")] public int H { get; set; }
|
||||
}
|
||||
|
||||
public class Images
|
||||
{
|
||||
[JsonProperty("pages")] public List<Page> Pages { get; set; }
|
||||
|
||||
[JsonProperty("cover")] public Cover Cover { get; set; }
|
||||
|
||||
[JsonProperty("thumbnail")] public Thumbnail Thumbnail { get; set; }
|
||||
}
|
||||
|
||||
public class Tag
|
||||
{
|
||||
[JsonProperty("id")] public int Id { get; set; }
|
||||
|
||||
[JsonProperty("type")] public string Type { get; set; }
|
||||
|
||||
[JsonProperty("name")] public string Name { get; set; }
|
||||
|
||||
[JsonProperty("url")] public string Url { get; set; }
|
||||
|
||||
[JsonProperty("count")] public int Count { get; set; }
|
||||
}
|
||||
|
||||
public class Gallery
|
||||
{
|
||||
[JsonProperty("id")] public int Id { get; set; }
|
||||
|
||||
[JsonProperty("media_id")] public string MediaId { get; set; }
|
||||
|
||||
[JsonProperty("title")] public Title Title { get; set; }
|
||||
|
||||
[JsonProperty("images")] public Images Images { get; set; }
|
||||
|
||||
[JsonProperty("scanlator")] public string Scanlator { get; set; }
|
||||
|
||||
[JsonProperty("upload_date")] public double UploadDate { get; set; }
|
||||
|
||||
[JsonProperty("tags")] public Tag[] Tags { get; set; }
|
||||
|
||||
[JsonProperty("num_pages")] public int NumPages { get; set; }
|
||||
|
||||
[JsonProperty("num_favorites")] public int NumFavorites { get; set; }
|
||||
}
|
||||
|
||||
public class SearchResult
|
||||
{
|
||||
[JsonProperty("result")] public Gallery[] Result { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
14
src/NadekoBot/Modules/Searches/Common/NovelData.cs
Normal file
14
src/NadekoBot/Modules/Searches/Common/NovelData.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace NadekoBot.Modules.Searches.Common
|
||||
{
|
||||
public class NovelResult
|
||||
{
|
||||
public string Description { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Link { get; set; }
|
||||
public string ImageUrl { get; set; }
|
||||
public string[] Authors { get; set; }
|
||||
public string Status { get; set; }
|
||||
public string[] Genres { get; set; }
|
||||
public string Score { get; set; }
|
||||
}
|
||||
}
|
13
src/NadekoBot/Modules/Searches/Common/OmdbMovie.cs
Normal file
13
src/NadekoBot/Modules/Searches/Common/OmdbMovie.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace NadekoBot.Core.Modules.Searches.Common
|
||||
{
|
||||
public class OmdbMovie
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Year { get; set; }
|
||||
public string ImdbRating { get; set; }
|
||||
public string ImdbId { get; set; }
|
||||
public string Genre { get; set; }
|
||||
public string Plot { get; set; }
|
||||
public string Poster { get; set; }
|
||||
}
|
||||
}
|
49
src/NadekoBot/Modules/Searches/Common/OsuUserData.cs
Normal file
49
src/NadekoBot/Modules/Searches/Common/OsuUserData.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Searches.Common
|
||||
{
|
||||
public class OsuUserData
|
||||
{
|
||||
[JsonProperty("user_id")] public string UserId { get; set; }
|
||||
|
||||
[JsonProperty("username")] public string Username { get; set; }
|
||||
|
||||
[JsonProperty("join_date")] public string JoinDate { get; set; }
|
||||
|
||||
[JsonProperty("count300")] public string Count300 { get; set; }
|
||||
|
||||
[JsonProperty("count100")] public string Count100 { get; set; }
|
||||
|
||||
[JsonProperty("count50")] public string Count50 { get; set; }
|
||||
|
||||
[JsonProperty("playcount")] public string Playcount { get; set; }
|
||||
|
||||
[JsonProperty("ranked_score")] public string RankedScore { get; set; }
|
||||
|
||||
[JsonProperty("total_score")] public string TotalScore { get; set; }
|
||||
|
||||
[JsonProperty("pp_rank")] public string PpRank { get; set; }
|
||||
|
||||
[JsonProperty("level")] public double Level { get; set; }
|
||||
|
||||
[JsonProperty("pp_raw")] public double PpRaw { get; set; }
|
||||
|
||||
[JsonProperty("accuracy")] public double Accuracy { get; set; }
|
||||
|
||||
[JsonProperty("count_rank_ss")] public string CountRankSs { get; set; }
|
||||
|
||||
[JsonProperty("count_rank_ssh")] public string CountRankSsh { get; set; }
|
||||
|
||||
[JsonProperty("count_rank_s")] public string CountRankS { get; set; }
|
||||
|
||||
[JsonProperty("count_rank_sh")] public string CountRankSh { get; set; }
|
||||
|
||||
[JsonProperty("count_rank_a")] public string CountRankA { get; set; }
|
||||
|
||||
[JsonProperty("country")] public string Country { get; set; }
|
||||
|
||||
[JsonProperty("total_seconds_played")] public string TotalSecondsPlayed { get; set; }
|
||||
|
||||
[JsonProperty("pp_country_rank")] public string PpCountryRank { get; set; }
|
||||
}
|
||||
}
|
42
src/NadekoBot/Modules/Searches/Common/PathOfExileModels.cs
Normal file
42
src/NadekoBot/Modules/Searches/Common/PathOfExileModels.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Searches.Common
|
||||
{
|
||||
|
||||
public class Account
|
||||
{
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("league")]
|
||||
public string League { get; set; }
|
||||
|
||||
[JsonProperty("classId")]
|
||||
public int ClassId { get; set; }
|
||||
|
||||
[JsonProperty("ascendancyClass")]
|
||||
public int AscendancyClass { get; set; }
|
||||
|
||||
[JsonProperty("class")]
|
||||
public string Class { get; set; }
|
||||
|
||||
[JsonProperty("level")]
|
||||
public int Level { get; set; }
|
||||
}
|
||||
|
||||
public class Leagues
|
||||
{
|
||||
[JsonProperty("id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
[JsonProperty("url")]
|
||||
public string Url { get; set; }
|
||||
|
||||
[JsonProperty("startAt")]
|
||||
public DateTime StartAt { get; set; }
|
||||
|
||||
[JsonProperty("endAt")]
|
||||
public object EndAt { get; set; }
|
||||
}
|
||||
}
|
312
src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs
Normal file
312
src/NadekoBot/Modules/Searches/Common/SearchImageCacher.cs
Normal file
@@ -0,0 +1,312 @@
|
||||
using NadekoBot.Extensions;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml;
|
||||
using Serilog;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common
|
||||
{
|
||||
// note: this is not the code that public nadeko is using
|
||||
public class SearchImageCacher
|
||||
{
|
||||
private readonly SemaphoreSlim _lock = new SemaphoreSlim(1, 1);
|
||||
private readonly IHttpClientFactory _httpFactory;
|
||||
private readonly Random _rng;
|
||||
private readonly SortedSet<ImageCacherObject> _cache;
|
||||
private static readonly List<string> defaultTagBlacklist = new List<string>() {
|
||||
"loli",
|
||||
"lolicon",
|
||||
"shota"
|
||||
};
|
||||
|
||||
public SearchImageCacher(IHttpClientFactory http)
|
||||
{
|
||||
_httpFactory = http;
|
||||
_rng = new Random();
|
||||
_cache = new SortedSet<ImageCacherObject>();
|
||||
}
|
||||
|
||||
public async Task<ImageCacherObject> GetImage(string[] tags, bool forceExplicit, DapiSearchType type,
|
||||
HashSet<string> blacklistedTags = null)
|
||||
{
|
||||
tags = tags.Select(tag => tag?.ToLowerInvariant()).ToArray();
|
||||
|
||||
blacklistedTags = blacklistedTags ?? new HashSet<string>();
|
||||
|
||||
foreach (var item in defaultTagBlacklist)
|
||||
{
|
||||
blacklistedTags.Add(item);
|
||||
}
|
||||
|
||||
blacklistedTags = blacklistedTags.Select(t => t.ToLowerInvariant()).ToHashSet();
|
||||
|
||||
if (tags.Any(x => blacklistedTags.Contains(x)))
|
||||
{
|
||||
throw new Exception("One of the specified tags is blacklisted");
|
||||
}
|
||||
|
||||
if (type == DapiSearchType.E621)
|
||||
tags = tags.Select(tag => tag?.Replace("yuri", "female/female", StringComparison.InvariantCulture))
|
||||
.ToArray();
|
||||
|
||||
await _lock.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
ImageCacherObject[] imgs;
|
||||
if (tags.Any())
|
||||
{
|
||||
imgs = _cache.Where(x => x.Tags.IsSupersetOf(tags) && x.SearchType == type && (!forceExplicit || x.Rating == "e")).ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
imgs = _cache.Where(x => x.SearchType == type).ToArray();
|
||||
}
|
||||
imgs = imgs.Where(x => x.Tags.All(t => !blacklistedTags.Contains(t.ToLowerInvariant()))).ToArray();
|
||||
ImageCacherObject img;
|
||||
if (imgs.Length == 0)
|
||||
img = null;
|
||||
else
|
||||
img = imgs[_rng.Next(imgs.Length)];
|
||||
|
||||
if (img != null)
|
||||
{
|
||||
_cache.Remove(img);
|
||||
return img;
|
||||
}
|
||||
else
|
||||
{
|
||||
var images = await DownloadImagesAsync(tags, forceExplicit, type).ConfigureAwait(false);
|
||||
images = images
|
||||
.Where(x => x.Tags.All(t => !blacklistedTags.Contains(t.ToLowerInvariant())))
|
||||
.ToArray();
|
||||
if (images.Length == 0)
|
||||
return null;
|
||||
var toReturn = images[_rng.Next(images.Length)];
|
||||
foreach (var dledImg in images)
|
||||
{
|
||||
if (dledImg != toReturn)
|
||||
_cache.Add(dledImg);
|
||||
}
|
||||
return toReturn;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ImageCacherObject[]> DownloadImagesAsync(string[] tags, bool isExplicit, DapiSearchType type)
|
||||
{
|
||||
isExplicit = type == DapiSearchType.Safebooru
|
||||
? false
|
||||
: isExplicit;
|
||||
var tag = "";
|
||||
tag += string.Join('+', tags.Select(x => x.Replace(" ", "_", StringComparison.InvariantCulture).ToLowerInvariant()));
|
||||
if (isExplicit)
|
||||
tag = "rating%3Aexplicit+" + tag;
|
||||
var website = "";
|
||||
switch (type)
|
||||
{
|
||||
case DapiSearchType.Safebooru:
|
||||
website = $"https://safebooru.org/index.php?page=dapi&s=post&q=index&limit=1000&tags={tag}&json=1";
|
||||
break;
|
||||
case DapiSearchType.E621:
|
||||
website = $"https://e621.net/posts.json?limit=200&tags={tag}";
|
||||
break;
|
||||
case DapiSearchType.Danbooru:
|
||||
website = $"http://danbooru.donmai.us/posts.json?limit=100&tags={tag}";
|
||||
break;
|
||||
case DapiSearchType.Gelbooru:
|
||||
website = $"http://gelbooru.com/index.php?page=dapi&s=post&q=index&limit=100&tags={tag}";
|
||||
break;
|
||||
case DapiSearchType.Rule34:
|
||||
website = $"https://rule34.xxx/index.php?page=dapi&s=post&q=index&limit=100&tags={tag}";
|
||||
break;
|
||||
case DapiSearchType.Konachan:
|
||||
website = $"https://konachan.com/post.json?s=post&q=index&limit=100&tags={tag}";
|
||||
break;
|
||||
case DapiSearchType.Yandere:
|
||||
website = $"https://yande.re/post.json?limit=100&tags={tag}";
|
||||
break;
|
||||
case DapiSearchType.Derpibooru:
|
||||
tag = string.IsNullOrWhiteSpace(tag) ? "safe" : tag;
|
||||
website = $"https://www.derpibooru.org/api/v1/json/search/images?q={tag?.Replace('+', ',')}&per_page=49";
|
||||
break;
|
||||
case DapiSearchType.Sankaku:
|
||||
website = $"https://capi-v2.sankakucomplex.com/posts?tags={tag}&limit=50";
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using (var _http = _httpFactory.CreateClient())
|
||||
{
|
||||
_http.AddFakeHeaders();
|
||||
if (type == DapiSearchType.Konachan || type == DapiSearchType.Yandere || type == DapiSearchType.Danbooru)
|
||||
{
|
||||
var data = await _http.GetStringAsync(website).ConfigureAwait(false);
|
||||
return JsonConvert.DeserializeObject<DapiImageObject[]>(data)
|
||||
.Where(x => x.FileUrl != null)
|
||||
.Select(x => new ImageCacherObject(x, type))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
if (type == DapiSearchType.Sankaku)
|
||||
{
|
||||
var data = await _http.GetStringAsync(website).ConfigureAwait(false);
|
||||
return JsonConvert.DeserializeObject<SankakuImageObject[]>(data)
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.FileUrl) && x.FileType.StartsWith("image"))
|
||||
.Select(x => new ImageCacherObject(
|
||||
x.FileUrl,
|
||||
DapiSearchType.Sankaku,
|
||||
x.Tags.Select(x => x.Name).JoinWith(','),
|
||||
x.Score))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
if (type == DapiSearchType.E621)
|
||||
{
|
||||
var data = await _http.GetStringAsync(website).ConfigureAwait(false);
|
||||
return JsonConvert.DeserializeAnonymousType(data, new { posts = new List<E621Object>() })
|
||||
.posts
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.File?.Url))
|
||||
.Select(x => new ImageCacherObject(x.File.Url,
|
||||
type, string.Join(' ', x.Tags.General), x.Score.Total))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
if (type == DapiSearchType.Derpibooru)
|
||||
{
|
||||
var data = await _http.GetStringAsync(website).ConfigureAwait(false);
|
||||
return JsonConvert.DeserializeObject<DerpiContainer>(data)
|
||||
.Images
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x.ViewUrl))
|
||||
.Select(x => new ImageCacherObject(x.ViewUrl,
|
||||
type, string.Join("\n", x.Tags), x.Score))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
if (type == DapiSearchType.Safebooru)
|
||||
{
|
||||
var data = await _http.GetStringAsync(website).ConfigureAwait(false);
|
||||
return JsonConvert.DeserializeObject<SafebooruElement[]>(data)
|
||||
.Select(x => new ImageCacherObject(x.FileUrl, type, x.Tags, x.Rating))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
return (await LoadXmlAsync(website, type).ConfigureAwait(false)).ToArray();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error downloading an image: {Message}", ex.Message);
|
||||
return Array.Empty<ImageCacherObject>();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<ImageCacherObject[]> LoadXmlAsync(string website, DapiSearchType type)
|
||||
{
|
||||
var list = new List<ImageCacherObject>();
|
||||
using (var http = _httpFactory.CreateClient())
|
||||
using (var stream = await http.GetStreamAsync(website).ConfigureAwait(false))
|
||||
using (var reader = XmlReader.Create(stream, new XmlReaderSettings()
|
||||
{
|
||||
Async = true,
|
||||
}))
|
||||
{
|
||||
while (await reader.ReadAsync().ConfigureAwait(false))
|
||||
{
|
||||
if (reader.NodeType == XmlNodeType.Element &&
|
||||
reader.Name == "post")
|
||||
{
|
||||
list.Add(new ImageCacherObject(new DapiImageObject()
|
||||
{
|
||||
FileUrl = reader["file_url"],
|
||||
Tags = reader["tags"],
|
||||
Rating = reader["rating"] ?? "e"
|
||||
|
||||
}, type));
|
||||
}
|
||||
}
|
||||
}
|
||||
return list.ToArray();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_cache.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class DapiImageObject
|
||||
{
|
||||
[JsonProperty("File_Url")]
|
||||
public string FileUrl { get; set; }
|
||||
public string Tags { get; set; }
|
||||
[JsonProperty("Tag_String")]
|
||||
public string TagString { get; set; }
|
||||
public string Rating { get; set; }
|
||||
}
|
||||
|
||||
public class DerpiContainer
|
||||
{
|
||||
public DerpiImageObject[] Images { get; set; }
|
||||
}
|
||||
|
||||
public class DerpiImageObject
|
||||
{
|
||||
[JsonProperty("view_url")]
|
||||
public string ViewUrl { get; set; }
|
||||
public string[] Tags { get; set; }
|
||||
public string Score { get; set; }
|
||||
}
|
||||
|
||||
public class SankakuImageObject
|
||||
{
|
||||
public class Tag
|
||||
{
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
[JsonProperty("file_url")]
|
||||
public string FileUrl { get; set; }
|
||||
|
||||
[JsonProperty("file_type")]
|
||||
public string FileType { get; set; }
|
||||
|
||||
public Tag[] Tags { get; set; }
|
||||
|
||||
[JsonProperty("total_score")]
|
||||
public string Score { get; set; }
|
||||
}
|
||||
|
||||
public enum DapiSearchType
|
||||
{
|
||||
Safebooru,
|
||||
E621,
|
||||
Derpibooru,
|
||||
Gelbooru,
|
||||
Konachan,
|
||||
Rule34,
|
||||
Yandere,
|
||||
Danbooru,
|
||||
Sankaku,
|
||||
}
|
||||
public class SafebooruElement
|
||||
{
|
||||
public string Directory { get; set; }
|
||||
public string Image { get; set; }
|
||||
|
||||
|
||||
public string FileUrl => $"https://safebooru.org/images/{Directory}/{Image}";
|
||||
public string Rating { get; set; }
|
||||
public string Tags { get; set; }
|
||||
}
|
||||
}
|
35
src/NadekoBot/Modules/Searches/Common/SteamGameId.cs
Normal file
35
src/NadekoBot/Modules/Searches/Common/SteamGameId.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NadekoBot.Modules.Searches
|
||||
{
|
||||
public class SteamGameId
|
||||
{
|
||||
[JsonProperty("name")]
|
||||
public string Name { get; set; }
|
||||
|
||||
[JsonProperty("appid")]
|
||||
public int AppId { get; set; }
|
||||
}
|
||||
|
||||
public class SteamGameData
|
||||
{
|
||||
public string ShortDescription { get; set; }
|
||||
|
||||
public class Container
|
||||
{
|
||||
[JsonProperty("success")]
|
||||
public bool Success { get; set; }
|
||||
|
||||
[JsonProperty("data")]
|
||||
public SteamGameData Data { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
public enum TimeErrors
|
||||
{
|
||||
InvalidInput,
|
||||
ApiKeyMissing,
|
||||
NotFound,
|
||||
Unknown
|
||||
}
|
||||
}
|
@@ -0,0 +1,114 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Searches.Common
|
||||
{
|
||||
public class PicartoChannelResponse
|
||||
{
|
||||
[JsonProperty("user_id")] public int UserId { get; set; }
|
||||
|
||||
[JsonProperty("name")] public string Name { get; set; }
|
||||
|
||||
[JsonProperty("avatar")] public string Avatar { get; set; }
|
||||
|
||||
[JsonProperty("online")] public bool Online { get; set; }
|
||||
|
||||
[JsonProperty("viewers")] public int Viewers { get; set; }
|
||||
|
||||
[JsonProperty("viewers_total")] public int ViewersTotal { get; set; }
|
||||
|
||||
[JsonProperty("thumbnails")] public Thumbnails Thumbnails { get; set; }
|
||||
|
||||
[JsonProperty("followers")] public int Followers { get; set; }
|
||||
|
||||
[JsonProperty("subscribers")] public int Subscribers { get; set; }
|
||||
|
||||
[JsonProperty("adult")] public bool Adult { get; set; }
|
||||
|
||||
[JsonProperty("category")] public string Category { get; set; }
|
||||
|
||||
[JsonProperty("account_type")] public string AccountType { get; set; }
|
||||
|
||||
[JsonProperty("commissions")] public bool Commissions { get; set; }
|
||||
|
||||
[JsonProperty("recordings")] public bool Recordings { get; set; }
|
||||
|
||||
[JsonProperty("title")] public string Title { get; set; }
|
||||
|
||||
[JsonProperty("description_panels")] public List<DescriptionPanel> DescriptionPanels { get; set; }
|
||||
|
||||
[JsonProperty("private")] public bool Private { get; set; }
|
||||
|
||||
[JsonProperty("private_message")] public string PrivateMessage { get; set; }
|
||||
|
||||
[JsonProperty("gaming")] public bool Gaming { get; set; }
|
||||
|
||||
[JsonProperty("chat_settings")] public ChatSettings ChatSettings { get; set; }
|
||||
|
||||
[JsonProperty("last_live")] public DateTime LastLive { get; set; }
|
||||
|
||||
[JsonProperty("tags")] public List<string> Tags { get; set; }
|
||||
|
||||
[JsonProperty("multistream")] public List<Multistream> Multistream { get; set; }
|
||||
|
||||
[JsonProperty("languages")] public List<Language> Languages { get; set; }
|
||||
|
||||
[JsonProperty("following")] public bool Following { get; set; }
|
||||
}
|
||||
public class Thumbnails
|
||||
{
|
||||
[JsonProperty("web")] public string Web { get; set; }
|
||||
|
||||
[JsonProperty("web_large")] public string WebLarge { get; set; }
|
||||
|
||||
[JsonProperty("mobile")] public string Mobile { get; set; }
|
||||
|
||||
[JsonProperty("tablet")] public string Tablet { get; set; }
|
||||
}
|
||||
|
||||
public class DescriptionPanel
|
||||
{
|
||||
[JsonProperty("title")] public string Title { get; set; }
|
||||
|
||||
[JsonProperty("body")] public string Body { get; set; }
|
||||
|
||||
[JsonProperty("image")] public string Image { get; set; }
|
||||
|
||||
[JsonProperty("image_link")] public string ImageLink { get; set; }
|
||||
|
||||
[JsonProperty("button_text")] public string ButtonText { get; set; }
|
||||
|
||||
[JsonProperty("button_link")] public string ButtonLink { get; set; }
|
||||
|
||||
[JsonProperty("position")] public int Position { get; set; }
|
||||
}
|
||||
|
||||
public class ChatSettings
|
||||
{
|
||||
[JsonProperty("guest_chat")] public bool GuestChat { get; set; }
|
||||
|
||||
[JsonProperty("links")] public bool Links { get; set; }
|
||||
|
||||
[JsonProperty("level")] public int Level { get; set; }
|
||||
}
|
||||
|
||||
public class Multistream
|
||||
{
|
||||
[JsonProperty("user_id")] public int UserId { get; set; }
|
||||
|
||||
[JsonProperty("name")] public string Name { get; set; }
|
||||
|
||||
[JsonProperty("online")] public bool Online { get; set; }
|
||||
|
||||
[JsonProperty("adult")] public bool Adult { get; set; }
|
||||
}
|
||||
|
||||
public class Language
|
||||
{
|
||||
[JsonProperty("id")] public int Id { get; set; }
|
||||
|
||||
[JsonProperty("name")] public string Name { get; set; }
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,20 @@
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Searches.Common
|
||||
{
|
||||
public class StreamData
|
||||
{
|
||||
public FollowedStream.FType StreamType { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string UniqueName { get; set; }
|
||||
public int Viewers { get; set; }
|
||||
public string Title { get; set; }
|
||||
public string Game { get; set; }
|
||||
public string Preview { get; set; }
|
||||
public bool IsLive { get; set; }
|
||||
public string StreamUrl { get; set; }
|
||||
public string AvatarUrl { get; set; }
|
||||
|
||||
public StreamDataKey CreateKey() => new StreamDataKey(StreamType, UniqueName.ToLower());
|
||||
}
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Searches.Common
|
||||
{
|
||||
public readonly struct StreamDataKey
|
||||
{
|
||||
public FollowedStream.FType Type { get; }
|
||||
public string Name { get; }
|
||||
|
||||
public StreamDataKey(FollowedStream.FType type, string name)
|
||||
{
|
||||
Type = type;
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
namespace NadekoBot.Core.Modules.Searches.Common
|
||||
{
|
||||
//
|
||||
// public class TwitchResponse
|
||||
// {
|
||||
// public List<StreamApiData> Data { get; set; }
|
||||
//
|
||||
// public class StreamApiData
|
||||
// {
|
||||
// public string Id { get; set; }
|
||||
// public string UserId { get; set; }
|
||||
// public string UserName { get; set; }
|
||||
// public string GameId { get; set; }
|
||||
// public string Type { get; set; }
|
||||
// public string Title { get; set; }
|
||||
// public int ViewerCount { get; set; }
|
||||
// public string Language { get; set; }
|
||||
// public string ThumbnailUrl { get; set; }
|
||||
// public DateTime StartedAt { get; set; }
|
||||
// }
|
||||
// }
|
||||
}
|
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Searches.Common
|
||||
{
|
||||
public class TwitchResponseV5
|
||||
{
|
||||
public List<Stream> Streams { get; set; }
|
||||
|
||||
public class Channel
|
||||
{
|
||||
[JsonProperty("_id")] public int Id { get; set; }
|
||||
|
||||
[JsonProperty("broadcaster_language")] public string BroadcasterLanguage { get; set; }
|
||||
|
||||
[JsonProperty("created_at")] public DateTime CreatedAt { get; set; }
|
||||
|
||||
[JsonProperty("display_name")] public string DisplayName { get; set; }
|
||||
|
||||
[JsonProperty("followers")] public int Followers { get; set; }
|
||||
|
||||
[JsonProperty("game")] public string Game { get; set; }
|
||||
|
||||
[JsonProperty("language")] public string Language { get; set; }
|
||||
|
||||
[JsonProperty("logo")] public string Logo { get; set; }
|
||||
|
||||
[JsonProperty("mature")] public bool Mature { get; set; }
|
||||
|
||||
[JsonProperty("name")] public string Name { get; set; }
|
||||
|
||||
[JsonProperty("partner")] public bool Partner { get; set; }
|
||||
|
||||
[JsonProperty("profile_banner")] public string ProfileBanner { get; set; }
|
||||
|
||||
[JsonProperty("profile_banner_background_color")]
|
||||
public object ProfileBannerBackgroundColor { get; set; }
|
||||
|
||||
[JsonProperty("status")] public string Status { get; set; }
|
||||
|
||||
[JsonProperty("updated_at")] public DateTime UpdatedAt { get; set; }
|
||||
|
||||
[JsonProperty("url")] public string Url { get; set; }
|
||||
|
||||
[JsonProperty("video_banner")] public string VideoBanner { get; set; }
|
||||
|
||||
[JsonProperty("views")] public int Views { get; set; }
|
||||
}
|
||||
|
||||
public class Preview
|
||||
{
|
||||
[JsonProperty("large")] public string Large { get; set; }
|
||||
|
||||
[JsonProperty("medium")] public string Medium { get; set; }
|
||||
|
||||
[JsonProperty("small")] public string Small { get; set; }
|
||||
|
||||
[JsonProperty("template")] public string Template { get; set; }
|
||||
}
|
||||
|
||||
public class Stream
|
||||
{
|
||||
[JsonProperty("_id")] public long Id { get; set; }
|
||||
|
||||
[JsonProperty("average_fps")] public double AverageFps { get; set; }
|
||||
|
||||
[JsonProperty("channel")] public Channel Channel { get; set; }
|
||||
|
||||
[JsonProperty("created_at")] public DateTime CreatedAt { get; set; }
|
||||
|
||||
[JsonProperty("delay")] public double Delay { get; set; }
|
||||
|
||||
[JsonProperty("game")] public string Game { get; set; }
|
||||
|
||||
[JsonProperty("is_playlist")] public bool IsPlaylist { get; set; }
|
||||
|
||||
[JsonProperty("preview")] public Preview Preview { get; set; }
|
||||
|
||||
[JsonProperty("video_height")] public int VideoHeight { get; set; }
|
||||
|
||||
[JsonProperty("viewers")] public int Viewers { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Searches.Common
|
||||
{
|
||||
public class TwitchUsersResponseV5
|
||||
{
|
||||
[JsonProperty("users")] public List<User> Users { get; set; }
|
||||
|
||||
public class User
|
||||
{
|
||||
|
||||
[JsonProperty("_id")]
|
||||
public string Id { get; set; }
|
||||
|
||||
// [JsonProperty("bio")]
|
||||
// public string Bio { get; set; }
|
||||
//
|
||||
// [JsonProperty("created_at")]
|
||||
// public DateTime CreatedAt { get; set; }
|
||||
//
|
||||
// [JsonProperty("display_name")]
|
||||
// public string DisplayName { get; set; }
|
||||
//
|
||||
// [JsonProperty("logo")]
|
||||
// public string Logo { get; set; }
|
||||
//
|
||||
// [JsonProperty("name")]
|
||||
// public string Name { get; set; }
|
||||
//
|
||||
// [JsonProperty("type")]
|
||||
// public string Type { get; set; }
|
||||
//
|
||||
// [JsonProperty("updated_at")]
|
||||
// public DateTime UpdatedAt { get; set; }
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,249 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Core.Modules.Searches.Common.StreamNotifications.Providers;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Serilog;
|
||||
using StackExchange.Redis;
|
||||
|
||||
#nullable enable
|
||||
namespace NadekoBot.Core.Modules.Searches.Common.StreamNotifications
|
||||
{
|
||||
public class NotifChecker
|
||||
{
|
||||
private readonly ConnectionMultiplexer _multi;
|
||||
private readonly string _key;
|
||||
|
||||
public event Func<List<StreamData>, Task> OnStreamsOffline = _ => Task.CompletedTask;
|
||||
public event Func<List<StreamData>, Task> OnStreamsOnline = _ => Task.CompletedTask;
|
||||
|
||||
private readonly Dictionary<FollowedStream.FType, Provider> _streamProviders;
|
||||
private readonly HashSet<(FollowedStream.FType, string)> _offlineBuffer;
|
||||
|
||||
public NotifChecker(IHttpClientFactory httpClientFactory, ConnectionMultiplexer multi, string uniqueCacheKey,
|
||||
bool isMaster)
|
||||
{
|
||||
_multi = multi;
|
||||
_key = $"{uniqueCacheKey}_followed_streams_data";
|
||||
_streamProviders = new Dictionary<FollowedStream.FType, Provider>()
|
||||
{
|
||||
{FollowedStream.FType.Twitch, new TwitchProvider(httpClientFactory)},
|
||||
{FollowedStream.FType.Picarto, new PicartoProvider(httpClientFactory)}
|
||||
};
|
||||
_offlineBuffer = new HashSet<(FollowedStream.FType, string)>();
|
||||
if (isMaster)
|
||||
{
|
||||
CacheClearAllData();
|
||||
}
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
if (remove)
|
||||
{
|
||||
foreach (var toBeRemoved in toReturn)
|
||||
{
|
||||
_streamProviders[toBeRemoved.Type].ClearErrorsFor(toBeRemoved.Name);
|
||||
}
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
public Task RunAsync() => Task.Run(async () =>
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var allStreamData = CacheGetAllData();
|
||||
|
||||
var oldStreamDataDict = allStreamData
|
||||
// group by type
|
||||
.GroupBy(entry => entry.Key.Type)
|
||||
.ToDictionary(
|
||||
entry => entry.Key,
|
||||
entry => entry.AsEnumerable().ToDictionary(x => x.Key.Name, x => x.Value)
|
||||
);
|
||||
|
||||
var newStreamData = await Task.WhenAll(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>());
|
||||
}));
|
||||
|
||||
var newlyOnline = new List<StreamData>();
|
||||
var newlyOffline = new List<StreamData>();
|
||||
// go through all new stream data, compare them with the old ones
|
||||
foreach (var newData in newStreamData.SelectMany(x => x))
|
||||
{
|
||||
// update cached data
|
||||
var key = newData.CreateKey();
|
||||
CacheAddData(key, newData, replace: true);
|
||||
|
||||
// compare old data with new data
|
||||
var oldData = oldStreamDataDict[key.Type][key.Name];
|
||||
|
||||
// this is the first pass
|
||||
if (oldData is null)
|
||||
continue;
|
||||
|
||||
// if the stream is offline, we need to check if it was
|
||||
// marked as offline once previously
|
||||
// if it was, that means this is second time we're getting offline
|
||||
// status for that stream -> notify subscribers
|
||||
// Note: This is done because twitch api will sometimes return an offline status
|
||||
// shortly after the stream is already online, which causes duplicate notifications.
|
||||
// (stream is online -> stream is offline -> stream is online again (and stays online))
|
||||
// This offlineBuffer will make it so that the stream has to be marked as offline TWICE
|
||||
// before it sends an offline notification to the subscribers.
|
||||
var streamId = (key.Type, key.Name);
|
||||
if (!newData.IsLive && _offlineBuffer.Remove(streamId))
|
||||
{
|
||||
newlyOffline.Add(newData);
|
||||
}
|
||||
else if (newData.IsLive != oldData.IsLive)
|
||||
{
|
||||
if (newData.IsLive)
|
||||
{
|
||||
_offlineBuffer.Remove(streamId);
|
||||
newlyOnline.Add(newData);
|
||||
}
|
||||
else
|
||||
{
|
||||
_offlineBuffer.Add(streamId);
|
||||
// newlyOffline.Add(newData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tasks = new List<Task>
|
||||
{
|
||||
Task.Delay(30_000)
|
||||
};
|
||||
|
||||
if (newlyOnline.Count > 0)
|
||||
{
|
||||
tasks.Add(OnStreamsOnline(newlyOnline));
|
||||
}
|
||||
|
||||
if (newlyOffline.Count > 0)
|
||||
{
|
||||
tasks.Add(OnStreamsOffline(newlyOffline));
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, $"Error getting stream notifications: {ex.Message}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public bool CacheAddData(StreamDataKey key, StreamData? data, bool replace)
|
||||
{
|
||||
var db = _multi.GetDatabase();
|
||||
return db.HashSet(
|
||||
_key,
|
||||
JsonConvert.SerializeObject(key),
|
||||
JsonConvert.SerializeObject(data),
|
||||
when: replace ? When.Always : When.NotExists);
|
||||
}
|
||||
|
||||
public void CacheDeleteData(StreamDataKey key)
|
||||
{
|
||||
var db = _multi.GetDatabase();
|
||||
db.HashDelete(_key, JsonConvert.SerializeObject(key));
|
||||
}
|
||||
|
||||
public void CacheClearAllData()
|
||||
{
|
||||
var db = _multi.GetDatabase();
|
||||
db.KeyDelete(_key);
|
||||
}
|
||||
|
||||
public Dictionary<StreamDataKey, StreamData?> CacheGetAllData()
|
||||
{
|
||||
var db = _multi.GetDatabase();
|
||||
if (!db.KeyExists(_key))
|
||||
{
|
||||
return new Dictionary<StreamDataKey, StreamData?>();
|
||||
}
|
||||
|
||||
return db.HashGetAll(_key)
|
||||
.ToDictionary(
|
||||
entry => JsonConvert.DeserializeObject<StreamDataKey>(entry.Name),
|
||||
entry => entry.Value.IsNullOrEmpty
|
||||
? default(StreamData)
|
||||
: JsonConvert.DeserializeObject<StreamData>(entry.Value));
|
||||
}
|
||||
|
||||
public async Task<StreamData?> GetStreamDataByUrlAsync(string url)
|
||||
{
|
||||
// loop through all providers and see which regex matches
|
||||
foreach (var (_, provider) in _streamProviders)
|
||||
{
|
||||
var isValid = await provider.IsValidUrl(url);
|
||||
if (!isValid)
|
||||
continue;
|
||||
// if it's not a valid url, try another provider
|
||||
var data = await provider.GetStreamDataByUrlAsync(url);
|
||||
return data;
|
||||
}
|
||||
|
||||
// if no provider found, return null
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return currently available stream data, get new one if none available, and start tracking the stream.
|
||||
/// </summary>
|
||||
/// <param name="url">Url of the stream</param>
|
||||
/// <returns>Stream data, if any</returns>
|
||||
public async Task<StreamData?> TrackStreamByUrlAsync(string url)
|
||||
{
|
||||
var data = await GetStreamDataByUrlAsync(url);
|
||||
EnsureTracked(data);
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make sure a stream is tracked using its stream data.
|
||||
/// </summary>
|
||||
/// <param name="data">Data to try to track if not already tracked</param>
|
||||
/// <returns>Whether it's newly added</returns>
|
||||
private bool EnsureTracked(StreamData? data)
|
||||
{
|
||||
// something failed, don't add anything to cache
|
||||
if (data is null)
|
||||
return false;
|
||||
|
||||
// if stream is found, add it to the cache for tracking only if it doesn't already exist
|
||||
// because stream will be checked and events will fire in a loop. We don't want to override old state
|
||||
return CacheAddData(data.CreateKey(), data, replace: false);
|
||||
}
|
||||
|
||||
public void UntrackStreamByKey(in StreamDataKey key)
|
||||
{
|
||||
CacheDeleteData(key);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,110 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Serilog;
|
||||
|
||||
#nullable enable
|
||||
namespace NadekoBot.Core.Modules.Searches.Common.StreamNotifications.Providers
|
||||
{
|
||||
public class PicartoProvider : Provider
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
private static Regex Regex { get; } = new Regex(@"picarto.tv/(?<name>.+[^/])/?",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
public override FollowedStream.FType Platform => FollowedStream.FType.Picarto;
|
||||
|
||||
public PicartoProvider(IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
public override Task<bool> IsValidUrl(string url)
|
||||
{
|
||||
var match = Regex.Match(url);
|
||||
if (!match.Success)
|
||||
return Task.FromResult(false);
|
||||
|
||||
// var username = match.Groups["name"].Value;
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public override Task<StreamData?> GetStreamDataByUrlAsync(string url)
|
||||
{
|
||||
var match = Regex.Match(url);
|
||||
if (match.Success)
|
||||
{
|
||||
var name = match.Groups["name"].Value;
|
||||
return GetStreamDataAsync(name);
|
||||
}
|
||||
|
||||
return Task.FromResult<StreamData?>(null);
|
||||
}
|
||||
|
||||
public override async Task<StreamData?> GetStreamDataAsync(string id)
|
||||
{
|
||||
var data = await GetStreamDataAsync(new List<string> {id});
|
||||
|
||||
return data.FirstOrDefault();
|
||||
}
|
||||
|
||||
public async override Task<List<StreamData>> GetStreamDataAsync(List<string> logins)
|
||||
{
|
||||
if (logins.Count == 0)
|
||||
return new List<StreamData>();
|
||||
|
||||
using (var http = _httpClientFactory.CreateClient())
|
||||
{
|
||||
var toReturn = new List<StreamData>();
|
||||
foreach (var login in logins)
|
||||
{
|
||||
try
|
||||
{
|
||||
http.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
// get id based on the username
|
||||
var res = await http.GetAsync($"https://api.picarto.tv/v1/channel/name/{login}");
|
||||
|
||||
if (!res.IsSuccessStatusCode)
|
||||
continue;
|
||||
|
||||
var userData = JsonConvert.DeserializeObject<PicartoChannelResponse>(await res.Content.ReadAsStringAsync());
|
||||
|
||||
toReturn.Add(ToStreamData(userData));
|
||||
_failingStreams.TryRemove(login, out _);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, $"Something went wrong retreiving {Platform} stream data for {login}: {ex.Message}");
|
||||
_failingStreams.TryAdd(login, DateTime.UtcNow);
|
||||
}
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
}
|
||||
|
||||
private StreamData ToStreamData(PicartoChannelResponse stream)
|
||||
{
|
||||
return new StreamData()
|
||||
{
|
||||
StreamType = FollowedStream.FType.Picarto,
|
||||
Name = stream.Name,
|
||||
UniqueName = stream.Name,
|
||||
Viewers = stream.Viewers,
|
||||
Title = stream.Title,
|
||||
IsLive = stream.Online,
|
||||
Preview = stream.Thumbnails.Web,
|
||||
Game = stream.Category,
|
||||
StreamUrl = $"https://picarto.tv/{stream.Name}",
|
||||
AvatarUrl = stream.Avatar
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
|
||||
#nullable enable
|
||||
namespace NadekoBot.Core.Modules.Searches.Common.StreamNotifications.Providers
|
||||
{
|
||||
/// <summary>
|
||||
/// Abstract class implemented by providers of all supported platforms
|
||||
/// </summary>
|
||||
public abstract class Provider
|
||||
{
|
||||
/// <summary>
|
||||
/// Type of the platform.
|
||||
/// </summary>
|
||||
public abstract FollowedStream.FType Platform { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the specified url is a valid stream url for this platform.
|
||||
/// </summary>
|
||||
/// <param name="url">Url to check</param>
|
||||
/// <returns>True if valid, otherwise false</returns>
|
||||
public abstract Task<bool> IsValidUrl(string url);
|
||||
|
||||
/// <summary>
|
||||
/// Gets stream data of the stream on the specified url on this <see cref="Platform"/>
|
||||
/// </summary>
|
||||
/// <param name="url">Url of the stream</param>
|
||||
/// <returns><see cref="StreamData"/> of the specified stream. Null if none found</returns>
|
||||
public abstract Task<StreamData?> GetStreamDataByUrlAsync(string url);
|
||||
|
||||
/// <summary>
|
||||
/// Gets stream data of the specified id/username on this <see cref="Platform"/>
|
||||
/// </summary>
|
||||
/// <param name="id">Name (or id where applicable) of the user on the platform</param>
|
||||
/// <returns><see cref="StreamData"/> of the user. Null if none found</returns>
|
||||
public abstract Task<StreamData?> GetStreamDataAsync(string id);
|
||||
|
||||
/// <summary>
|
||||
/// Gets stream data of all specified ids/usernames on this <see cref="Platform"/>
|
||||
/// </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);
|
||||
|
||||
/// <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.
|
||||
/// </summary>
|
||||
public IEnumerable<(string Login, DateTime ErroringSince)> FailingStreams =>
|
||||
_failingStreams.Select(entry => (entry.Key, entry.Value)).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// When was the first time the stream continually had errors while being retrieved
|
||||
/// </summary>
|
||||
protected readonly ConcurrentDictionary<string, DateTime> _failingStreams =
|
||||
new ConcurrentDictionary<string, DateTime>();
|
||||
|
||||
public void ClearErrorsFor(string login)
|
||||
=> _failingStreams.TryRemove(login, out _);
|
||||
}
|
||||
}
|
@@ -0,0 +1,180 @@
|
||||
// using System;
|
||||
// using System.Collections.Generic;
|
||||
// using System.Linq;
|
||||
// using System.Net.Http;
|
||||
// using System.Text.Json;
|
||||
// using System.Text.Json.Serialization;
|
||||
// using System.Text.RegularExpressions;
|
||||
// using System.Threading.Tasks;
|
||||
// using NadekoBot.Core.Services.Database.Models;
|
||||
// using NadekoBot.Extensions;
|
||||
// using Serilog;
|
||||
// using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||
//
|
||||
// namespace NadekoBot.Core.Modules.Searches.Common.StreamNotifications.Providers
|
||||
// {
|
||||
// public sealed class TwitchHelixProvider : Provider
|
||||
// {
|
||||
// private readonly IHttpClientFactory _httpClientFactory;
|
||||
//
|
||||
// //
|
||||
// private static Regex Regex { get; } = new Regex(@"twitch.tv/(?<name>.+[^/])/?",
|
||||
// RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
//
|
||||
// public override FollowedStream.FType Platform => FollowedStream.FType.Twitch;
|
||||
//
|
||||
// private (string Token, DateTime Expiry) _token = default;
|
||||
//
|
||||
// public TwitchHelixProvider(IHttpClientFactory httpClientFactory)
|
||||
// {
|
||||
// _httpClientFactory = httpClientFactory;
|
||||
// }
|
||||
//
|
||||
// private async Task EnsureTokenValidAsync()
|
||||
// {
|
||||
// if (_token != default && (DateTime.UtcNow - _token.Expiry) > TimeSpan.FromHours(1))
|
||||
// return;
|
||||
//
|
||||
// const string clientId = "";
|
||||
// const string clientSecret = "";
|
||||
//
|
||||
// var client = _httpClientFactory.CreateClient();
|
||||
// var res = await client.PostAsync("https://id.twitch.tv/oauth2/token" +
|
||||
// $"?client_id={clientId}" +
|
||||
// $"&client_secret={clientSecret}" +
|
||||
// "&grant_type=client_credentials", new StringContent(""));
|
||||
//
|
||||
// var data = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
|
||||
//
|
||||
// _token = (data.GetProperty("access_token").GetString(),
|
||||
// DateTime.UtcNow + TimeSpan.FromSeconds(data.GetProperty("expires_in").GetInt32()));
|
||||
//
|
||||
// }
|
||||
//
|
||||
// public override Task<bool> IsValidUrl(string url)
|
||||
// {
|
||||
// var match = Regex.Match(url);
|
||||
// if (!match.Success)
|
||||
// return Task.FromResult(false);
|
||||
//
|
||||
// var username = match.Groups["name"].Value;
|
||||
// return Task.FromResult(true);
|
||||
// }
|
||||
//
|
||||
// public override Task<StreamData?> GetStreamDataByUrlAsync(string url)
|
||||
// {
|
||||
// var match = Regex.Match(url);
|
||||
// if (match.Success)
|
||||
// {
|
||||
// var name = match.Groups["name"].Value;
|
||||
// return GetStreamDataAsync(name);
|
||||
// }
|
||||
//
|
||||
// return Task.FromResult<StreamData?>(null);
|
||||
// }
|
||||
//
|
||||
// public override async Task<StreamData?> GetStreamDataAsync(string id)
|
||||
// {
|
||||
// var data = await GetStreamDataAsync(new List<string> {id});
|
||||
//
|
||||
// return data.FirstOrDefault();
|
||||
// }
|
||||
//
|
||||
// public override async Task<List<StreamData>> GetStreamDataAsync(List<string> logins)
|
||||
// {
|
||||
// if (logins.Count == 0)
|
||||
// return new List<StreamData>();
|
||||
//
|
||||
// await EnsureTokenValidAsync();
|
||||
//
|
||||
// using var http = _httpClientFactory.CreateClient();
|
||||
// http.DefaultRequestHeaders.Clear();
|
||||
// http.DefaultRequestHeaders.Add("client-id","67w6z9i09xv2uoojdm9l0wsyph4hxo6");
|
||||
// http.DefaultRequestHeaders.Add("Authorization",$"Bearer {_token.Token}");
|
||||
//
|
||||
// var res = new TwitchResponse()
|
||||
// {
|
||||
// Data = new List<TwitchResponse.StreamApiData>()
|
||||
// };
|
||||
// foreach (var chunk in logins.Chunk(500))
|
||||
// {
|
||||
// try
|
||||
// {
|
||||
// var str = await http.GetStringAsync($"https://api.twitch.tv/helix/streams" +
|
||||
// $"?user_login={chunk.JoinWith(',')}" +
|
||||
// $"&first=100");
|
||||
//
|
||||
// res = JsonSerializer.Deserialize<TwitchResponse>(str);
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Log.Warning(ex, "Something went wrong retreiving {StreamPlatform} streams", Platform);
|
||||
// return new List<StreamData>();
|
||||
// }
|
||||
//
|
||||
// if (res.Data.Count == 0)
|
||||
// {
|
||||
// return new List<StreamData>();
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// return res.Data.Select(ToStreamData).ToList();
|
||||
// }
|
||||
//
|
||||
// private StreamData ToStreamData(TwitchResponse.StreamApiData apiData)
|
||||
// {
|
||||
// return new StreamData()
|
||||
// {
|
||||
// StreamType = FollowedStream.FType.Twitch,
|
||||
// Name = apiData.UserName,
|
||||
// UniqueName = apiData.UserId,
|
||||
// Viewers = apiData.ViewerCount,
|
||||
// Title = apiData.Title,
|
||||
// IsLive = apiData.Type == "live",
|
||||
// Preview = apiData.ThumbnailUrl
|
||||
// ?.Replace("{width}", "640")
|
||||
// ?.Replace("{height}", "480"),
|
||||
// Game = apiData.GameId,
|
||||
// };
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// public class TwitchResponse
|
||||
// {
|
||||
// [JsonPropertyName("data")]
|
||||
// public List<StreamApiData> Data { get; set; }
|
||||
//
|
||||
// public class StreamApiData
|
||||
// {
|
||||
// [JsonPropertyName("id")]
|
||||
// public string Id { get; set; }
|
||||
//
|
||||
// [JsonPropertyName("user_id")]
|
||||
// public string UserId { get; set; }
|
||||
//
|
||||
// [JsonPropertyName("user_name")]
|
||||
// public string UserName { get; set; }
|
||||
//
|
||||
// [JsonPropertyName("game_id")]
|
||||
// public string GameId { get; set; }
|
||||
//
|
||||
// [JsonPropertyName("type")]
|
||||
// public string Type { get; set; }
|
||||
//
|
||||
// [JsonPropertyName("title")]
|
||||
// public string Title { get; set; }
|
||||
//
|
||||
// [JsonPropertyName("viewer_count")]
|
||||
// public int ViewerCount { get; set; }
|
||||
//
|
||||
// [JsonPropertyName("language")]
|
||||
// public string Language { get; set; }
|
||||
//
|
||||
// [JsonPropertyName("thumbnail_url")]
|
||||
// public string ThumbnailUrl { get; set; }
|
||||
//
|
||||
// [JsonPropertyName("started_at")]
|
||||
// public DateTime StartedAt { get; set; }
|
||||
// }
|
||||
// }
|
||||
// }
|
@@ -0,0 +1,138 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using NadekoBot.Core.Services.Database.Models;
|
||||
using Newtonsoft.Json;
|
||||
using Serilog;
|
||||
|
||||
#nullable enable
|
||||
namespace NadekoBot.Core.Modules.Searches.Common.StreamNotifications.Providers
|
||||
{
|
||||
public class TwitchProvider : Provider
|
||||
{
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
private static Regex Regex { get; } = new Regex(@"twitch.tv/(?<name>.+[^/])/?",
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
public override FollowedStream.FType Platform => FollowedStream.FType.Twitch;
|
||||
|
||||
public TwitchProvider(IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
public override Task<bool> IsValidUrl(string url)
|
||||
{
|
||||
var match = Regex.Match(url);
|
||||
if (!match.Success)
|
||||
return Task.FromResult(false);
|
||||
|
||||
// var username = match.Groups["name"].Value;
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
public override Task<StreamData?> GetStreamDataByUrlAsync(string url)
|
||||
{
|
||||
var match = Regex.Match(url);
|
||||
if (match.Success)
|
||||
{
|
||||
var name = match.Groups["name"].Value;
|
||||
return GetStreamDataAsync(name);
|
||||
}
|
||||
|
||||
return Task.FromResult<StreamData?>(null);
|
||||
}
|
||||
|
||||
public override async Task<StreamData?> GetStreamDataAsync(string id)
|
||||
{
|
||||
var data = await GetStreamDataAsync(new List<string> {id});
|
||||
|
||||
return data.FirstOrDefault();
|
||||
}
|
||||
|
||||
public override async Task<List<StreamData>> GetStreamDataAsync(List<string> logins)
|
||||
{
|
||||
if (logins.Count == 0)
|
||||
return new List<StreamData>();
|
||||
|
||||
using (var http = _httpClientFactory.CreateClient())
|
||||
{
|
||||
http.DefaultRequestHeaders.Add("Client-Id", "67w6z9i09xv2uoojdm9l0wsyph4hxo6");
|
||||
http.DefaultRequestHeaders.Add("Accept", "application/vnd.twitchtv.v5+json");
|
||||
|
||||
var toReturn = new List<StreamData>();
|
||||
foreach (var login in logins)
|
||||
{
|
||||
try
|
||||
{
|
||||
// get id based on the username
|
||||
var idsStr = await http.GetStringAsync($"https://api.twitch.tv/kraken/users?login={login}");
|
||||
var userData = JsonConvert.DeserializeObject<TwitchUsersResponseV5>(idsStr);
|
||||
var user = userData.Users.FirstOrDefault();
|
||||
|
||||
// if user can't be found, skip, it means there is no such user
|
||||
if (user is null)
|
||||
continue;
|
||||
|
||||
// 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()});
|
||||
|
||||
// if stream is null, user is not streaming
|
||||
if (resObj.Stream is null)
|
||||
{
|
||||
// if user is not streaming, get his offline banner
|
||||
var chStr = await http.GetStringAsync($"https://api.twitch.tv/kraken/channels/{user.Id}");
|
||||
var ch = JsonConvert.DeserializeObject<TwitchResponseV5.Channel>(chStr);
|
||||
|
||||
toReturn.Add(new StreamData
|
||||
{
|
||||
StreamType = FollowedStream.FType.Twitch,
|
||||
Name = ch.DisplayName,
|
||||
UniqueName = ch.Name,
|
||||
Title = ch.Status,
|
||||
IsLive = false,
|
||||
AvatarUrl = ch.Logo,
|
||||
StreamUrl = $"https://twitch.tv/{ch.Name}",
|
||||
Preview = ch.VideoBanner // set video banner as the preview,
|
||||
});
|
||||
continue; // move on
|
||||
}
|
||||
|
||||
toReturn.Add(ToStreamData(resObj.Stream));
|
||||
_failingStreams.TryRemove(login, out _);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning($"Something went wrong retreiving {Platform} stream data for {login}: {ex.Message}");
|
||||
_failingStreams.TryAdd(login, DateTime.UtcNow);
|
||||
}
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
}
|
||||
}
|
||||
|
||||
private StreamData ToStreamData(TwitchResponseV5.Stream stream)
|
||||
{
|
||||
return new StreamData()
|
||||
{
|
||||
StreamType = FollowedStream.FType.Twitch,
|
||||
Name = stream.Channel.DisplayName,
|
||||
UniqueName = stream.Channel.Name,
|
||||
Viewers = stream.Viewers,
|
||||
Title = stream.Channel.Status,
|
||||
IsLive = true,
|
||||
Preview = stream.Preview.Large,
|
||||
Game = stream.Channel.Game,
|
||||
StreamUrl = $"https://twitch.tv/{stream.Channel.Name}",
|
||||
AvatarUrl = stream.Channel.Logo
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
11
src/NadekoBot/Modules/Searches/Common/TimeData.cs
Normal file
11
src/NadekoBot/Modules/Searches/Common/TimeData.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using System;
|
||||
|
||||
namespace NadekoBot.Core.Modules.Searches.Common
|
||||
{
|
||||
public class TimeData
|
||||
{
|
||||
public string Address { get; set; }
|
||||
public DateTime Time { get; set; }
|
||||
public string TimeZoneName { get; set; }
|
||||
}
|
||||
}
|
21
src/NadekoBot/Modules/Searches/Common/TimeModels.cs
Normal file
21
src/NadekoBot/Modules/Searches/Common/TimeModels.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common
|
||||
{
|
||||
public class TimeZoneResult
|
||||
{
|
||||
[JsonProperty("abbreviation")]
|
||||
public string TimezoneName { get; set; }
|
||||
[JsonProperty("timestamp")]
|
||||
public int Timestamp { get; set; }
|
||||
}
|
||||
|
||||
public class LocationIqResponse
|
||||
{
|
||||
public float Lat { get; set; }
|
||||
public float Lon { get; set; }
|
||||
|
||||
[JsonProperty("display_name")]
|
||||
public string DisplayName { get; set; }
|
||||
}
|
||||
}
|
13
src/NadekoBot/Modules/Searches/Common/UrbanDef.cs
Normal file
13
src/NadekoBot/Modules/Searches/Common/UrbanDef.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace NadekoBot.Core.Modules.Searches.Common
|
||||
{
|
||||
public class UrbanResponse
|
||||
{
|
||||
public UrbanDef[] List { get; set; }
|
||||
}
|
||||
public class UrbanDef
|
||||
{
|
||||
public string Word { get; set; }
|
||||
public string Definition { get; set; }
|
||||
public string Permalink { get; set; }
|
||||
}
|
||||
}
|
66
src/NadekoBot/Modules/Searches/Common/WeatherModels.cs
Normal file
66
src/NadekoBot/Modules/Searches/Common/WeatherModels.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using Newtonsoft.Json;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NadekoBot.Modules.Searches.Common
|
||||
{
|
||||
public class Coord
|
||||
{
|
||||
public double Lon { get; set; }
|
||||
public double Lat { get; set; }
|
||||
}
|
||||
|
||||
public class Weather
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Main { get; set; }
|
||||
public string Description { get; set; }
|
||||
public string Icon { get; set; }
|
||||
}
|
||||
|
||||
public class Main
|
||||
{
|
||||
public double Temp { get; set; }
|
||||
public float Pressure { get; set; }
|
||||
public float Humidity { get; set; }
|
||||
[JsonProperty("temp_min")]
|
||||
public double TempMin { get; set; }
|
||||
[JsonProperty("temp_max")]
|
||||
public double TempMax { get; set; }
|
||||
}
|
||||
|
||||
public class Wind
|
||||
{
|
||||
public double Speed { get; set; }
|
||||
public double Deg { get; set; }
|
||||
}
|
||||
|
||||
public class Clouds
|
||||
{
|
||||
public int All { get; set; }
|
||||
}
|
||||
|
||||
public class Sys
|
||||
{
|
||||
public int Type { get; set; }
|
||||
public int Id { get; set; }
|
||||
public double Message { get; set; }
|
||||
public string Country { get; set; }
|
||||
public double Sunrise { get; set; }
|
||||
public double Sunset { get; set; }
|
||||
}
|
||||
|
||||
public class WeatherData
|
||||
{
|
||||
public Coord Coord { get; set; }
|
||||
public List<Weather> Weather { get; set; }
|
||||
public Main Main { get; set; }
|
||||
public int Visibility { get; set; }
|
||||
public Wind Wind { get; set; }
|
||||
public Clouds Clouds { get; set; }
|
||||
public int Dt { get; set; }
|
||||
public Sys Sys { get; set; }
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public int Cod { get; set; }
|
||||
}
|
||||
}
|
18
src/NadekoBot/Modules/Searches/Common/WikipediaApiModel.cs
Normal file
18
src/NadekoBot/Modules/Searches/Common/WikipediaApiModel.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace NadekoBot.Modules.Searches.Common
|
||||
{
|
||||
public class WikipediaApiModel
|
||||
{
|
||||
public WikipediaQuery Query { get; set; }
|
||||
|
||||
public class WikipediaQuery
|
||||
{
|
||||
public WikipediaPage[] Pages { get; set; }
|
||||
|
||||
public class WikipediaPage
|
||||
{
|
||||
public bool Missing { get; set; } = false;
|
||||
public string FullUrl { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
9
src/NadekoBot/Modules/Searches/Common/WoWJoke.cs
Normal file
9
src/NadekoBot/Modules/Searches/Common/WoWJoke.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace NadekoBot.Modules.Searches.Common
|
||||
{
|
||||
public class WoWJoke
|
||||
{
|
||||
public string Question { get; set; }
|
||||
public string Answer { get; set; }
|
||||
public override string ToString() => $"`{Question}`\n\n**{Answer}**";
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user