From 814cb587427d7d20662ebc10b66f233c1c58f6e2 Mon Sep 17 00:00:00 2001 From: Enrico Ludwig Date: Wed, 4 Sep 2024 02:27:55 +0200 Subject: [PATCH] Lots of code cleanup, moved Twitch stuff to services, further improvements on UI --- App.axaml.cs | 2 +- Attributes/PubSubAttribute.cs | 2 +- Attributes/PubSubType.cs | 4 +- Misc/Tools.cs | 4 +- Models/TwitchChannel.cs | 2 + Services/ITwitchDataService.cs | 4 +- Services/Implementations/TwitchDataService.cs | 12 +- .../Implementations/TwitchPubSubService.cs | 236 ++++++++++++------ ViewModels/MainWindowViewModel.cs | 49 +++- Views/MainWindow.axaml | 139 ++++++----- Views/MainWindow.axaml.cs | 8 + 11 files changed, 307 insertions(+), 155 deletions(-) diff --git a/App.axaml.cs b/App.axaml.cs index 9f0e640..4817aa7 100644 --- a/App.axaml.cs +++ b/App.axaml.cs @@ -12,7 +12,7 @@ using Microsoft.Extensions.DependencyInjection; namespace BetterRaid; -public partial class App : Application +public class App : Application { private static readonly ServiceCollection Services = []; private static ServiceProvider? _serviceProvider; diff --git a/Attributes/PubSubAttribute.cs b/Attributes/PubSubAttribute.cs index 20e0849..3aa39a4 100644 --- a/Attributes/PubSubAttribute.cs +++ b/Attributes/PubSubAttribute.cs @@ -2,7 +2,7 @@ using System; namespace BetterRaid.Attributes; -[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, Inherited = true)] +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)] public class PubSubAttribute : Attribute { public PubSubType Type { get; } diff --git a/Attributes/PubSubType.cs b/Attributes/PubSubType.cs index b9a0e03..ab6d59d 100644 --- a/Attributes/PubSubType.cs +++ b/Attributes/PubSubType.cs @@ -7,5 +7,7 @@ public enum PubSubType Subscriptions, ChannelPoints, Bits, - Raids + Raids, + StreamUp, + StreamDown } \ No newline at end of file diff --git a/Misc/Tools.cs b/Misc/Tools.cs index 069d953..f92bdc5 100644 --- a/Misc/Tools.cs +++ b/Misc/Tools.cs @@ -20,7 +20,7 @@ public static class Tools if (_oauthListener == null) { _oauthListener = new HttpListener(); - _oauthListener.Prefixes.Add(Constants.TwitchOAuthRedirectUrl); + _oauthListener.Prefixes.Add(Constants.TwitchOAuthRedirectUrl + "/"); _oauthListener.Start(); Task.Run(() => WaitForCallback(callback, token), token); @@ -110,7 +110,7 @@ public static class Tools var dataService = App.ServiceProvider?.GetService(typeof(ITwitchDataService)); if (dataService is ITwitchDataService twitchDataService) { - twitchDataService.ConnectApi(Constants.TwitchClientId, accessToken!); + twitchDataService.ConnectApiAsync(Constants.TwitchClientId, accessToken!); } res.StatusCode = 200; diff --git a/Models/TwitchChannel.cs b/Models/TwitchChannel.cs index 1d134cc..d3cd13d 100644 --- a/Models/TwitchChannel.cs +++ b/Models/TwitchChannel.cs @@ -45,6 +45,8 @@ public class TwitchChannel : INotifyPropertyChanged } } + [PubSub(PubSubType.StreamUp, nameof(BroadcasterId))] + [PubSub(PubSubType.StreamDown, nameof(BroadcasterId))] public bool IsLive { get => _isLive; diff --git a/Services/ITwitchDataService.cs b/Services/ITwitchDataService.cs index 664de42..0ec8d96 100644 --- a/Services/ITwitchDataService.cs +++ b/Services/ITwitchDataService.cs @@ -1,4 +1,5 @@ using System.ComponentModel; +using System.Threading.Tasks; using BetterRaid.Models; using TwitchLib.Api; @@ -11,11 +12,12 @@ public interface ITwitchDataService public TwitchAPI TwitchApi { get; } public bool IsRaidStarted { get; set; } - public void ConnectApi(string clientId, string accessToken); + public Task ConnectApiAsync(string clientId, string accessToken); public void SaveAccessToken(string token); public bool TryGetUserChannel(out TwitchChannel? userChannel); public string GetOAuthUrl(); public void StartRaid(string from, string to); + public bool CanStartRaidCommand(object? arg); public void StartRaidCommand(object? arg); public void StopRaid(); public void StopRaidCommand(); diff --git a/Services/Implementations/TwitchDataService.cs b/Services/Implementations/TwitchDataService.cs index 2f9350c..8b89ce2 100644 --- a/Services/Implementations/TwitchDataService.cs +++ b/Services/Implementations/TwitchDataService.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.IO; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using BetterRaid.Misc; using BetterRaid.Models; using TwitchLib.Api; @@ -45,7 +46,7 @@ public class TwitchDataService : ITwitchDataService, INotifyPropertyChanged, INo if (TryLoadAccessToken(out var token)) { Console.WriteLine($"[INFO][{nameof(TwitchDataService)}] Found access token."); - ConnectApi(Constants.TwitchClientId, token); + Task.Run(() => ConnectApiAsync(Constants.TwitchClientId, token)); } else { @@ -53,7 +54,7 @@ public class TwitchDataService : ITwitchDataService, INotifyPropertyChanged, INo } } - public void ConnectApi(string clientId, string accessToken) + public async Task ConnectApiAsync(string clientId, string accessToken) { Console.WriteLine($"[INFO][{nameof(TwitchDataService)}] Connecting to Twitch API ..."); @@ -74,6 +75,8 @@ public class TwitchDataService : ITwitchDataService, INotifyPropertyChanged, INo Console.WriteLine($"[ERROR][{nameof(TwitchDataService)}] Could not get user channel."); Console.WriteLine($"[ERROR][{nameof(TwitchDataService)}] Failed to connect to Twitch API."); } + + await Task.CompletedTask; } private bool TryLoadAccessToken(out string token) @@ -129,6 +132,11 @@ public class TwitchDataService : ITwitchDataService, INotifyPropertyChanged, INo IsRaidStarted = true; } + public bool CanStartRaidCommand(object? arg) + { + return UserChannel?.IsLive == true && IsRaidStarted == false; + } + public void StartRaidCommand(object? arg) { if (arg == null || UserChannel?.BroadcasterId == null) diff --git a/Services/Implementations/TwitchPubSubService.cs b/Services/Implementations/TwitchPubSubService.cs index 1bd441e..c8056b3 100644 --- a/Services/Implementations/TwitchPubSubService.cs +++ b/Services/Implementations/TwitchPubSubService.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Threading.Tasks; using BetterRaid.Attributes; using BetterRaid.Extensions; using BetterRaid.Models; @@ -13,45 +14,127 @@ namespace BetterRaid.Services.Implementations; public class TwitchPubSubService : ITwitchPubSubService { private readonly Dictionary> _targets = new(); - private readonly TwitchPubSub _sub; private readonly ITwitchDataService _dataService; + private TwitchPubSub? _sub; public TwitchPubSubService(ITwitchDataService dataService) { _dataService = dataService; + + Task.Run(InitializePubSubAsync); + } + + private async Task InitializePubSubAsync() + { + while (_dataService.UserChannel == null) + { + await Task.Delay(100); + } _sub = new TwitchPubSub(); - + _sub.OnPubSubServiceConnected += OnSubOnOnPubSubServiceConnected; _sub.OnPubSubServiceError += OnSubOnOnPubSubServiceError; _sub.OnPubSubServiceClosed += OnSubOnOnPubSubServiceClosed; _sub.OnListenResponse += OnSubOnOnListenResponse; _sub.OnViewCount += OnSubOnOnViewCount; - + _sub.OnStreamUp += OnStreamUp; + _sub.OnStreamDown += OnStreamDown; + _sub.Connect(); - + if (_dataService.UserChannel != null) { RegisterReceiver(_dataService.UserChannel); } - + _dataService.PropertyChanging += (_, args) => { if (args.PropertyName != nameof(_dataService.UserChannel)) return; - + if (_dataService.UserChannel != null) UnregisterReceiver(_dataService.UserChannel); }; - + _dataService.PropertyChanged += (_, args) => { if (args.PropertyName != nameof(_dataService.UserChannel)) return; - + if (_dataService.UserChannel != null) RegisterReceiver(_dataService.UserChannel); }; + + await Task.CompletedTask; + } + + private void OnStreamDown(object? sender, OnStreamDownArgs args) + { + var listeners = _targets + .Where(x => x.Key == PubSubType.StreamDown) + .SelectMany(x => x.Value) + .Where(x => x.ChannelId == args.ChannelId) + .ToList(); + + foreach (var listener in listeners) + { + if (listener.Listener == null || listener.Instance == null) + continue; + + try + { + if (listener.Listener.SetValue(listener.Instance, false) == false) + { + Console.WriteLine( + $"[ERROR][{nameof(TwitchPubSubService)}] Failed to set {listener.Instance.GetType().Name}.{listener.Listener.Name} to true"); + } + else + { + Console.WriteLine( + $"[DEBUG][{nameof(TwitchPubSubService)}] Setting {listener.Instance.GetType().Name}.{listener.Listener.Name} to true"); + } + } + catch (Exception e) + { + Console.WriteLine( + $"[ERROR][{nameof(TwitchPubSubService)}] Exception while setting {listener.Instance?.GetType().Name}.{listener.Listener?.Name}: {e.Message}"); + } + } + } + + private void OnStreamUp(object? sender, OnStreamUpArgs args) + { + var listeners = _targets + .Where(x => x.Key == PubSubType.StreamUp) + .SelectMany(x => x.Value) + .Where(x => x.ChannelId == args.ChannelId) + .ToList(); + + foreach (var listener in listeners) + { + if (listener.Listener == null || listener.Instance == null) + continue; + + try + { + if (listener.Listener.SetValue(listener.Instance, true) == false) + { + Console.WriteLine( + $"[ERROR][{nameof(TwitchPubSubService)}] Failed to set {listener.Instance.GetType().Name}.{listener.Listener.Name} to true"); + } + else + { + Console.WriteLine( + $"[DEBUG][{nameof(TwitchPubSubService)}] Setting {listener.Instance.GetType().Name}.{listener.Listener.Name} to true"); + } + } + catch (Exception e) + { + Console.WriteLine( + $"[ERROR][{nameof(TwitchPubSubService)}] Exception while setting {listener.Instance?.GetType().Name}.{listener.Listener?.Name}: {e.Message}"); + } + } } private void OnSubOnOnViewCount(object? sender, OnViewCountArgs args) @@ -61,26 +144,29 @@ public class TwitchPubSubService : ITwitchPubSubService .SelectMany(x => x.Value) .Where(x => x.ChannelId == args.ChannelId) .ToList(); - + foreach (var listener in listeners) { if (listener.Listener == null || listener.Instance == null) continue; - + try { if (listener.Listener.SetValue(listener.Instance, args.Viewers) == false) { - Console.WriteLine($"[ERROR][{nameof(TwitchPubSubService)}] Failed to set {listener.Instance.GetType().Name}.{listener.Listener.Name} to {args.Viewers}"); + Console.WriteLine( + $"[ERROR][{nameof(TwitchPubSubService)}] Failed to set {listener.Instance.GetType().Name}.{listener.Listener.Name} to {args.Viewers}"); } else { - Console.WriteLine($"[DEBUG][{nameof(TwitchPubSubService)}] Setting {listener.Instance.GetType().Name}.{listener.Listener.Name} to {args.Viewers}"); + Console.WriteLine( + $"[DEBUG][{nameof(TwitchPubSubService)}] Setting {listener.Instance.GetType().Name}.{listener.Listener.Name} to {args.Viewers}"); } } catch (Exception e) { - Console.WriteLine($"[ERROR][{nameof(TwitchPubSubService)}] Exception while setting {listener.Instance?.GetType().Name}.{listener.Listener?.Name}: {e.Message}"); + Console.WriteLine( + $"[ERROR][{nameof(TwitchPubSubService)}] Exception while setting {listener.Instance?.GetType().Name}.{listener.Listener?.Name}: {e.Message}"); } } } @@ -109,8 +195,14 @@ public class TwitchPubSubService : ITwitchPubSubService { ArgumentNullException.ThrowIfNull(receiver, nameof(receiver)); + if (_sub == null) + { + Console.WriteLine($"[ERROR][{nameof(TwitchPubSubService)}] PubSub is not initialized"); + return; + } + Console.WriteLine($"[DEBUG][{nameof(TwitchPubSubService)}] Registering {receiver.GetType().Name}"); - + var type = typeof(T); var publicTargets = type .GetProperties() @@ -120,93 +212,79 @@ public class TwitchPubSubService : ITwitchPubSubService foreach (var target in publicTargets) { - if (target.GetCustomAttribute() is not { } attr) + if (target.GetCustomAttributes() is not { } attrs) { continue; } - - var channelId = - type.GetProperty(attr.ChannelIdField)?.GetValue(receiver)?.ToString() ?? - type.GetField(attr.ChannelIdField)?.GetValue(receiver)?.ToString(); - - if (string.IsNullOrEmpty(channelId)) + + foreach (var attr in attrs) { - Console.WriteLine($"[ERROR][{nameof(TwitchPubSubService)}] {target.Name} is missing ChannelIdField named {attr.ChannelIdField}"); - continue; - } - - switch (attr.Type) - { - case PubSubType.Bits: - break; - case PubSubType.ChannelPoints: - break; - case PubSubType.Follows: - break; - case PubSubType.Raids: - break; - case PubSubType.Subscriptions: - break; - case PubSubType.VideoPlayback: - Console.WriteLine($"[DEBUG][{nameof(TwitchPubSubService)}] Registering {target.Name} for {attr.Type}"); - if (_targets.TryGetValue(PubSubType.VideoPlayback, out var value)) + var channelId = + type.GetProperty(attr.ChannelIdField)?.GetValue(receiver)?.ToString() ?? + type.GetField(attr.ChannelIdField)?.GetValue(receiver)?.ToString(); + + if (channelId == null) + { + Console.WriteLine( + $"[ERROR][{nameof(TwitchPubSubService)}] {target.Name} is missing ChannelIdField named {attr.ChannelIdField}"); + continue; + } + + if (string.IsNullOrWhiteSpace(channelId)) + { + Console.WriteLine( + $"[ERROR][{nameof(TwitchPubSubService)}] {target.Name} ChannelIdField named {attr.ChannelIdField} is empty"); + continue; + } + + Console.WriteLine($"[DEBUG][{nameof(TwitchPubSubService)}] Registering {target.Name} for {attr.Type}"); + if (_targets.TryGetValue(attr.Type, out var listeners)) + { + listeners.Add(new PubSubListener { - value.Add(new PubSubListener + ChannelId = channelId, + Instance = receiver, + Listener = target + }); + } + else + { + _targets.Add(attr.Type, [ + new PubSubListener { ChannelId = channelId, Instance = receiver, Listener = target - }); - } - else - { - _targets.Add(PubSubType.VideoPlayback, [ - new PubSubListener - { - ChannelId = channelId, - Instance = receiver, - Listener = target - } - ]); - } - - _sub.ListenToVideoPlayback(channelId); - _sub.SendTopics(_dataService.AccessToken); - - break; + } + ]); + } + + _sub.ListenToVideoPlayback(channelId); + _sub.SendTopics(_dataService.AccessToken, true); } } } - + public void UnregisterReceiver(T receiver) where T : class { ArgumentNullException.ThrowIfNull(receiver, nameof(receiver)); - + + if (_sub == null) + { + Console.WriteLine($"[ERROR][{nameof(TwitchPubSubService)}] PubSub is not initialized"); + return; + } + foreach (var (topic, listeners) in _targets) { var listener = listeners.Where(x => x.Instance == receiver).ToList(); foreach (var l in listener) { - switch (topic) - { - case PubSubType.Bits: - break; - case PubSubType.ChannelPoints: - break; - case PubSubType.Follows: - break; - case PubSubType.Raids: - break; - case PubSubType.Subscriptions: - break; - case PubSubType.VideoPlayback: - _sub.ListenToVideoPlayback(l.ChannelId); - _sub.SendTopics(_dataService.AccessToken, true); - break; - } + _sub.ListenToVideoPlayback(l.ChannelId); + _sub.SendTopics(_dataService.AccessToken, true); } - + _targets[topic].RemoveAll(x => x.Instance == receiver); } } diff --git a/ViewModels/MainWindowViewModel.cs b/ViewModels/MainWindowViewModel.cs index 00a62fe..57a1c19 100644 --- a/ViewModels/MainWindowViewModel.cs +++ b/ViewModels/MainWindowViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.ObjectModel; +using System.ComponentModel; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -12,17 +13,17 @@ using BetterRaid.Views; namespace BetterRaid.ViewModels; -public partial class MainWindowViewModel : ViewModelBase +public class MainWindowViewModel : ViewModelBase { private string? _filter; private ObservableCollection _channels = []; - private BetterRaidDatabase? _db; + private readonly BetterRaidDatabase? _db; private readonly ITwitchPubSubService _pubSub; public BetterRaidDatabase? Database { get => _db; - set + private init { if (SetProperty(ref _db, value) && _db != null) { @@ -37,6 +38,8 @@ public partial class MainWindowViewModel : ViewModelBase set => SetProperty(ref _channels, value); } + public ObservableCollection FilteredChannels => GetFilteredChannels(); + public ITwitchDataService DataService { get; } public string? Filter @@ -51,10 +54,12 @@ public partial class MainWindowViewModel : ViewModelBase { _pubSub = pubSub; DataService = dataService; + DataService.PropertyChanged += OnDataServicePropertyChanged; Database = BetterRaidDatabase.LoadFromFile(Constants.DatabaseFilePath); + Database.PropertyChanged += OnDatabasePropertyChanged; } - + public void ExitApplication() { //TODO polish later @@ -107,4 +112,40 @@ public partial class MainWindowViewModel : ViewModelBase Channels.Add(channel); } } + + private ObservableCollection GetFilteredChannels() + { + var filteredChannels = Channels + .Where(channel => Database?.OnlyOnline == false || channel.IsLive) + .Where(channel => string.IsNullOrWhiteSpace(Filter) || channel.Name?.Contains(Filter, StringComparison.OrdinalIgnoreCase) == true) + .ToList(); + + return new ObservableCollection(filteredChannels); + } + + private void OnDataServicePropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName != nameof(DataService.UserChannel)) + return; + + OnPropertyChanged(nameof(IsLoggedIn)); + } + + protected override void OnPropertyChanged(PropertyChangedEventArgs e) + { + base.OnPropertyChanged(e); + + if (e.PropertyName == nameof(Filter)) + { + OnPropertyChanged(nameof(FilteredChannels)); + } + } + + private void OnDatabasePropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName != nameof(BetterRaidDatabase.OnlyOnline)) + return; + + OnPropertyChanged(nameof(FilteredChannels)); + } } diff --git a/Views/MainWindow.axaml b/Views/MainWindow.axaml index 6f2a0cd..e8c63ac 100644 --- a/Views/MainWindow.axaml +++ b/Views/MainWindow.axaml @@ -22,83 +22,85 @@ + VerticalAlignment="Stretch" + ColumnDefinitions="Auto,*" + RowDefinitions="50,*"> - - - - + + + + + + + + + + + + - - - - - - - - - - - - - - - - + IsChecked="{Binding Database.OnlyOnline, Mode=TwoWay, FallbackValue=False}" /> - + + - + + + + + + + + - + diff --git a/Views/MainWindow.axaml.cs b/Views/MainWindow.axaml.cs index 3535110..2d38699 100644 --- a/Views/MainWindow.axaml.cs +++ b/Views/MainWindow.axaml.cs @@ -8,4 +8,12 @@ public partial class MainWindow : Window { InitializeComponent(); } + + private void SelectingItemsControl_OnSelectionChanged(object? sender, SelectionChangedEventArgs e) + { + if (sender is ListBox listBox) + { + listBox.SelectedItems?.Clear(); + } + } } \ No newline at end of file