-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
30 changed files
with
1,597 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
root = true | ||
|
||
[*] | ||
indent_style = space | ||
indent_size = 4 | ||
charset = utf-8 | ||
trim_trailing_whitespace = true | ||
insert_final_newline = true | ||
|
||
[*.{config,csproj,json,props,xml,yml}] | ||
indent_size = 2 | ||
|
||
[*.cs] | ||
dotnet_sort_system_directives_first = false |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<Project Sdk="Microsoft.NET.Sdk"> | ||
<PropertyGroup> | ||
<OutputType>Exe</OutputType> | ||
<TargetFramework>netcoreapp2.2</TargetFramework> | ||
|
||
<AssemblyName>BoardGameGeek.Dungeon</AssemblyName> | ||
<RootNamespace>BoardGameGeek.Dungeon</RootNamespace> | ||
</PropertyGroup> | ||
|
||
<ItemGroup> | ||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" /> | ||
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" /> | ||
<PackageReference Include="PocketLogger.Subscribe" Version="0.6.1" /> | ||
<PackageReference Include="System.CommandLine.Experimental" Version="0.1.0-alpha-63625-01" /> | ||
<PackageReference Include="System.Interactive" Version="3.2.0" /> | ||
</ItemGroup> | ||
</Project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
using Pocket; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using System.Net; | ||
using System.Net.Http; | ||
using System.Net.Http.Formatting; | ||
using System.Threading.Tasks; | ||
using static Pocket.Logger<BoardGameGeek.Dungeon.BggService>; | ||
// ReSharper disable StringLiteralTypo | ||
|
||
namespace BoardGameGeek.Dungeon | ||
{ | ||
public interface IBggService | ||
{ | ||
Task<ThingItems> DownloadThingsAsync(IEnumerable<int> ids); | ||
Task<CollectionItems> DownloadUserCollectionAsync(string userName); | ||
Task<UserPlays> DownloadUserPlaysAsync(string userName, int? year = null, int? id = null, int? page = null); | ||
} | ||
|
||
public sealed class BggService : IBggService | ||
{ | ||
public async Task<ThingItems> DownloadThingsAsync(IEnumerable<int> ids) | ||
{ | ||
var query = $"id={string.Join(",", ids)}"; | ||
var uri = new Uri($"https://boardgamegeek.com/xmlapi2/thing?{query}"); | ||
Log.Info($" {uri}"); | ||
return await DownloadAsync<ThingItems>(uri); | ||
} | ||
|
||
public async Task<CollectionItems> DownloadUserCollectionAsync(string userName) | ||
{ | ||
var query = $"username={userName}&subtype=boardgame&minplays=1"; | ||
var uri = new Uri($"https://boardgamegeek.com/xmlapi2/collection?{query}"); | ||
Log.Info($" {uri}"); | ||
await Task.Delay(TimeSpan.FromSeconds(5)); | ||
var response = await HttpClient.GetAsync(uri); | ||
response.EnsureSuccessStatusCode(); | ||
if (response.StatusCode == HttpStatusCode.Accepted || response.StatusCode == HttpStatusCode.TooManyRequests) | ||
{ | ||
// collection requests are queued; http://boardgamegeek.com/wiki/page/BGG_XML_API2#toc11 | ||
using (var delay = EnumerateDelay().GetEnumerator()) | ||
{ | ||
do | ||
{ | ||
delay.MoveNext(); | ||
await Task.Delay(delay.Current); | ||
response = await HttpClient.GetAsync(uri); | ||
response.EnsureSuccessStatusCode(); | ||
} | ||
while (response.StatusCode == HttpStatusCode.Accepted || response.StatusCode == HttpStatusCode.TooManyRequests); | ||
} | ||
} | ||
return await response.Content.ReadAsAsync<CollectionItems>(XmlFormatterCollection); | ||
} | ||
|
||
public async Task<UserPlays> DownloadUserPlaysAsync(string userName, int? year = null, int? id = null, int? page = null) | ||
{ | ||
var query = $"username={userName}&subtype=boardgame"; | ||
if (id != null) | ||
{ | ||
query += $"&id={id}"; | ||
} | ||
if (year != null) | ||
{ | ||
query += $"&mindate={year}-01-01&maxdate={year}-12-31"; | ||
} | ||
if (page != null) | ||
{ | ||
query += $"&page={page}"; | ||
} | ||
var uri = new Uri($"https://boardgamegeek.com/xmlapi2/plays?{query}"); | ||
Log.Info($" {uri}"); | ||
return await DownloadAsync<UserPlays>(uri); | ||
} | ||
|
||
private async Task<T> DownloadAsync<T>(Uri uri) | ||
{ | ||
await Task.Delay(TimeSpan.FromSeconds(5)); | ||
var response = await HttpClient.GetAsync(uri); | ||
response.EnsureSuccessStatusCode(); | ||
return await response.Content.ReadAsAsync<T>(XmlFormatterCollection); | ||
} | ||
|
||
private static IEnumerable<TimeSpan> EnumerateDelay() | ||
{ | ||
return Enumerable.Range(3, 5).Select(seconds => TimeSpan.FromSeconds(Math.Pow(2, seconds))) | ||
.Concat(Enumerable.Repeat(TimeSpan.FromMinutes(1), int.MaxValue)); | ||
} | ||
|
||
private static readonly HttpClient HttpClient = new HttpClient(); | ||
private static readonly XmlMediaTypeFormatter XmlFormatter = new XmlMediaTypeFormatter { UseXmlSerializer = true }; | ||
private static readonly MediaTypeFormatterCollection XmlFormatterCollection = new MediaTypeFormatterCollection(new MediaTypeFormatter[] { XmlFormatter }); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
namespace BoardGameGeek.Dungeon | ||
{ | ||
public class CollectionDto | ||
{ | ||
public int GameId { get; set; } | ||
public string GameName { get; set; } | ||
public string Image { get; set; } | ||
public string Thumbnail { get; set; } | ||
public int TotalPlays { get; set; } | ||
public string Comments { get; set; } | ||
|
||
public override string ToString() | ||
{ | ||
return $"{GameName} (\u03a3 {TotalPlays}x)"; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
using System.Text; | ||
using System.Xml.Serialization; | ||
// ReSharper disable StringLiteralTypo | ||
// ReSharper disable UnusedMember.Global | ||
|
||
namespace BoardGameGeek.Dungeon | ||
{ | ||
[XmlRoot("items")] | ||
public class CollectionItems | ||
{ | ||
[XmlAttribute("totalitems")] | ||
public int TotalItems { get; set; } | ||
|
||
[XmlAttribute("termsofuse")] | ||
public string TermsOfUse { get; set; } | ||
|
||
[XmlAttribute("pubdate")] | ||
public string PubDate { get; set; } | ||
|
||
[XmlElement("item")] | ||
public CollectionItem[] Items { get; set; } | ||
} | ||
|
||
public class CollectionItem | ||
{ | ||
[XmlAttribute("objecttype")] | ||
public string ObjectType { get; set; } | ||
|
||
[XmlAttribute("objectid")] | ||
public int ObjectId { get; set; } | ||
|
||
[XmlAttribute("subtype")] | ||
public string SubType { get; set; } | ||
|
||
[XmlElement("name")] | ||
public string Name { get; set; } | ||
|
||
[XmlElement("yearpublished")] | ||
public int YearPublished { get; set; } | ||
|
||
[XmlElement("image")] | ||
public string Image { get; set; } | ||
|
||
[XmlElement("thumbnail")] | ||
public string Thumbnail { get; set; } | ||
|
||
[XmlElement("numplays")] | ||
public int TotalPlays { get; set; } | ||
|
||
[XmlElement("comment")] | ||
public string Comments { get; set; } | ||
|
||
[XmlElement("status")] | ||
public CollectionItemStatus Status { get; set; } | ||
|
||
public override string ToString() | ||
{ | ||
return $"Name = {Name}, TotalPlays = {TotalPlays}"; | ||
} | ||
} | ||
|
||
public class CollectionItemStatus | ||
{ | ||
[XmlAttribute("own")] | ||
public bool Own { get; set; } | ||
|
||
[XmlAttribute("prevowned")] | ||
public bool PrevOwned { get; set; } | ||
|
||
[XmlAttribute("fortrade")] | ||
public bool ForTrade { get; set; } | ||
|
||
[XmlAttribute("want")] | ||
public bool WantInTrade { get; set; } | ||
|
||
[XmlAttribute("wanttoplay")] | ||
public bool WantToPlay { get; set; } | ||
|
||
[XmlAttribute("wanttobuy")] | ||
public bool WantToBuy { get; set; } | ||
|
||
[XmlAttribute("wishlist")] | ||
public bool WishList { get; set; } | ||
|
||
[XmlAttribute("wishlistpriority")] | ||
public int WishListPriority { get; set; } | ||
|
||
[XmlAttribute("preordered")] | ||
public bool Preordered { get; set; } | ||
|
||
[XmlAttribute("lastmodified")] | ||
public string LastModified { get; set; } | ||
|
||
public override string ToString() | ||
{ | ||
var builder = new StringBuilder(); | ||
if (Own) | ||
{ | ||
builder.Append(nameof(Own)); | ||
} | ||
if (PrevOwned) | ||
{ | ||
if (builder.Length > 0) | ||
{ | ||
builder.Append(", "); | ||
} | ||
builder.Append(nameof(PrevOwned)); | ||
} | ||
if (ForTrade) | ||
{ | ||
if (builder.Length > 0) | ||
{ | ||
builder.Append(", "); | ||
} | ||
builder.Append(nameof(ForTrade)); | ||
} | ||
if (WantInTrade) | ||
{ | ||
if (builder.Length > 0) | ||
{ | ||
builder.Append(", "); | ||
} | ||
builder.Append(nameof(WantInTrade)); | ||
} | ||
if (WantToPlay) | ||
{ | ||
if (builder.Length > 0) | ||
{ | ||
builder.Append(", "); | ||
} | ||
builder.Append(nameof(WantToPlay)); | ||
} | ||
if (WantToBuy) | ||
{ | ||
if (builder.Length > 0) | ||
{ | ||
builder.Append(", "); | ||
} | ||
builder.Append(nameof(WantToBuy)); | ||
} | ||
if (WishList) | ||
{ | ||
if (builder.Length > 0) | ||
{ | ||
builder.Append(", "); | ||
} | ||
builder.Append($"{nameof(WishList)}({WishListPriority})"); | ||
} | ||
if (Preordered) | ||
{ | ||
if (builder.Length > 0) | ||
{ | ||
builder.Append(", "); | ||
} | ||
builder.Append(nameof(Preordered)); | ||
} | ||
return builder.ToString(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
using System; | ||
using System.CommandLine; | ||
using System.CommandLine.Builder; | ||
using System.CommandLine.Invocation; | ||
using System.Threading.Tasks; | ||
|
||
namespace BoardGameGeek.Dungeon | ||
{ | ||
public class CommandLine | ||
{ | ||
static CommandLine() | ||
{ | ||
// arguments | ||
var userNameArgument = new Argument<string> { Name = "username", Description = "Geek user name." }; | ||
|
||
// options | ||
var allOption = new Option(new[] { "-a", "--all" }, "Analyze all override.", new Argument<bool>()); | ||
var yearOption = new Option(new[] { "-y", "--year" }, "Year to analyze. Defaults to current year.", new Argument<int>(DateTime.Now.Year)); | ||
|
||
// parser | ||
Parser = new CommandLineBuilder(new RootCommand("A command line tool for interacting with the BoardGameGeek API") | ||
{ | ||
new Command("plays", "Get user plays.", new[] { allOption, yearOption }, userNameArgument, | ||
handler: CommandHandler.Create(typeof(CommandLine).GetMethod(nameof(PlaysAsync)))), | ||
new Command("stats", "Get user stats.", new[] { allOption, yearOption }, userNameArgument, | ||
handler: CommandHandler.Create(typeof(CommandLine).GetMethod(nameof(StatsAsync)))) | ||
}) | ||
.UseDefaults() | ||
.Build(); | ||
} | ||
|
||
// commands | ||
public static async Task PlaysAsync(string userName, bool all, int? year) | ||
{ | ||
if (all) | ||
{ | ||
year = null; | ||
} | ||
var processor = new Processor(new BggService()); | ||
var renderer = new Renderer(); | ||
await renderer.RenderPlays(userName, year, await processor.ProcessPlays(userName, year)); | ||
} | ||
|
||
public static async Task StatsAsync(string userName, bool all, int? year) | ||
{ | ||
if (all) | ||
{ | ||
year = null; | ||
} | ||
var processor = new Processor(new BggService()); | ||
var renderer = new Renderer(); | ||
await renderer.RenderStats(userName, year, await processor.ProcessStats(userName, year)); | ||
} | ||
|
||
public static Parser Parser { get; } | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
namespace BoardGameGeek.Dungeon | ||
{ | ||
public class GameDto | ||
{ | ||
public int Id { get; set; } | ||
public string Name { get; set; } | ||
public bool IsExpansion { get; set; } | ||
public int ParentId { get; set; } | ||
public int Plays { get; set; } | ||
public int Sessions { get; set; } | ||
public int CumulativePlays { get; set; } | ||
public int CumulativeSessions { get; set; } | ||
public bool IsHighlight { get; set; } | ||
public bool IsNew { get; set; } | ||
public string Comments { get; set; } | ||
|
||
public override string ToString() | ||
{ | ||
return $"{Plays}x {Name} (\u03a3 {CumulativePlays}x)"; | ||
} | ||
} | ||
} |
Oops, something went wrong.