Lots of rewrites, optimization, logging stuff

This commit is contained in:
Enrico Ludwig 2024-09-06 21:59:25 +02:00
parent 8489d0c25c
commit 6d1959fb4c
12 changed files with 383 additions and 259 deletions

View File

@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins;
@ -6,27 +7,69 @@ using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using BetterRaid.Services;
using BetterRaid.Services.Implementations;
using BetterRaid.ViewModels;
using BetterRaid.Views;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace BetterRaid;
public class App : Application
{
private ServiceProvider? _serviceProvider;
private ILogger<App>? _logger;
public override void Initialize()
{
_serviceProvider = InitializeServices();
_logger = _serviceProvider.GetRequiredService<ILogger<App>>();
if (TryLoadDatabase() == false)
{
_logger?.LogError("Failed to load or initialize database");
Environment.Exit(1);
}
AvaloniaXamlLoader.Load(_serviceProvider, this);
}
private bool TryLoadDatabase()
{
if (_serviceProvider == null)
{
throw new FieldAccessException($"\"{nameof(_serviceProvider)}\" was null");
}
var db = _serviceProvider.GetRequiredService<IDatabaseService>();
try
{
db.LoadOrCreate();
Task.Run(db.UpdateLoadedChannels);
return true;
}
catch (Exception e)
{
_logger?.LogError(e, "Failed to load database");
return false;
}
}
private ServiceProvider InitializeServices()
{
var services = new ServiceCollection();
services.AddLogging(logging =>
{
logging.SetMinimumLevel(LogLevel.Debug);
logging.AddConsole();
});
services.AddSingleton<ITwitchService, TwitchService>();
services.AddSingleton<ISynchronizaionService, DispatcherService>(serviceProvider => new DispatcherService(Dispatcher.UIThread));
services.AddTransient<IMainViewModelFactory, MainWindowViewModelFactory>();
services.AddSingleton<IWebToolsService, WebToolsService>();
services.AddSingleton<IDatabaseService, DatabaseService>();
services.AddSingleton<ISynchronizaionService, DispatcherService>(_ => new DispatcherService(Dispatcher.UIThread));
services.AddTransient<MainWindowViewModel>();
return services.BuildServiceProvider();
}
@ -40,17 +83,16 @@ public class App : Application
throw new FieldAccessException($"\"{nameof(_serviceProvider)}\" was null");
}
var viewModelFactory = _serviceProvider.GetRequiredService<IMainViewModelFactory>();
var mainWindowViewModel = viewModelFactory.CreateMainWindowViewModel();
var mainWindow = new MainWindow
{
DataContext = mainWindowViewModel
DataContext = _serviceProvider.GetRequiredService<MainWindowViewModel>()
};
switch (ApplicationLifetime)
{
case IClassicDesktopStyleApplicationLifetime desktop:
desktop.MainWindow = mainWindow;
desktop.Exit += OnDesktopOnExit;
break;
case ISingleViewApplicationLifetime singleViewPlatform:
@ -60,4 +102,22 @@ public class App : Application
base.OnFrameworkInitializationCompleted();
}
private void OnDesktopOnExit(object? o, ControlledApplicationLifetimeExitEventArgs controlledApplicationLifetimeExitEventArgs)
{
if (_serviceProvider == null)
{
throw new FieldAccessException($"\"{nameof(_serviceProvider)}\" was null");
}
try
{
var db = _serviceProvider.GetRequiredService<IDatabaseService>();
db.Save();
}
catch (Exception e)
{
_logger?.LogError(e, "Failed to save database");
}
}
}

View File

@ -27,6 +27,9 @@
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
<PackageReference Include="TwitchLib" Version="3.5.3" />
</ItemGroup>
</Project>

View File

