Merge branch 'ui-rework' into dispatcher-service

This commit is contained in:
Enrico Ludwig 2024-09-05 16:47:02 +02:00 committed by GitHub
commit b2252557a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 394 additions and 608 deletions

View File

@ -7,7 +7,6 @@ using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using BetterRaid.Extensions;
using BetterRaid.Services;
using BetterRaid.Services.Implementations;
using BetterRaid.ViewModels;
using BetterRaid.Views;
using Microsoft.Extensions.DependencyInjection;
@ -27,8 +26,7 @@ public class App : Application
private ServiceProvider InitializeServices()
{
var Services = new ServiceCollection();
Services.AddSingleton<ITwitchDataService, TwitchDataService>();
Services.AddSingleton<ITwitchPubSubService, TwitchPubSubService>();
Services.AddSingleton<ITwitchService, TwitchService>();
Services.AddSingleton<ISynchronizaionService, DispatcherService>(serviceProvider => new DispatcherService(Dispatcher.UIThread));
Services.AddTransient<IMainViewModelFactory, MainWindowViewModelFactory>();

View File

@ -1,17 +0,0 @@
using System;
namespace BetterRaid.Attributes;
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field, AllowMultiple = true)]
public class PubSubAttribute : Attribute
{
public PubSubType Type { get; }
public string ChannelIdField { get; set; }
public PubSubAttribute(PubSubType type, string channelIdField)
{
Type = type;
ChannelIdField = channelIdField;
}
}

View File

@ -1,13 +0,0 @@
namespace BetterRaid.Attributes;
public enum PubSubType
{
VideoPlayback,
Follows,
Subscriptions,
ChannelPoints,
Bits,
Raids,
StreamUp,
StreamDown
}

View File

@ -15,7 +15,7 @@ public static class Tools
private static HttpListener? _oauthListener;
// Source: https://stackoverflow.com/a/43232486
public static void StartOAuthLogin(string url, ITwitchDataService twitchDataService, Action? callback = null, CancellationToken token = default)
public static void StartOAuthLogin(ITwitchService twitchService, Action? callback = null, CancellationToken token = default)
{
if (_oauthListener == null)
{
@ -23,13 +23,13 @@ public static class Tools
_oauthListener.Prefixes.Add(Constants.TwitchOAuthRedirectUrl + "/");
_oauthListener.Start();
Task.Run(() => WaitForCallback(callback, token, twitchDataService), token);
Task.Run(() => WaitForCallback(callback, token, twitchService), token);
}
OpenUrl(url);
OpenUrl(twitchService.GetOAuthUrl());
}
private static async Task WaitForCallback(Action? callback, CancellationToken token, ITwitchDataService twitchDataService)
private static async Task WaitForCallback(Action? callback, CancellationToken token, ITwitchService twitchService)
{
if (_oauthListener == null)
return;
@ -107,7 +107,7 @@ public static class Tools
var accessToken = jsonData["access_token"]?.ToString();
twitchDataService.ConnectApiAsync(Constants.TwitchClientId, accessToken!);
twitchService.ConnectApiAsync(Constants.TwitchClientId, accessToken!);
res.StatusCode = 200;
res.Close();

View File

@ -1,10 +0,0 @@
using System.Reflection;
namespace BetterRaid.Models;
public class PubSubListener
{
public string ChannelId { get; set; }
public object? Instance { get; set; }
public MemberInfo? Listener { get; set; }
}

View File

@ -2,8 +2,8 @@ using System;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using BetterRaid.Attributes;
using BetterRaid.Services;
using TwitchLib.PubSub.Events;
namespace BetterRaid.Models;
@ -45,8 +45,6 @@ public class TwitchChannel : INotifyPropertyChanged
}
}
[PubSub(PubSubType.StreamUp, nameof(BroadcasterId))]
[PubSub(PubSubType.StreamDown, nameof(BroadcasterId))]
public bool IsLive
{
get => _isLive;
@ -60,7 +58,6 @@ public class TwitchChannel : INotifyPropertyChanged
}
}
[PubSub(PubSubType.VideoPlayback, nameof(BroadcasterId))]
public string? ViewerCount
{
get => _viewerCount;
@ -144,15 +141,15 @@ public class TwitchChannel : INotifyPropertyChanged
Name = channelName;
}
public void UpdateChannelData(ITwitchDataService dataService)
public void UpdateChannelData(ITwitchService service)
{
var channel = dataService.TwitchApi.Helix.Search.SearchChannelsAsync(Name).Result.Channels
var channel = service.TwitchApi.Helix.Search.SearchChannelsAsync(Name).Result.Channels
.FirstOrDefault(c => c.BroadcasterLogin.Equals(Name, StringComparison.CurrentCultureIgnoreCase));
if (channel == null)
return;
var stream = dataService.TwitchApi.Helix.Streams.GetStreamsAsync(userLogins: [ Name ]).Result.Streams
var stream = service.TwitchApi.Helix.Streams.GetStreamsAsync(userLogins: [ Name ]).Result.Streams
.FirstOrDefault(s => s.UserLogin.Equals(Name, StringComparison.CurrentCultureIgnoreCase));
BroadcasterId = channel.Id;
@ -166,6 +163,21 @@ public class TwitchChannel : INotifyPropertyChanged
: $"{stream.ViewerCount}";
}
public void OnStreamUp(object? sender, OnStreamUpArgs args)
{
IsLive = true;
}
public void OnStreamDown(object? sender, OnStreamDownArgs e)
{
IsLive = false;
}
public void OnViewCount(object? sender, OnViewCountArgs e)
{
ViewerCount = $"{e.Viewers}";
}
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)

