Skip to content

Commit

Permalink
feat: Improves formating on the Excel report
Browse files Browse the repository at this point in the history
also a lot of R# love.
  • Loading branch information
negri committed Feb 28, 2021
1 parent 86f66bd commit af19215
Show file tree
Hide file tree
Showing 19 changed files with 71 additions and 84 deletions.
2 changes: 1 addition & 1 deletion WhoIsStreaming/Api/ExponentialRetryPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Negri.Twitch.Api
/// </summary>
public class ExponentialRetryPolicy : RetryPolicy
{
private static readonly Random Random = new Random();
private static readonly Random Random = new();

private readonly TimeSpan _maxWait;
private readonly TimeSpan _minWait;
Expand Down
2 changes: 2 additions & 0 deletions WhoIsStreaming/Api/Game.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Text.Json.Serialization;
using JetBrains.Annotations;

namespace Negri.Twitch.Api
{
[PublicAPI]
public class Game
{
[JsonPropertyName("id")]
Expand Down
2 changes: 2 additions & 0 deletions WhoIsStreaming/Api/GetStreamsResponse.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Text.Json.Serialization;
using JetBrains.Annotations;

namespace Negri.Twitch.Api
{
[PublicAPI]
public class GetStreamsResponse : ResponseBase
{
[JsonPropertyName("data")]
Expand Down
2 changes: 2 additions & 0 deletions WhoIsStreaming/Api/Pagination.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Text.Json.Serialization;
using JetBrains.Annotations;

namespace Negri.Twitch.Api
{
[PublicAPI]
public class Pagination
{
[JsonPropertyName("cursor")]
Expand Down
61 changes: 2 additions & 59 deletions WhoIsStreaming/Api/RetryPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,70 +11,13 @@ public abstract class RetryPolicy
/// <summary>
/// Se a primeira tentativa após falha deve ser imediatamente executada
/// </summary>
public bool FastFirstRetry { get; set; } = true;
private bool FastFirstRetry { get; } = true;

/// <summary>
/// O numero máximo de tentativas padrão
/// </summary>
public int DefaultMaxTries { get; set; } = 5;
private int DefaultMaxTries { get; } = 5;

/// <summary>
/// Retorna a politica padrão, com os intervalos padrão de espera (Exponencial com progresso de 100ms)
/// </summary>
public static RetryPolicy Default => GetExponentialRetryPolicy(TimeSpan.FromMilliseconds(100));



/// <summary>
/// Obtém uma politica de intervalos de tentativas esperando exponencialmente mais.
/// </summary>
public static RetryPolicy GetExponentialRetryPolicy(TimeSpan progress, TimeSpan minWait = default,
TimeSpan maxWait = default)
{
return new ExponentialRetryPolicy(progress, minWait, maxWait);
}

/// <summary>
/// Executa com a politica padrão (Exponencial)
/// </summary>
/// <param name="action">A ação a ser executada</param>
/// <param name="maxTries">Numero de tentativas, opcional, o padrão são 10 que leva a cerca de 1,5 minutos de espera</param>
public static void ExecuteDefault(Action action, int? maxTries = null)
{
Default.ExecuteAction(action, maxTries ?? 10);
}

/// <summary>
/// Executa com a politica padrão (Exponencial)
/// </summary>
/// <param name="action">A ação a ser executada</param>
/// <param name="maxTries">Numero de tentativas, opcional, o padrão são 10 que leva a cerca de 1,5 minutos de espera</param>
public static T ExecuteDefault<T>(Func<T> action, int? maxTries = null)
{
return Default.ExecuteAction(action, maxTries ?? 10);
}

/// <summary>
/// Executa uma função com a possibilidade de tentar novamente
/// </summary>
/// <param name="action">A ação a ser executada</param>
/// <param name="maxTries">Numero total de tentativas a serem feitas</param>
/// <param name="beforeWaitAction">Função, opcional, que é chamada imediatamente antes de esperar</param>
/// <param name="isTransientError">
/// Função para determinar se uma exceção é transiente ou não. Se não informado, toda
/// exceção é considerada transiente.
/// </param>
public void ExecuteAction(Action action, int? maxTries = null,
Action<int, TimeSpan, Exception> beforeWaitAction = null, Func<Exception, bool> isTransientError = null)
{
if (action == null) throw new ArgumentNullException(nameof(action));

ExecuteAction<object>(() =>
{
action();
return null;
}, maxTries, beforeWaitAction, isTransientError);
}

/// <summary>
/// Executa uma função com a possibilidade de tentar novamente
Expand Down
2 changes: 2 additions & 0 deletions WhoIsStreaming/Api/SearchGameResponse.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Text.Json.Serialization;
using JetBrains.Annotations;

namespace Negri.Twitch.Api
{
[PublicAPI]
public class SearchGameResponse : ResponseBase
{
[JsonPropertyName("data")]
Expand Down
2 changes: 2 additions & 0 deletions WhoIsStreaming/Api/Stream.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
using System;
using System.Text.Json.Serialization;
using JetBrains.Annotations;

namespace Negri.Twitch.Api
{
[PublicAPI]
public class Stream
{
[JsonPropertyName("id")]
Expand Down
2 changes: 2 additions & 0 deletions WhoIsStreaming/Api/Token.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System.Text.Json.Serialization;
using JetBrains.Annotations;

namespace Negri.Twitch.Api
{
[PublicAPI]
public class Token
{
[JsonPropertyName("access_token")]
Expand Down
2 changes: 1 addition & 1 deletion WhoIsStreaming/Api/TwitchClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class TwitchClient
private readonly string _clientId;
private readonly string _clientSecret;

private readonly HttpClient _client = new HttpClient {BaseAddress = new Uri("https://api.twitch.tv/helix/")};
private readonly HttpClient _client = new() {BaseAddress = new Uri("https://api.twitch.tv/helix/")};

public TwitchClient(string clientId, string clientSecret)
{
Expand Down
2 changes: 2 additions & 0 deletions WhoIsStreaming/Api/WebApiException.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using System;
using System.Net;
using JetBrains.Annotations;

namespace Negri.Twitch.Api
{
/// <summary>
/// Exceção quando uma API Web falhar por erros na chamada
/// </summary>
[PublicAPI]
public class WebApiException : ApplicationException
{
public string Url { get; }
Expand Down
5 changes: 2 additions & 3 deletions WhoIsStreaming/Api/WebApiRetryPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,10 @@ private static bool IsTransientError(Exception ex)
return false;
}

public static T ExecuteAction<T>(Func<T> func, int? maxTries = null,
Action<int, TimeSpan, Exception> beforeWaitAction = null)
public static T ExecuteAction<T>(Func<T> func, int? maxTries = null)
{
var rp = new ExponentialRetryPolicy(TimeSpan.FromMilliseconds(100));
return rp.ExecuteAction<T>(func, maxTries ?? 10, isTransientError: IsTransientError);
return rp.ExecuteAction(func, maxTries ?? 10, isTransientError: IsTransientError);
}
}
}
16 changes: 11 additions & 5 deletions WhoIsStreaming/Commands/Collect.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using CliFx;
using CliFx.Attributes;
using CliFx.Exceptions;
using JetBrains.Annotations;

namespace Negri.Twitch.Commands
{
Expand All @@ -20,20 +21,25 @@ public Collect(AppSettings appSettings)
_appSettings = appSettings;
}

[PublicAPI]
[CommandParameter(0, Name = "game", Description = "Game id to retrieve streamers.")]
public string Game { get; set; }

[PublicAPI]
[CommandOption("data-dir", 'd', Description = "The directory where to write collected data.", EnvironmentVariableName = "who-is-streaming-data-dir")]
public string DataDir { get; set; }

[PublicAPI]
[CommandOption("save-thumbs", 't', Description = "If thumbnails should be saved.")]
public bool SaveThumbnails { get; set; } = false;
public bool SaveThumbnails { get; set; }

[PublicAPI]
[CommandOption("min-viewers", 'v', Description = "Minimum numbers of viewers to save.")]
public int MinViewers { get; set; } = 0;
public int MinViewers { get; set; }

[PublicAPI]
[CommandOption("min-viewers-thumbs", Description = "Minimum numbers of viewers to save.")]
public int MinViewersForThumbnails { get; set; } = 0;
public int MinViewersForThumbnails { get; set; }

public ValueTask ExecuteAsync(IConsole console)
{
Expand Down Expand Up @@ -73,7 +79,7 @@ public ValueTask ExecuteAsync(IConsole console)

var streams = client.GetStreams(game.Id).Where(s => s.ViewerCount >= MinViewers).ToArray();

console.Output.WriteLine($"Streamer Language Viewers");
console.Output.WriteLine("Streamer Language Viewers");
foreach (var s in streams)
{
console.Output.WriteLine($"{s.UserName,-30} {s.Language,-8} {s.ViewerCount,7:N0}");
Expand All @@ -96,7 +102,7 @@ public ValueTask ExecuteAsync(IConsole console)
var count = 0;
var messageLock = new object();

Parallel.ForEach(streamsToGetThumbs, (s, ps, loopCount) =>
Parallel.ForEach(streamsToGetThumbs, (s, _, _) =>
{
Interlocked.Increment(ref count);
lock (messageLock)
Expand Down
34 changes: 23 additions & 11 deletions WhoIsStreaming/Commands/Report.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using CsvHelper;
using CsvHelper.Configuration;
using CsvHelper.Configuration.Attributes;
using JetBrains.Annotations;
using Negri.Twitch.Api;
using OfficeOpenXml;

Expand All @@ -26,14 +27,16 @@ public Report(AppSettings appSettings)
_appSettings = appSettings;
}

[PublicAPI]
[CommandParameter(0, Name = "game", Description = "Game id to report.")]
public string Game { get; set; }

[PublicAPI]
[CommandOption("excel-file", 'e', Description = "The Excel report to write.")]
public string ExcelFile { get; set; }

[CommandOption("data-dir", 'd', Description = "The directory where to read collected data.", EnvironmentVariableName = "who-is-streaming-data-dir",
IsRequired = true)]
[PublicAPI]
[CommandOption("data-dir", 'd', Description = "The directory where to read collected data.", EnvironmentVariableName = "who-is-streaming-data-dir", IsRequired = true)]
public string DataDir { get; set; } = string.Empty;

[CommandOption("start", Description = "The start moment to report.")]
Expand All @@ -45,8 +48,9 @@ public Report(AppSettings appSettings)
[CommandOption("period", 'p', Description = "The most recent period to report.")]
public ReportPeriod? Period { get; set; }

[PublicAPI]
[CommandOption("use-utc", 'u', Description = "All input dates are in UTC.")]
public bool UseUtc { get; set; } = false;
public bool UseUtc { get; set; }

public ValueTask ExecuteAsync(IConsole console)
{
Expand Down Expand Up @@ -91,7 +95,7 @@ public ValueTask ExecuteAsync(IConsole console)
throw new CommandException("No streams where found.", (int) ReturnCode.NoObservations);
}

var sessions = GetSessions(observations.OrderBy(o => o.Moment)).OrderByDescending(s => s.Observations).ToList();
var sessions = GetSessions(observations.OrderBy(o => o.Moment)).OrderByDescending(s => s.ViewersMinutes).ToList();
console.Output.WriteLine($"{sessions.Count:N0} sessions found for a total of {sessions.Sum(s => s.Duration.TotalHours):N0} hours.");

console.Output.WriteLine("Streamer Language Avg Viewers Max Viewers Duration");
Expand All @@ -102,14 +106,15 @@ public ValueTask ExecuteAsync(IConsole console)

if (!string.IsNullOrWhiteSpace(ExcelFile))
{
console.Output.WriteLine($"Creating Excel report at {ExcelFile}...");
WriteExcel(game.Name, observations, sessions);
console.Output.WriteLine($"Excel report saved on '{ExcelFile}'.");
}

return default;
}

private void WriteExcel(string gameName, List<Observation> observations, List<Session> sessions)
private void WriteExcel(string gameName, IEnumerable<Observation> observations, IEnumerable<Session> sessions)
{
var templateFile = Path.Combine(AppContext.BaseDirectory, "Template.xlsx");

Expand All @@ -129,17 +134,21 @@ private void WriteExcel(string gameName, List<Observation> observations, List<Se
sessionsSheet.Cells[row, 4].Value = UseUtc ? session.Start : session.Start.ToLocalTime();
sessionsSheet.Cells[row, 5].Value = UseUtc ? session.End : session.End.ToLocalTime();
sessionsSheet.Cells[row, 6].FormulaR1C1 = "=RC[-1]-RC[-2]";
sessionsSheet.Cells[row, 7].Value = session.MaxViewers;
sessionsSheet.Cells[row, 8].Value = session.AverageViewers;
sessionsSheet.Cells[row, 9].Value = session.Observations;
sessionsSheet.Cells[row, 10].Value = session.Title;
sessionsSheet.Cells[row, 7].FormulaR1C1 = "=RC[-1]*24*RC[2]";
sessionsSheet.Cells[row, 8].Value = session.MaxViewers;
sessionsSheet.Cells[row, 9].Value = session.AverageViewers;
sessionsSheet.Cells[row, 10].Value = session.Observations;
sessionsSheet.Cells[row, 11].Value = session.Title;

++row;
}

sessionsSheet.Cells[4, 2].FormulaR1C1 = $"=MIN(R8C4:R{row - 1}C4)";
sessionsSheet.Cells[5, 2].FormulaR1C1 = $"=MAX(R8C5:R{row - 1}C5)";

sessionsSheet.Cells[7, 1, row - 1, 11].AutoFilter = true;
sessionsSheet.Cells[7, 2, row - 1, 11].AutoFitColumns();

// The observations
var observationsSheet = package.Workbook.Worksheets["Observations"];
observationsSheet.Cells[2, 1].Value = gameName;
Expand All @@ -162,6 +171,8 @@ private void WriteExcel(string gameName, List<Observation> observations, List<Se
observationsSheet.Cells[4, 2].FormulaR1C1 = $"=MIN(R8C4:R{row - 1}C4)";
observationsSheet.Cells[5, 2].FormulaR1C1 = $"=MAX(R8C5:R{row - 1}C5)";

observationsSheet.Cells[7, 1, row - 1, 8].AutoFilter = true;
observationsSheet.Cells[7, 2, row - 1, 8].AutoFitColumns();

var destFile = new FileInfo(ExcelFile);
package.SaveAs(destFile);
Expand Down Expand Up @@ -295,10 +306,10 @@ private class Session
public int AverageViewers => _cumulativeViewers / Observations;
public int MaxViewers { get; private set; }



public TimeSpan Duration => End - Start;

public int ViewersMinutes => (int) (AverageViewers * Duration.TotalMinutes);

public void Add(Observation o)
{
// The streamer can change the title on the middle of the session. Let's report the most frequent title used
Expand All @@ -322,6 +333,7 @@ public void Add(Observation o)
}
}

[PublicAPI]
private record Observation
{
[Name("User Id")] public long UserId { get; init; }
Expand Down
3 changes: 2 additions & 1 deletion WhoIsStreaming/Commands/ReturnCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public enum ReturnCode
ThreeDateParameters = 5,
MissingDateParameters = 6,
InvalidDateParameters = 7,
NoObservations = 8
NoObservations = 8,
NoSecrets = 9
}
}
2 changes: 2 additions & 0 deletions WhoIsStreaming/Commands/SearchGame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Threading.Tasks;
using CliFx;
using CliFx.Attributes;
using JetBrains.Annotations;

namespace Negri.Twitch.Commands
{
Expand All @@ -15,6 +16,7 @@ public SearchGame(AppSettings appSettings)
_appSettings = appSettings;
}

[PublicAPI]
[CommandParameter(0, Name = "game", Description = "part of the name of the game to search for.")]
public string GameName { get; set; }

Expand Down
Loading

0 comments on commit af19215

Please sign in to comment.