@ -1,149 +0,0 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
using Newtonsoft.Json;
namespace BetterRaid.Models;
[JsonObject]
public class BetterRaidDatabase : INotifyPropertyChanged
{
[JsonIgnore]
private string? _databaseFilePath;
private bool _onlyOnline;
public event PropertyChangedEventHandler? PropertyChanged;
public bool OnlyOnline
{
get => _onlyOnline;
set
{
if (value == _onlyOnline)
return;
_onlyOnline = value;
OnPropertyChanged();
}
}
public List<string> Channels { get; set; } = [];
public Dictionary<string, DateTime?> LastRaided = [];
public bool AutoSave { get; set; }
public static BetterRaidDatabase LoadFromFile(string path)
{
ArgumentNullException.ThrowIfNullOrEmpty(path);
path = Path.Combine(Environment.CurrentDirectory, path);
if (File.Exists(path) == false)
{
throw new FileNotFoundException("Database file not found", path);
}
var dbStr = File.ReadAllText(path);
var dbObj = JsonConvert.DeserializeObject<BetterRaidDatabase>(dbStr);
if (dbObj == null)
{
throw new JsonException("Failed to read database file");
}
dbObj._databaseFilePath = path;
foreach (var channel in dbObj.Channels)
{
if (dbObj.LastRaided.ContainsKey(channel) == false)
{
dbObj.LastRaided.Add(channel, null);
}
}
Console.WriteLine("[DEBUG] Loaded database from {0}", path);
return dbObj;
}
public void Save(string? path = null)
{
if (string.IsNullOrEmpty(_databaseFilePath) && string.IsNullOrEmpty(path))
{
throw new ArgumentException("No target path given to save database at");
}
if (string.IsNullOrEmpty(path) == false && string.IsNullOrEmpty(_databaseFilePath))
{
_databaseFilePath = path;
}
var dbStr = JsonConvert.SerializeObject(this);
var targetPath = (path ?? _databaseFilePath)!;
File.WriteAllText(targetPath, dbStr);
Console.WriteLine("[DEBUG] Saved database to {0}", targetPath);
}
public void AddChannel(string channel)
{
ArgumentNullException.ThrowIfNull(channel);
if (Channels.Contains(channel))
return;
Channels.Add(channel);
OnPropertyChanged(nameof(Channels));
}
public void RemoveChannel(string channel)
{
ArgumentNullException.ThrowIfNull(channel);
if (Channels.Contains(channel) == false)
return;
Channels.Remove(channel);
OnPropertyChanged(nameof(Channels));
}
public void SetRaided(string channel, DateTime dateTime)
{
ArgumentNullException.ThrowIfNull(channel);
if (LastRaided.ContainsKey(channel))
{
LastRaided[channel] = dateTime;
}
else
{
LastRaided.Add(channel, dateTime);
}
OnPropertyChanged(nameof(LastRaided));
}
public DateTime? GetLastRaided(string channel)
{
ArgumentNullException.ThrowIfNull(channel);
if (LastRaided.ContainsKey(channel))
{
return LastRaided[channel];
}
else
{
return null;
}
}
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
if (AutoSave && _databaseFilePath != null)
{
Save();
}
}
}

View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace BetterRaid.Models.Database;
[JsonObject]
public class BetterRaidDatabase
{
public bool OnlyOnline { get; set; }
public List<TwitchChannel> Channels { get; set; } = [];
}

View File