View File

@ -1,28 +0,0 @@
using System.ComponentModel;
using System.Threading.Tasks;
using BetterRaid.Models;
using TwitchLib.Api;
namespace BetterRaid.Services;
public interface ITwitchDataService
{
public string? AccessToken { get; set; }
public TwitchChannel? UserChannel { get; set; }
public TwitchAPI TwitchApi { get; }
public bool IsRaidStarted { get; set; }
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();
public void OpenChannelCommand(object? arg);
public event PropertyChangingEventHandler? PropertyChanging;
public event PropertyChangedEventHandler? PropertyChanged;
}

View File

@ -1,7 +0,0 @@
namespace BetterRaid.Services;
public interface ITwitchPubSubService
{
void RegisterReceiver<T>(T receiver) where T : class;
void UnregisterReceiver<T>(T receiver) where T : class;
}

View File

@ -1,206 +0,0 @@
using System;
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;
namespace BetterRaid.Services.Implementations;
public class TwitchDataService : ITwitchDataService, INotifyPropertyChanged, INotifyPropertyChanging
{
private bool _isRaidStarted;
private TwitchChannel? _userChannel;
public string AccessToken { get; set; } = string.Empty;
public bool IsRaidStarted
{
get => _isRaidStarted;
set => SetField(ref _isRaidStarted, value);
}
public TwitchChannel? UserChannel
{
get => _userChannel;
set
{
if (_userChannel != null && _userChannel.Name?.Equals(value?.Name) == true)
return;
SetField(ref _userChannel, value);
_userChannel?.UpdateChannelData(this);
}
}
public TwitchAPI TwitchApi { get; }
public TwitchDataService()
{
TwitchApi = new TwitchAPI();
if (TryLoadAccessToken(out var token))
{
Console.WriteLine($"[INFO][{nameof(TwitchDataService)}] Found access token.");
Task.Run(() => ConnectApiAsync(Constants.TwitchClientId, token));
}
else
{
Console.WriteLine($"[INFO][{nameof(TwitchDataService)}] No access token found.");
}
}
public async Task ConnectApiAsync(string clientId, string accessToken)
{
Console.WriteLine($"[INFO][{nameof(TwitchDataService)}] Connecting to Twitch API ...");
AccessToken = accessToken;
TwitchApi.Settings.ClientId = clientId;
TwitchApi.Settings.AccessToken = accessToken;
if (TryGetUserChannel(out var channel))
{
UserChannel = channel;
Console.WriteLine($"[INFO][{nameof(TwitchDataService)}] Connected to Twitch API as {channel?.Name}.");
}
else
{
UserChannel = null;
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)
{
token = string.Empty;
if (!File.Exists(Constants.TwitchOAuthAccessTokenFilePath))
return false;
token = File.ReadAllText(Constants.TwitchOAuthAccessTokenFilePath);
return true;
}
public void SaveAccessToken(string token)
{
File.WriteAllText(Constants.TwitchOAuthAccessTokenFilePath, token);
}
public bool TryGetUserChannel(out TwitchChannel? userChannel)
{
userChannel = null;
try
{
var user = TwitchApi.Helix.Users.GetUsersAsync().Result.Users[0];
userChannel = new TwitchChannel(user.Login);
return true;
}
catch (Exception e)
{
Console.WriteLine($"[ERROR][{nameof(TwitchDataService)}] {e.Message}");
return false;
}
}
public string GetOAuthUrl()
{
var scopes = string.Join("+", Constants.TwitchOAuthScopes);
return $"https://id.twitch.tv/oauth2/authorize"
+ $"?client_id={Constants.TwitchClientId}"
+ $"&redirect_uri={Constants.TwitchOAuthRedirectUrl}"
+ $"&response_type={Constants.TwitchOAuthResponseType}"
+ $"&scope={scopes}";
}
public void StartRaid(string from, string to)
{
// TODO: Also check, if the logged in user is live
TwitchApi.Helix.Raids.StartRaidAsync(from, to);
IsRaidStarted = true;
}
public bool CanStartRaidCommand(object? arg)
{
return UserChannel?.IsLive == true && IsRaidStarted == false;
}
public void StartRaidCommand(object? arg)
{
if (arg == null || UserChannel?.BroadcasterId == null)
{
return;
}
var from = UserChannel.BroadcasterId!;
var to = arg.ToString()!;
StartRaid(from, to);
}
public void StopRaid()
{
if (UserChannel?.BroadcasterId == null)
return;
if (IsRaidStarted == false)
return;
TwitchApi.Helix.Raids.CancelRaidAsync(UserChannel.BroadcasterId);
IsRaidStarted = false;
}
public void StopRaidCommand()
{
StopRaid();
}
public void OpenChannelCommand(object? arg)
{
var channelName = arg?.ToString();
if (string.IsNullOrEmpty(channelName))
return;
var url = $"https://twitch.tv/{channelName}";
Tools.OpenUrl(url);
}
public event PropertyChangingEventHandler? PropertyChanging;
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanging([CallerMemberName] string? propertyName = null)
{
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
}
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
OnPropertyChanging(propertyName);
field = value;
OnPropertyChanged(propertyName);
return true;
}
}

