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;
using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core.Plugins; using Avalonia.Data.Core.Plugins;
@ -6,27 +7,69 @@ using Avalonia.Markup.Xaml;
using Avalonia.Threading; using Avalonia.Threading;
using BetterRaid.Services; using BetterRaid.Services;
using BetterRaid.Services.Implementations; using BetterRaid.Services.Implementations;
using BetterRaid.ViewModels;
using BetterRaid.Views; using BetterRaid.Views;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace BetterRaid; namespace BetterRaid;
public class App : Application public class App : Application
{ {
private ServiceProvider? _serviceProvider; private ServiceProvider? _serviceProvider;
private ILogger<App>? _logger;
public override void Initialize() public override void Initialize()
{ {
_serviceProvider = InitializeServices(); _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); 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() private ServiceProvider InitializeServices()
{ {
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddLogging(logging =>
{
logging.SetMinimumLevel(LogLevel.Debug);
logging.AddConsole();
});
services.AddSingleton<ITwitchService, TwitchService>(); services.AddSingleton<ITwitchService, TwitchService>();
services.AddSingleton<ISynchronizaionService, DispatcherService>(serviceProvider => new DispatcherService(Dispatcher.UIThread)); services.AddSingleton<IWebToolsService, WebToolsService>();
services.AddTransient<IMainViewModelFactory, MainWindowViewModelFactory>(); services.AddSingleton<IDatabaseService, DatabaseService>();
services.AddSingleton<ISynchronizaionService, DispatcherService>(_ => new DispatcherService(Dispatcher.UIThread));
services.AddTransient<MainWindowViewModel>();
return services.BuildServiceProvider(); return services.BuildServiceProvider();
} }
@ -40,17 +83,16 @@ public class App : Application
throw new FieldAccessException($"\"{nameof(_serviceProvider)}\" was null"); throw new FieldAccessException($"\"{nameof(_serviceProvider)}\" was null");
} }
var viewModelFactory = _serviceProvider.GetRequiredService<IMainViewModelFactory>();
var mainWindowViewModel = viewModelFactory.CreateMainWindowViewModel();
var mainWindow = new MainWindow var mainWindow = new MainWindow
{ {
DataContext = mainWindowViewModel DataContext = _serviceProvider.GetRequiredService<MainWindowViewModel>()
}; };
switch (ApplicationLifetime) switch (ApplicationLifetime)
{ {
case IClassicDesktopStyleApplicationLifetime desktop: case IClassicDesktopStyleApplicationLifetime desktop:
desktop.MainWindow = mainWindow; desktop.MainWindow = mainWindow;
desktop.Exit += OnDesktopOnExit;
break; break;
case ISingleViewApplicationLifetime singleViewPlatform: case ISingleViewApplicationLifetime singleViewPlatform:
@ -60,4 +102,22 @@ public class App : Application
base.OnFrameworkInitializationCompleted(); 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 Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.1.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" /> <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" /> <PackageReference Include="TwitchLib" Version="3.5.3" />
</ItemGroup> </ItemGroup>
</Project> </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.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using BetterRaid.Services; using BetterRaid.Services;
using Newtonsoft.Json;
using TwitchLib.PubSub.Events; using TwitchLib.PubSub.Events;
namespace BetterRaid.Models; namespace BetterRaid.Models;
[JsonObject]
public class TwitchChannel : INotifyPropertyChanged public class TwitchChannel : INotifyPropertyChanged
{ {
private string? _id;
private string? _broadcasterId; private string? _broadcasterId;
private string? _viewerCount; private string? _viewerCount;
private bool _isLive; private bool _isLive;
@ -18,7 +21,6 @@ public class TwitchChannel : INotifyPropertyChanged
private string? _category; private string? _category;
private string? _title; private string? _title;
private DateTime? _lastRaided; private DateTime? _lastRaided;
private string? _id;
public string? Id public string? Id
{ {
@ -59,6 +61,7 @@ public class TwitchChannel : INotifyPropertyChanged
} }
} }
[JsonIgnore]
public bool IsLive public bool IsLive
{ {
get => _isLive; get => _isLive;
@ -72,6 +75,7 @@ public class TwitchChannel : INotifyPropertyChanged
} }
} }
[JsonIgnore]
public string? ViewerCount public string? ViewerCount
{ {
get => _viewerCount; get => _viewerCount;
@ -111,6 +115,7 @@ public class TwitchChannel : INotifyPropertyChanged
} }
} }
[JsonIgnore]
public string? Category public string? Category
{ {
get => _category; get => _category;
@ -124,6 +129,7 @@ public class TwitchChannel : INotifyPropertyChanged
} }
} }
[JsonIgnore]
public string? Title public string? Title
{ {
get => _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; namespace BetterRaid.Services.Implementations;
public class MainWindowViewModelFactory : IMainViewModelFactory public class MainWindowViewModelFactory// : IMainViewModelFactory
{ {
private readonly ITwitchService _twitchService; private readonly ITwitchService _twitchService;
private readonly ISynchronizaionService _synchronizaionService; private readonly ISynchronizaionService _synchronizaionService;
@ -13,8 +13,8 @@ public class MainWindowViewModelFactory : IMainViewModelFactory
_synchronizaionService = synchronizaionService; _synchronizaionService = synchronizaionService;
} }
public MainWindowViewModel CreateMainWindowViewModel() //public MainWindowViewModel CreateMainWindowViewModel()
{ //{
return new MainWindowViewModel(_twitchService, _synchronizaionService); // return new MainWindowViewModel(_twitchService, _synchronizaionService);
} //}
} }