@ -3,12 +3,15 @@ using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using BetterRaid.Services;
using Newtonsoft.Json;
using TwitchLib.PubSub.Events;
namespace BetterRaid.Models;
[JsonObject]
public class TwitchChannel : INotifyPropertyChanged
{
private string? _id;
private string? _broadcasterId;
private string? _viewerCount;
private bool _isLive;
@ -18,7 +21,6 @@ public class TwitchChannel : INotifyPropertyChanged
private string? _category;
private string? _title;
private DateTime? _lastRaided;
private string? _id;
public string? Id
{
@ -59,6 +61,7 @@ public class TwitchChannel : INotifyPropertyChanged
}
}
[JsonIgnore]
public bool IsLive
{
get => _isLive;
@ -72,6 +75,7 @@ public class TwitchChannel : INotifyPropertyChanged
}
}
[JsonIgnore]
public string? ViewerCount
{
get => _viewerCount;
@ -111,6 +115,7 @@ public class TwitchChannel : INotifyPropertyChanged
}
}
[JsonIgnore]
public string? Category
{
get => _category;
@ -124,6 +129,7 @@ public class TwitchChannel : INotifyPropertyChanged
}
}
[JsonIgnore]
public string? Title
{
get => _title;

162
Services/DatabaseService.cs Normal file
View File

@ -0,0 +1,162 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BetterRaid.Misc;
using BetterRaid.Models;
using BetterRaid.Models.Database;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace BetterRaid.Services;
public interface IDatabaseService
{
bool OnlyOnline { get; set; }
bool AutoSave { get; set; }
BetterRaidDatabase? Database { get; set; }
void LoadOrCreate();
void LoadFromFile(string path, bool createIfNotExist = false);
Task UpdateLoadedChannels();
void Save(string? path = null);
bool TrySetRaided(TwitchChannel channel, DateTime dateTime);
}
public class DatabaseService : IDatabaseService
{
private string? _databaseFilePath;
private readonly ILogger<DatabaseService> _logger;
private readonly ITwitchService _twitch;
public bool OnlyOnline { get; set; }
public bool AutoSave { get; set; }
public BetterRaidDatabase? Database { get; set; }
public DatabaseService(ILogger<DatabaseService> logger, ITwitchService twitch)
{
_logger = logger;
_twitch = twitch;
}
public void LoadOrCreate()
{
LoadFromFile(Constants.DatabaseFilePath, true);
}
public void LoadFromFile(string path, bool createIfNotExist = false)
{
ArgumentException.ThrowIfNullOrEmpty(path);
path = Path.Combine(Environment.CurrentDirectory, path);
var exists = File.Exists(path);
switch (exists)
{
case false when createIfNotExist == false:
throw new FileNotFoundException("Database file not found", path);
case false when createIfNotExist:
_logger.LogWarning("Database file not found, creating new database");
Database = new BetterRaidDatabase();
Save(path);
_logger.LogDebug("Created new database at {path}", path);
return;
case true:
var dbStr = File.ReadAllText(path);
var dbObj = JsonConvert.DeserializeObject<BetterRaidDatabase>(dbStr);
_databaseFilePath = path;
Database = dbObj ?? throw new JsonException("Failed to read database file");
_logger.LogDebug("Loaded database from {path}", path);
return;
}
}
public async Task UpdateLoadedChannels()
{
if (Database == null || Database.Channels.Count == 0)
return;
await Parallel.ForAsync(0, Database.Channels.Count, (i, c) =>
{
if (c.IsCancellationRequested)
return ValueTask.FromCanceled(c);
var channel = Database.Channels[i];
channel.UpdateChannelData(_twitch);
return ValueTask.CompletedTask;
});
}
public void Save(string? path = null)
{
if (string.IsNullOrEmpty(_databaseFilePath) && string.IsNullOrEmpty(path))
{
throw new ArgumentException("No target path given to save database at");
}
if (string.IsNullOrEmpty(path) == false && string.IsNullOrEmpty(_databaseFilePath))
{
_databaseFilePath = path;
}
var dbStr = JsonConvert.SerializeObject(Database, Formatting.Indented);
var targetPath = _databaseFilePath!;
File.WriteAllText(targetPath, dbStr);
_logger.LogDebug("Saved database to {targetPath}", targetPath);
}
public void AddChannel(TwitchChannel channel)
{
ArgumentNullException.ThrowIfNull(channel);
if (Database == null)
throw new InvalidOperationException("Database is not loaded");
if (Database.Channels.Any(c => c.Name?.Equals(c.Name, StringComparison.CurrentCultureIgnoreCase) == true))
return;
Database.Channels.Add(channel);
}
public void RemoveChannel(TwitchChannel channel)
{
ArgumentNullException.ThrowIfNull(channel);
if (Database == null)
throw new InvalidOperationException("Database is not loaded");
var index = Database.Channels.FindIndex(c => c.Name?.Equals(c.Name, StringComparison.CurrentCultureIgnoreCase) == true);
if (index == -1)
return;
Database.Channels.RemoveAt(index);
}
public bool TrySetRaided(TwitchChannel channel, DateTime dateTime)
{
ArgumentNullException.ThrowIfNull(channel);
if (Database == null)
throw new InvalidOperationException("Database is not loaded");
var twitchChannel = Database.Channels.FirstOrDefault(c => c.Name?.Equals(channel.Name, StringComparison.CurrentCultureIgnoreCase) == true);
if (twitchChannel == null)
return false;
twitchChannel.LastRaided = dateTime;
return true;
}
}

View File

@ -2,7 +2,7 @@
namespace BetterRaid.Services.Implementations;
public class MainWindowViewModelFactory : IMainViewModelFactory
public class MainWindowViewModelFactory// : IMainViewModelFactory
{
private readonly ITwitchService _twitchService;
private readonly ISynchronizaionService _synchronizaionService;
@ -13,8 +13,8 @@ public class MainWindowViewModelFactory : IMainViewModelFactory
_synchronizaionService = synchronizaionService;
}
public MainWindowViewModel CreateMainWindowViewModel()
{
return new MainWindowViewModel(_twitchService, _synchronizaionService);
}
//public MainWindowViewModel CreateMainWindowViewModel()
//{
// return new MainWindowViewModel(_twitchService, _synchronizaionService);
//}
}