View File

@ -1,291 +0,0 @@
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;
using TwitchLib.PubSub;
using TwitchLib.PubSub.Events;
namespace BetterRaid.Services.Implementations;
public class TwitchPubSubService : ITwitchPubSubService
{
private readonly Dictionary<PubSubType, List<PubSubListener>> _targets = new();
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)
{
var listeners = _targets
.Where(x => x.Key == PubSubType.VideoPlayback)
.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}");
}
else
{
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}");
}
}
}
private void OnSubOnOnListenResponse(object? sender, OnListenResponseArgs args)
{
Console.WriteLine($"Listen Response: {args.Topic}");
}
private void OnSubOnOnPubSubServiceClosed(object? sender, EventArgs args)
{
Console.WriteLine("PubSub Closed");
}
private void OnSubOnOnPubSubServiceError(object? sender, OnPubSubServiceErrorArgs args)
{
Console.WriteLine($"PubSub Error: {args.Exception.Message}");
}
private void OnSubOnOnPubSubServiceConnected(object? sender, EventArgs args)
{
Console.WriteLine("Connected to PubSub");
}
public void RegisterReceiver<T>(T receiver) where T : class
{
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()
.Concat(
type.GetFields() as MemberInfo[]
);
foreach (var target in publicTargets)
{
if (target.GetCustomAttributes<PubSubAttribute>() is not { } attrs)
{
continue;
}
foreach (var attr in attrs)
{
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
{
ChannelId = channelId,
Instance = receiver,
Listener = target
});
}
else
{
_targets.Add(attr.Type, [
new PubSubListener
{
ChannelId = channelId,
Instance = receiver,
Listener = target
}
]);
}
_sub.ListenToVideoPlayback(channelId);
_sub.SendTopics(_dataService.AccessToken, true);
}
}
}
public void UnregisterReceiver<T>(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)
{
_sub.ListenToVideoPlayback(l.ChannelId);
_sub.SendTopics(_dataService.AccessToken, true);
}
_targets[topic].RemoveAll(x => x.Instance == receiver);
}
}
}

349
Services/TwitchService.cs Normal file
View File