View File

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

View File

@ -6,16 +6,29 @@ using System.Text;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using System.Threading; using System.Threading;
using System.Threading.Tasks; 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 // 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) if (_oauthListener == null)
{ {
@ -23,13 +36,13 @@ public static class Tools
_oauthListener.Prefixes.Add(Constants.TwitchOAuthRedirectUrl + "/"); _oauthListener.Prefixes.Add(Constants.TwitchOAuthRedirectUrl + "/");
_oauthListener.Start(); _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) if (_oauthListener == null)
return; return;
@ -37,7 +50,7 @@ public static class Tools
if (token.IsCancellationRequested) if (token.IsCancellationRequested)
return; return;
Console.WriteLine("Starting token listener"); _logger.LogDebug("Starting token listener");
while (!token.IsCancellationRequested) while (!token.IsCancellationRequested)
{ {
@ -48,7 +61,7 @@ public static class Tools
if (req.Url == null) if (req.Url == null)
continue; 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 // Response, that may contain the access token as fragment
// It must be extracted client-side in browser // It must be extracted client-side in browser
@ -64,7 +77,7 @@ public static class Tools
req.InputStream.Close(); req.InputStream.Close();
Console.WriteLine(data.ToString()); _logger.LogTrace("{data}", data);
res.StatusCode = 200; res.StatusCode = 200;
await res.OutputStream.WriteAsync(Encoding.UTF8.GetBytes(OAUTH_CLIENT_DOCUMENT).AsMemory(0, OAUTH_CLIENT_DOCUMENT.Length), token); 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) if (jsonData == null)
{ {
Console.WriteLine("[ERROR] Failed to parse JSON data:"); _logger.LogError("Failed to parse JSON data:");
Console.WriteLine(json); _logger.LogError("{json}", json);
res.StatusCode = 400; res.StatusCode = 400;
res.Close(); res.Close();
@ -98,21 +111,21 @@ public static class Tools
if (jsonData["access_token"] == null) 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.StatusCode = 400;
res.Close(); res.Close();
continue; 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.StatusCode = 200;
res.Close(); res.Close();
Console.WriteLine("[INFO] Received access token!"); _logger.LogInformation("Received access token!");
callback?.Invoke(); callback?.Invoke();
@ -122,7 +135,7 @@ public static class Tools
} }
} }
public static void OpenUrl(string url) public void OpenUrl(string url)
{ {
try try
{ {

View File

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