View File

@ -6,6 +6,7 @@ using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using BetterRaid.Misc;
using BetterRaid.Models;
using Microsoft.Extensions.Logging;
using TwitchLib.Api;
using TwitchLib.Api.Helix.Models.Users.GetUsers;
using TwitchLib.PubSub;
@ -40,8 +41,9 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot
private bool _isRaidStarted;
private int _raidParticipants;
private TwitchChannel? _userChannel;
private readonly List<TwitchChannel> _registeredChannels;
private User? _user;
private readonly ILogger<TwitchService> _logger;
private readonly IWebToolsService _webTools;
public string AccessToken { get; private set; } = string.Empty;
@ -80,46 +82,46 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot
set => SetField(ref _raidParticipants, value);
}
public TwitchService()
public TwitchService(ILogger<TwitchService> logger, IWebToolsService webTools)
{
_registeredChannels = [];
_logger = logger;
_webTools = webTools;
TwitchApi = new TwitchAPI();
TwitchEvents = new TwitchPubSub();
if (TryLoadAccessToken(out var token))
{
Console.WriteLine($"[INFO][{nameof(TwitchService)}] Found access token.");
_logger.LogInformation("Found access token.");
Task.Run(() => ConnectApiAsync(Constants.TwitchClientId, token))
.ContinueWith(_ => ConnectTwitchEvents(token));
.ContinueWith(_ => ConnectTwitchEvents());
}
else
{
Console.WriteLine($"[INFO][{nameof(TwitchService)}] No access token found.");
_logger.LogInformation("No access token found.");
}
}
private async Task ConnectTwitchEvents(string token)
private async Task ConnectTwitchEvents()
{
if (UserChannel == null || User == null)
return;
Console.WriteLine($"[INFO][{nameof(TwitchService)}] Connecting to Twitch Events ...");
_logger.LogInformation("Connecting to Twitch Events ...");
TwitchEvents.OnRaidGo += OnUserRaidGo;
TwitchEvents.OnRaidUpdate += OnUserRaidUpdate;
TwitchEvents.OnStreamUp += OnUserStreamUp;
TwitchEvents.OnStreamDown += OnUserStreamDown;
TwitchEvents.OnViewCount += OnViewCount;
TwitchEvents.OnLog += (sender, args) => Console.WriteLine($"[INFO][{nameof(TwitchService)}] {args.Data}");
TwitchEvents.OnPubSubServiceError += (sender, args) => Console.WriteLine($"[ERROR][{nameof(TwitchService)}] {args.Exception.Message}");
TwitchEvents.OnPubSubServiceConnected += (sender, args) => Console.WriteLine($"[INFO][{nameof(TwitchService)}] Connected to Twitch PubSub.");
TwitchEvents.OnPubSubServiceClosed += (sender, args) => Console.WriteLine($"[INFO][{nameof(TwitchService)}] Disconnected from Twitch PubSub.");
TwitchEvents.OnLog += OnPubSubLog;
TwitchEvents.OnPubSubServiceError += OnPubSubServiceError;
TwitchEvents.OnPubSubServiceConnected += OnPubSubServiceConnected;
TwitchEvents.OnPubSubServiceClosed += OnPubSubServiceClosed;
TwitchEvents.ListenToVideoPlayback(UserChannel.BroadcasterId);
TwitchEvents.ListenToRaid(UserChannel.BroadcasterId);
TwitchEvents.SendTopics(token);
TwitchEvents.Connect();
await Task.CompletedTask;
@ -127,7 +129,7 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot
public async Task ConnectApiAsync(string clientId, string accessToken)
{
Console.WriteLine($"[INFO][{nameof(TwitchService)}] Connecting to Twitch API ...");
_logger.LogInformation("Connecting to Twitch API ...");
AccessToken = accessToken;
@ -142,24 +144,24 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot
{
User = null;
Console.WriteLine($"[ERROR][{nameof(TwitchService)}] Could not get user.");
_logger.LogError("Could not get user with client id {clientId} - please check your clientId and accessToken", clientId);
}
if (TryGetUserChannel(out var channel))
{
UserChannel = channel;
Console.WriteLine($"[INFO][{nameof(TwitchService)}] Connected to Twitch API as {channel?.Name}.");
_logger.LogInformation("Connected to Twitch API as {channelName} with broadcaster id {channelBroadcasterId}.", channel?.Name, channel?.BroadcasterId);
}
else
{
UserChannel = null;
Console.WriteLine($"[ERROR][{nameof(TwitchService)}] Could not get user channel.");
_logger.LogError("Could not get user channel.");
}
if (User == null || UserChannel == null)
{
Console.WriteLine($"[ERROR][{nameof(TwitchService)}] Could not connect to Twitch API.");
_logger.LogError("Could not connect to Twitch API.");
}
await Task.CompletedTask;
@ -199,7 +201,7 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot
}
catch (Exception e)
{
Console.WriteLine($"[ERROR][{nameof(TwitchService)}] {e.Message}");
_logger.LogError(e, "Could not get user.");
return false;
}
}
@ -218,7 +220,7 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot
public void RegisterForEvents(TwitchChannel channel)
{
Console.WriteLine($"[DEBUG][{nameof(TwitchService)}] Registering for events for {channel.Name} ...");
_logger.LogDebug("Registering for events for {channelName} with broadcaster id {channelBroadcasterId} ...", channel.Name, channel.BroadcasterId);
TwitchEvents.OnStreamUp += channel.OnStreamUp;
TwitchEvents.OnStreamDown += channel.OnStreamDown;
@ -227,12 +229,12 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot
TwitchEvents.ListenToVideoPlayback(channel.Id);
TwitchEvents.SendTopics(AccessToken);
_registeredChannels.Add(channel);
}
public void UnregisterFromEvents(TwitchChannel channel)
{
_logger.LogDebug("Unregistering from events for {channelName} with broadcaster id {channelBroadcasterId} ...", channel.Name, channel.BroadcasterId);
TwitchEvents.OnStreamUp -= channel.OnStreamUp;
TwitchEvents.OnStreamDown -= channel.OnStreamDown;
TwitchEvents.OnViewCount -= channel.OnViewCount;
@ -240,8 +242,6 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot
TwitchEvents.ListenToVideoPlayback(channel.Id);
TwitchEvents.SendTopics(AccessToken, true);
_registeredChannels.Remove(channel);
}
public string GetOAuthUrl()
@ -304,7 +304,28 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot
var url = $"https://twitch.tv/{channelName}";
Tools.OpenUrl(url);
_webTools.OpenUrl(url);
}
private void OnPubSubServiceClosed(object? sender, EventArgs e)
{
_logger.LogWarning("PubSub: Connection closed.");
}
private void OnPubSubServiceError(object? sender, OnPubSubServiceErrorArgs e)
{
_logger.LogError(e.Exception, "PubSub: {exception}", e.Exception);
}
private void OnPubSubLog(object? sender, OnLogArgs e)
{
_logger.LogInformation("PubSub: {data}", e.Data);
}
private void OnPubSubServiceConnected(object? sender, EventArgs e)
{
TwitchEvents.SendTopics(AccessToken);
_logger.LogInformation("PubSub: Connected.");
}
// TODO Not called while raid is ongoing
@ -314,7 +335,7 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot
// return;
RaidParticipants = e.ViewerCount;
Console.WriteLine($"[INFO][{nameof(TwitchService)}] Raid participants: {RaidParticipants}");
_logger.LogInformation("Raid participants: {participants}", RaidParticipants);
}
private void OnViewCount(object? sender, OnViewCountArgs e)
@ -333,7 +354,7 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot
if (e.ChannelId != UserChannel?.Id)
return;
Console.WriteLine($"[INFO][{nameof(TwitchService)}] Raid started.");
_logger.LogInformation("Raid started.");
IsRaidStarted = false;
}
@ -346,7 +367,7 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot
if (e.ChannelId != UserChannel?.Id)
return;
Console.WriteLine($"[INFO][{nameof(TwitchService)}] Stream down.");
_logger.LogInformation("Stream down.");
IsRaidStarted = false;
@ -361,10 +382,9 @@ public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INot
if (e.ChannelId != UserChannel?.Id)
return;
Console.WriteLine($"[INFO][{nameof(TwitchService)}] Stream up.");
_logger.LogInformation("Stream up.");
IsRaidStarted = false;
UserChannel.IsLive = true;
}