@ -0,0 +1,349 @@
using System;
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;
using TwitchLib.Api.Helix.Models.Users.GetUsers;
using TwitchLib.PubSub;
using TwitchLib.PubSub.Events;
using OnEmoteOnlyArgs = TwitchLib.PubSub.Events.OnEmoteOnlyArgs;
using OnLogArgs = TwitchLib.PubSub.Events.OnLogArgs;
namespace BetterRaid.Services;
public interface ITwitchService
{
public string? AccessToken { get; }
public TwitchChannel? UserChannel { get; set; }
public TwitchAPI TwitchApi { get; }
public bool IsRaidStarted { get; set; }
public Task ConnectApiAsync(string clientId, string accessToken);
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();
public void OpenChannelCommand(object? arg);
public void RegisterForEvents(TwitchChannel channel);
public void UnregisterFromEvents(TwitchChannel channel);
public event PropertyChangingEventHandler? PropertyChanging;
public event PropertyChangedEventHandler? PropertyChanged;
}
public sealed class TwitchService : ITwitchService, INotifyPropertyChanged, INotifyPropertyChanging
{
private bool _isRaidStarted;
private TwitchChannel? _userChannel;
private readonly List<TwitchChannel> _registeredChannels;
private User? _user;
public string AccessToken { get; private set; } = string.Empty;
public bool IsRaidStarted
{
get => _isRaidStarted;
set => SetField(ref _isRaidStarted, value);
}
public User? User
{
get => _user;
set => SetField(ref _user, value);
}
public TwitchChannel? UserChannel
{
get => _userChannel;
set
{
if (_userChannel != null && _userChannel.Name?.Equals(value?.Name) == true)
return;
SetField(ref _userChannel, value);
_userChannel?.UpdateChannelData(this);
}
}
public TwitchAPI TwitchApi { get; }
public TwitchPubSub TwitchEvents { get; }
public TwitchService()
{
_registeredChannels = [];
TwitchApi = new TwitchAPI();
TwitchEvents = new TwitchPubSub();
if (TryLoadAccessToken(out var token))
{
Console.WriteLine($"[INFO][{nameof(TwitchService)}] Found access token.");
Task.Run(() => ConnectApiAsync(Constants.TwitchClientId, token))
.ContinueWith(_ => ConnectTwitchEvents(token));
}
else
{
Console.WriteLine($"[INFO][{nameof(TwitchService)}] No access token found.");
}
}
private async Task ConnectTwitchEvents(string token)
{
if (UserChannel == null || User == null)
return;
Console.WriteLine($"[INFO][{nameof(TwitchService)}] Connecting to Twitch Events ...");
TwitchEvents.OnRaidGo += OnUserRaidGo;
TwitchEvents.OnRaidUpdateV2 += OnUserRaidUpdate;
TwitchEvents.OnStreamUp += OnUserStreamUp;
TwitchEvents.OnStreamDown += OnUserStreamDown;
TwitchEvents.ListenToRaid(UserChannel.BroadcasterId);
TwitchEvents.ListenToVideoPlayback(UserChannel.BroadcasterId);
TwitchEvents.SendTopics(token);
TwitchEvents.Connect();
RegisterForEvents(UserChannel);
Console.WriteLine($"[INFO][{nameof(TwitchService)}] Connected to Twitch Events.");
await Task.CompletedTask;
}
public async Task ConnectApiAsync(string clientId, string accessToken)
{
Console.WriteLine($"[INFO][{nameof(TwitchService)}] Connecting to Twitch API ...");
AccessToken = accessToken;
TwitchApi.Settings.ClientId = clientId;
TwitchApi.Settings.AccessToken = accessToken;
if (TryGetUser(out var user))
{
User = user;
}
else
{
User = null;
Console.WriteLine($"[ERROR][{nameof(TwitchService)}] Could not get user.");
}
if (TryGetUserChannel(out var channel))
{
UserChannel = channel;
Console.WriteLine($"[INFO][{nameof(TwitchService)}] Connected to Twitch API as {channel?.Name}.");
}
else
{
UserChannel = null;
Console.WriteLine($"[ERROR][{nameof(TwitchService)}] Could not get user channel.");
}
if (User == null || UserChannel == null)
{
Console.WriteLine($"[ERROR][{nameof(TwitchService)}] Could not connect to Twitch API.");
}
await Task.CompletedTask;
}
private bool TryLoadAccessToken(out string token)
{
token = string.Empty;
if (!File.Exists(Constants.TwitchOAuthAccessTokenFilePath))
return false;
token = File.ReadAllText(Constants.TwitchOAuthAccessTokenFilePath);
return true;
}
public void SaveAccessToken(string token)
{
File.WriteAllText(Constants.TwitchOAuthAccessTokenFilePath, token);
}
public bool TryGetUser(out User? user)
{
user = null;
try
{
var userResult = TwitchApi.Helix.Users.GetUsersAsync().Result.Users[0];
if (userResult == null)
{
return false;
}
user = userResult;
return true;
}
catch (Exception e)
{
Console.WriteLine($"[ERROR][{nameof(TwitchService)}] {e.Message}");
return false;
}
}
public bool TryGetUserChannel(out TwitchChannel? channel)
{
channel = null;
if (User == null)
return false;
channel = new TwitchChannel(User.Login);
return true;
}
public void RegisterForEvents(TwitchChannel channel)
{
TwitchEvents.OnStreamUp += channel.OnStreamUp;
TwitchEvents.OnStreamDown += channel.OnStreamDown;
TwitchEvents.OnViewCount += channel.OnViewCount;
TwitchEvents.ListenToVideoPlayback(channel.BroadcasterId);
TwitchEvents.SendTopics(AccessToken);
_registeredChannels.Add(channel);
}
public void UnregisterFromEvents(TwitchChannel channel)
{
TwitchEvents.OnStreamUp -= channel.OnStreamUp;
TwitchEvents.OnStreamDown -= channel.OnStreamDown;
TwitchEvents.OnViewCount -= channel.OnViewCount;
TwitchEvents.ListenToVideoPlayback(channel.BroadcasterId);
TwitchEvents.SendTopics(AccessToken, true);
_registeredChannels.Remove(channel);
}
public string GetOAuthUrl()
{
var scopes = string.Join("+", Constants.TwitchOAuthScopes);
return $"https://id.twitch.tv/oauth2/authorize"
+ $"?client_id={Constants.TwitchClientId}"
+ $"&redirect_uri={Constants.TwitchOAuthRedirectUrl}"
+ $"&response_type={Constants.TwitchOAuthResponseType}"
+ $"&scope={scopes}";
}
public void StartRaid(string from, string to)
{
// TODO: Also check, if the logged in user is live
TwitchApi.Helix.Raids.StartRaidAsync(from, to);
IsRaidStarted = true;
}
public bool CanStartRaidCommand(object? arg)
{
return UserChannel?.IsLive == true && IsRaidStarted == false;
}
public void StartRaidCommand(object? arg)
{
if (arg == null || UserChannel?.BroadcasterId == null)
{
return;
}
var from = UserChannel.BroadcasterId!;
var to = arg.ToString()!;
StartRaid(from, to);
}
public void StopRaid()
{
if (UserChannel?.BroadcasterId == null)
return;
if (IsRaidStarted == false)
return;
TwitchApi.Helix.Raids.CancelRaidAsync(UserChannel.BroadcasterId);
IsRaidStarted = false;
}
public void StopRaidCommand()
{
StopRaid();
}
public void OpenChannelCommand(object? arg)
{
var channelName = arg?.ToString();
if (string.IsNullOrEmpty(channelName))
return;
var url = $"https://twitch.tv/{channelName}";
Tools.OpenUrl(url);
}
private void OnUserRaidUpdate(object? sender, OnRaidUpdateV2Args e)
{
}
private void OnUserRaidGo(object? sender, OnRaidGoArgs e)
{
IsRaidStarted = false;
}
private void OnUserStreamDown(object? sender, OnStreamDownArgs e)
{
IsRaidStarted = false;
}
private void OnUserStreamUp(object? sender, OnStreamUpArgs e)
{
IsRaidStarted = false;
}
public event PropertyChangingEventHandler? PropertyChanging;
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void OnPropertyChanging([CallerMemberName] string? propertyName = null)
{
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
}
private bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
OnPropertyChanging(propertyName);
field = value;
OnPropertyChanged(propertyName);
return true;
}
}