View File

@ -6,16 +6,29 @@ using System.Text;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using BetterRaid.Services;
using BetterRaid.Misc;
using Microsoft.Extensions.Logging;
namespace BetterRaid.Misc;
namespace BetterRaid.Services;
public static class Tools
public interface IWebToolsService
{
private static HttpListener? _oauthListener;
void StartOAuthLogin(ITwitchService twitch, Action? callback = null, CancellationToken token = default);
void OpenUrl(string url);
}
public class WebToolsService : IWebToolsService
{
private HttpListener? _oauthListener;
private readonly ILogger<WebToolsService> _logger;
public WebToolsService(ILogger<WebToolsService> logger)
{
_logger = logger;
}
// Source: https://stackoverflow.com/a/43232486
public static void StartOAuthLogin(ITwitchService twitchService, Action? callback = null, CancellationToken token = default)
public void StartOAuthLogin(ITwitchService twitch, Action? callback = null, CancellationToken token = default)
{
if (_oauthListener == null)
{
@ -23,13 +36,13 @@ public static class Tools
_oauthListener.Prefixes.Add(Constants.TwitchOAuthRedirectUrl + "/");
_oauthListener.Start();
Task.Run(() => WaitForCallback(callback, token, twitchService), token);
Task.Run(() => WaitForCallback(twitch, callback, token), token);
}
OpenUrl(twitchService.GetOAuthUrl());
OpenUrl(twitch.GetOAuthUrl());
}
private static async Task WaitForCallback(Action? callback, CancellationToken token, ITwitchService twitchService)
private async Task WaitForCallback(ITwitchService twitch, Action? callback, CancellationToken token)
{
if (_oauthListener == null)
return;
@ -37,7 +50,7 @@ public static class Tools
if (token.IsCancellationRequested)
return;
Console.WriteLine("Starting token listener");
_logger.LogDebug("Starting token listener");
while (!token.IsCancellationRequested)
{
@ -48,7 +61,7 @@ public static class Tools
if (req.Url == null)
continue;
Console.WriteLine("{0} {1}", req.HttpMethod, req.Url);
_logger.LogDebug("{method} {url}", req.HttpMethod, req.Url);
// Response, that may contain the access token as fragment
// It must be extracted client-side in browser
@ -64,7 +77,7 @@ public static class Tools
req.InputStream.Close();
Console.WriteLine(data.ToString());
_logger.LogTrace("{data}", data);
res.StatusCode = 200;
await res.OutputStream.WriteAsync(Encoding.UTF8.GetBytes(OAUTH_CLIENT_DOCUMENT).AsMemory(0, OAUTH_CLIENT_DOCUMENT.Length), token);
@ -88,8 +101,8 @@ public static class Tools
if (jsonData == null)
{
Console.WriteLine("[ERROR] Failed to parse JSON data:");
Console.WriteLine(json);
_logger.LogError("Failed to parse JSON data:");
_logger.LogError("{json}", json);
res.StatusCode = 400;
res.Close();
@ -98,21 +111,21 @@ public static class Tools
if (jsonData["access_token"] == null)
{
Console.WriteLine("[ERROR] Missing access_token in JSON data.");
_logger.LogError("Missing access_token in JSON data.");
res.StatusCode = 400;
res.Close();
continue;
}
var accessToken = jsonData["access_token"]?.ToString();
var accessToken = jsonData["access_token"]?.ToString()!;
twitchService.ConnectApiAsync(Constants.TwitchClientId, accessToken!);
await twitch.ConnectApiAsync(Constants.TwitchClientId, accessToken);
res.StatusCode = 200;
res.Close();
Console.WriteLine("[INFO] Received access token!");
_logger.LogInformation("Received access token!");
callback?.Invoke();
@ -122,7 +135,7 @@ public static class Tools
}
}
public static void OpenUrl(string url)
public void OpenUrl(string url)
{
try
{

View File

@ -1,11 +1,12 @@
using System;
using Microsoft.Extensions.Logging;
namespace BetterRaid.ViewModels;
public class AddChannelWindowViewModel : ViewModelBase
{
public AddChannelWindowViewModel()
public AddChannelWindowViewModel(ILogger<AddChannelWindowViewModel> logger)
{
Console.WriteLine("[DEBUG] AddChannelWindowViewModel created");
logger.LogDebug("AddChannelWindowViewModel created");
}
}

View File

@ -10,27 +10,24 @@ using BetterRaid.Misc;
using BetterRaid.Models;
using BetterRaid.Services;
using BetterRaid.Views;
using Microsoft.Extensions.Logging;
namespace BetterRaid.ViewModels;
public class MainWindowViewModel : ViewModelBase
{
private string? _filter;
private ObservableCollection<TwitchChannel> _channels = [];
private readonly BetterRaidDatabase? _db;
private readonly ISynchronizaionService _synchronizaionService;
public BetterRaidDatabase? Database
{
get => _db;
private init
{
if (SetProperty(ref _db, value) && _db != null)
{
LoadChannelsFromDb();
}
}
}
private readonly ISynchronizaionService _synchronizationService;
private readonly ILogger<MainWindowViewModel> _logger;
private readonly IWebToolsService _webTools;
private readonly IDatabaseService _db;
private readonly ITwitchService _twitch;
private string? _filter;
private bool _onlyOnline;
public ITwitchService Twitch => _twitch;
public ObservableCollection<TwitchChannel> Channels
{
@ -40,25 +37,36 @@ public class MainWindowViewModel : ViewModelBase
public ObservableCollection<TwitchChannel> FilteredChannels => GetFilteredChannels();
public ITwitchService Twitch { get; }
public string? Filter
{
get => _filter;
set => SetProperty(ref _filter, value);
}
public bool IsLoggedIn => Twitch.UserChannel != null;
public MainWindowViewModel(ITwitchService twitch, ISynchronizaionService synchronizaionService)
public bool OnlyOnline
{
_synchronizaionService = synchronizaionService;
Twitch = twitch;
Twitch.PropertyChanged += OnTwitchPropertyChanged;
get => _db.OnlyOnline;
set => SetProperty(ref _onlyOnline, value);
}
Database = BetterRaidDatabase.LoadFromFile(Constants.DatabaseFilePath);
Database.PropertyChanged += OnDatabasePropertyChanged;
public bool IsLoggedIn => _twitch.UserChannel != null;
public MainWindowViewModel(
ILogger<MainWindowViewModel> logger,
ITwitchService twitch,
IWebToolsService webTools,
IDatabaseService db,
ISynchronizaionService synchronizationService)
{
_logger = logger;
_twitch = twitch;
_webTools = webTools;
_db = db;
_synchronizationService = synchronizationService;
_twitch.PropertyChanged += OnTwitchPropertyChanged;
LoadChannelsFromDb();
}
public void ExitApplication()
@ -76,7 +84,7 @@ public class MainWindowViewModel : ViewModelBase
public void LoginWithTwitch()
{
Tools.StartOAuthLogin(Twitch, OnTwitchLoginCallback, CancellationToken.None);
_webTools.StartOAuthLogin(_twitch, OnTwitchLoginCallback, CancellationToken.None);
}
private void OnTwitchLoginCallback()
@ -86,28 +94,25 @@ public class MainWindowViewModel : ViewModelBase
private void LoadChannelsFromDb()
{
if (_db == null)
if (_db.Database == null)
{
_logger.LogError("Database is null");
return;
}
foreach (var channel in Channels)
{
Twitch.UnregisterFromEvents(channel);
_twitch.UnregisterFromEvents(channel);
}
Channels.Clear();
var channels = _db.Channels
.Select(channelName => new TwitchChannel(channelName))
.ToList();
foreach (var channel in channels)
foreach (var channel in _db.Database.Channels)
{
Task.Run(() =>
{
channel.UpdateChannelData(Twitch);
Twitch.RegisterForEvents(channel);
channel.UpdateChannelData(_twitch);
_twitch.RegisterForEvents(channel);
});
Channels.Add(channel);
@ -117,7 +122,7 @@ public class MainWindowViewModel : ViewModelBase
private ObservableCollection<TwitchChannel> GetFilteredChannels()
{
var filteredChannels = Channels
.Where(channel => Database?.OnlyOnline == false || channel.IsLive)
.Where(channel => OnlyOnline == false || channel.IsLive)
.Where(channel => string.IsNullOrWhiteSpace(Filter) || channel.Name?.Contains(Filter, StringComparison.OrdinalIgnoreCase) == true)
.ToList();
@ -126,7 +131,7 @@ public class MainWindowViewModel : ViewModelBase
private void OnTwitchPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(Twitch.UserChannel))
if (e.PropertyName != nameof(_twitch.UserChannel))
return;
OnPropertyChanged(nameof(IsLoggedIn));
@ -141,12 +146,4 @@ public class MainWindowViewModel : ViewModelBase
OnPropertyChanged(nameof(FilteredChannels));
}
}
private void OnDatabasePropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(BetterRaidDatabase.OnlyOnline))
return;
OnPropertyChanged(nameof(FilteredChannels));
}
}

View File

@ -59,7 +59,7 @@
Spacing="5">
<CheckBox Content="Only Online"
IsChecked="{Binding Database.OnlyOnline, Mode=TwoWay, FallbackValue=False}" />
IsChecked="{Binding OnlyOnline, Mode=TwoWay, FallbackValue=False}" />
<TextBox Width="200"
Margin="5, 10, 5, 10"