View File

@ -18,7 +18,6 @@ public class MainWindowViewModel : ViewModelBase
private string? _filter;
private ObservableCollection<TwitchChannel> _channels = [];
private readonly BetterRaidDatabase? _db;
private readonly ITwitchPubSubService _pubSub;
private readonly ISynchronizaionService _synchronizaionService;
public BetterRaidDatabase? Database
@ -41,7 +40,7 @@ public class MainWindowViewModel : ViewModelBase
public ObservableCollection<TwitchChannel> FilteredChannels => GetFilteredChannels();
public ITwitchDataService DataService { get; }
public ITwitchService Twitch { get; }
public string? Filter
{
@ -49,14 +48,14 @@ public class MainWindowViewModel : ViewModelBase
set => SetProperty(ref _filter, value);
}
public bool IsLoggedIn => DataService.UserChannel != null;
public bool IsLoggedIn => Twitch.UserChannel != null;
public MainWindowViewModel(ITwitchPubSubService pubSub, ITwitchDataService dataService, ISynchronizaionService synchronizaionService)
public MainWindowViewModel(ITwitchService twitch, ISynchronizaionService synchronizaionService)
{
_pubSub = pubSub;
DataService = dataService;
_synchronizaionService = synchronizaionService;
DataService.PropertyChanged += OnDataServicePropertyChanged;
Twitch = twitch;
Twitch.PropertyChanged += OnTwitchPropertyChanged;
Database = BetterRaidDatabase.LoadFromFile(Constants.DatabaseFilePath);
Database.PropertyChanged += OnDatabasePropertyChanged;
@ -77,7 +76,7 @@ public class MainWindowViewModel : ViewModelBase
public void LoginWithTwitch()
{
Tools.StartOAuthLogin(DataService.GetOAuthUrl(), DataService, OnTwitchLoginCallback, CancellationToken.None);
Tools.StartOAuthLogin(Twitch, OnTwitchLoginCallback, CancellationToken.None);
}
private void OnTwitchLoginCallback()
@ -94,7 +93,7 @@ public class MainWindowViewModel : ViewModelBase
foreach (var channel in Channels)
{
_pubSub.UnregisterReceiver(channel);
Twitch.UnregisterFromEvents(channel);
}
Channels.Clear();
@ -107,8 +106,8 @@ public class MainWindowViewModel : ViewModelBase
{
Task.Run(() =>
{
channel.UpdateChannelData(DataService);
_pubSub.RegisterReceiver(channel);
channel.UpdateChannelData(Twitch);
Twitch.RegisterForEvents(channel);
});
Channels.Add(channel);
@ -125,9 +124,9 @@ public class MainWindowViewModel : ViewModelBase
return new ObservableCollection<TwitchChannel>(filteredChannels);
}
private void OnDataServicePropertyChanged(object? sender, PropertyChangedEventArgs e)
private void OnTwitchPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName != nameof(DataService.UserChannel))
if (e.PropertyName != nameof(Twitch.UserChannel))
return;
OnPropertyChanged(nameof(IsLoggedIn));

View File

@ -33,7 +33,7 @@
Width="40"
Height="40"
Margin="5"
Source="{Binding DataService.UserChannel.ThumbnailUrl,
Source="{Binding Twitch.UserChannel.ThumbnailUrl,
FallbackValue={x:Static misc:Constants.ChannelPlaceholderImageUrl},
TargetNullValue={x:Static misc:Constants.ChannelPlaceholderImageUrl}}" />
@ -42,9 +42,9 @@
FontWeight="Bold">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0} ({1})">
<Binding Path="DataService.UserChannel.DisplayName"
<Binding Path="Twitch.UserChannel.DisplayName"
FallbackValue="-" />
<Binding Path="DataService.UserChannel.ViewerCount"
<Binding Path="Twitch.UserChannel.ViewerCount"
FallbackValue="Offline"
TargetNullValue="Offline" />
</MultiBinding>
@ -210,8 +210,8 @@
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
IsEnabled="{Binding IsLive}"
IsVisible="{Binding !$parent[Window].((vm:MainWindowViewModel)DataContext).DataService.IsRaidStarted}"
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).DataService.StartRaidCommand}"
IsVisible="{Binding !$parent[Window].((vm:MainWindowViewModel)DataContext).Twitch.IsRaidStarted}"
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).Twitch.StartRaidCommand}"
CommandParameter="{Binding BroadcasterId}" />
<Button Content="Cancel Raid"
@ -223,8 +223,8 @@
VerticalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
IsVisible="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).DataService.IsRaidStarted}"
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).DataService.StopRaidCommand}" />
IsVisible="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).Twitch.IsRaidStarted}"
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).Twitch.StopRaidCommand}" />
<Button Content="View Channel"
Height="50"
@ -234,7 +234,7 @@
VerticalAlignment="Stretch"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).DataService.OpenChannelCommand}"
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).Twitch.OpenChannelCommand}"
CommandParameter="{Binding Name}" />
</StackPanel>
</Grid>