using System;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Diagnostics;
using OwlCore.AbstractStorage;
using OwlCore.AbstractUI.Models;
using OwlCore.Extensions;
using OwlCore.Services;
using StrixMusic.Cores.Files;
using StrixMusic.Cores.Files.Models;
using StrixMusic.Cores.OneDrive.ConfigPanels;
using StrixMusic.Cores.OneDrive.Services;
using StrixMusic.Cores.OneDrive.Storage;
using StrixMusic.Sdk.AppModels;
using StrixMusic.Sdk.CoreModels;
using StrixMusic.Sdk.FileMetadata;
using StrixMusic.Sdk.Services;
namespace StrixMusic.Cores.OneDrive
{
///
/// Scan and play audio files from OneDrive.
///
public sealed class OneDriveCore : FilesCore
{
private readonly IFolderData _metadataStorage;
private readonly AbstractButton _completeGenericSetupButton;
private AbstractUICollection _configPanel;
///
/// Initializes a new instance of the class.
///
/// A unique identifier for this core instance.
/// A folder abstraction where this core can persist settings data beyond the lifetime of the application.
/// A folder abstraction where this core can persist metadata for scanned files.
/// A service that can notify the user with interactive UI or messages.
public OneDriveCore(string instanceId, IFolderData settingsStorage, IFolderData metadataStorage, INotificationService notificationService)
: base(instanceId)
{
_metadataStorage = metadataStorage;
NotificationService = notificationService;
Settings = new OneDriveCoreSettings(settingsStorage);
_configPanel = new AbstractUICollection(string.Empty);
_completeGenericSetupButton = new AbstractButton(Guid.NewGuid().ToString(), "OK");
_completeGenericSetupButton.Clicked += CompleteGenericSetupButton_Clicked;
}
///
/// Initializes a new instance of the class.
///
///
/// This overload allows passing preconfigured settings that, if all values are valid, will allow initialization to complete without
/// any interaction from the user.
///
/// A unique identifier for this core instance.
/// A preconfigured instance of that will be used instead of a new instance with default values.
/// A folder abstraction where this core can persist metadata for scanned files.
/// A service that can notify the user with interactive UI or messages.
public OneDriveCore(string instanceId, OneDriveCoreSettings settings, IFolderData metadataStorage, INotificationService notificationService)
: base(instanceId)
{
_metadataStorage = metadataStorage;
NotificationService = notificationService;
Settings = settings;
_configPanel = new AbstractUICollection(string.Empty);
_completeGenericSetupButton = new AbstractButton(Guid.NewGuid().ToString(), "OK");
_completeGenericSetupButton.Clicked += CompleteGenericSetupButton_Clicked;
}
///
public override CoreMetadata Registration { get; } = Metadata;
///
/// The metadata that identifies this core before instantiation.
///
public static CoreMetadata Metadata { get; } = new CoreMetadata(id: nameof(OneDriveCore),
displayName: "OneDrive",
logoUri: new Uri("ms-appx:///Assets/Cores/OneDrive/Logo.svg"),
sdkVer: Version.Parse("0.0.0.0"));
///
public override string InstanceDescriptor { get; set; } = string.Empty;
///
public override AbstractUICollection AbstractConfigPanel => _configPanel;
///
/// The message handler to use or requests (wherever possible).
///
public HttpMessageHandler HttpMessageHandler { get; set; } = new HttpClientHandler();
///
/// The settings for this core instance.
///
public OneDriveCoreSettings Settings { get; }
///
/// The method that should be used for login.
///
public LoginMethod LoginMethod { get; set; }
///
/// Gets a service that can notify the user with interactive UI or generic messages.
///
internal INotificationService NotificationService { get; }
///
/// Raised when the user requests to visit an external web page for OneDrive login.
///
public event EventHandler? LoginNavigationRequested;
///
public event EventHandler? MsalPublicClientApplicationBuilderCreated;
///
public event EventHandler? MsalAcquireTokenInteractiveParameterBuilderCreated;
///
public override event EventHandler? CoreStateChanged;
///
public override event EventHandler? InstanceDescriptorChanged;
///
public override event EventHandler? AbstractConfigPanelChanged;
///
public async override Task InitAsync(CancellationToken cancellationToken = default)
{
var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cancellationToken = cancellationTokenSource.Token;
ChangeCoreState(CoreState.Loading);
Logger.LogInformation("Getting setting values");
await Settings.LoadAsync();
using var onCancelledRegistration = cancellationTokenSource.Token.Register(() => ChangeCoreState(CoreState.Unloaded));
if (string.IsNullOrWhiteSpace(Settings.ClientId) ||
string.IsNullOrWhiteSpace(Settings.TenantId) ||
string.IsNullOrWhiteSpace(Settings.RedirectUri) ||
!Settings.UserHasSeenAuthClientKeysSettings)
{
Logger.LogInformation("Need custom app key values");
ChangeCoreState(CoreState.NeedsConfiguration);
var oobePanel = new OutOfBoxExperiencePanel(Settings, NotificationService, cancellationToken);
_configPanel = oobePanel;
AbstractConfigPanelChanged?.Invoke(this, EventArgs.Empty);
await oobePanel.ExecuteCustomAppKeyStageAsync();
Settings.UserHasSeenAuthClientKeysSettings = true;
await Settings.SaveAsync();
}
if (!Settings.UserHasSeenGeneralOobeSettings)
{
Logger.LogInformation("Displaying general OOBE settings");
ChangeCoreState(CoreState.NeedsConfiguration);
var oobePanel = new OutOfBoxExperiencePanel(Settings, NotificationService, cancellationToken);
_configPanel = oobePanel;
AbstractConfigPanelChanged?.Invoke(this, EventArgs.Empty);
await oobePanel.ExecuteSettingsStageAsync();
Settings.UserHasSeenGeneralOobeSettings = true;
await Settings.SaveAsync();
}
if (string.IsNullOrWhiteSpace(Settings.AccountIdentifier))
{
Logger.LogInformation("User needs to login");
ChangeCoreState(CoreState.NeedsConfiguration);
try
{
Settings.AccountIdentifier = await AcquireLoginAsync(cancellationToken);
await Settings.SaveAsync();
}
catch (HttpRequestException)
{
RaiseFailedConnectionState();
return;
}
catch (AggregateException ex)
{
if (ex.InnerExceptions.Any(x => x is HttpRequestException))
{
ex.Handle(x =>
{
if (x is not HttpRequestException)
return false;
RaiseFailedConnectionState();
return true;
});
cancellationTokenSource.Cancel();
return;
}
}
}
if (string.IsNullOrWhiteSpace(Settings.SelectedFolderId))
{
Logger.LogInformation("User needs to pick folder");
ChangeCoreState(CoreState.NeedsConfiguration);
var folder = await AcquireUserSelectedFolderAsync(cancellationToken);
if (folder is null)
{
cancellationTokenSource.Cancel();
return;
}
Settings.SelectedFolderId = folder.Id ?? string.Empty;
await Settings.SaveAsync();
}
Logger.LogInformation("Fully configured, setting state.");
ChangeCoreState(CoreState.Configured);
ChangeCoreState(CoreState.Loading);
var authManager = new AuthenticationManager(Settings.ClientId, Settings.TenantId, Settings.RedirectUri)
{
HttpMessageHandler = HttpMessageHandler,
};
var authenticationToken = await authManager.TryAcquireCachedTokenAsync(Settings.AccountIdentifier, cancellationToken);
Guard.IsNotNull(authenticationToken, nameof(authenticationToken));
InstanceDescriptor = authenticationToken.Account.Username;
InstanceDescriptorChanged?.Invoke(this, InstanceDescriptor);
var graphClient = authManager.CreateGraphClient(authenticationToken.AccessToken);
var driveItem = await graphClient.Drive.Items[Settings.SelectedFolderId].Request().GetAsync(cancellationToken);
Guard.IsNotNull(driveItem, nameof(driveItem));
var folderToScan = new OneDriveFolderData(graphClient, driveItem);
FileMetadataManager = new FileMetadataManager(folderToScan, _metadataStorage, NotificationService);
// Scanning file contents are possible but extremely slow over the network.
// The Graph API supplies music metadata from file properties, which is much faster.
// Use the user's preferences.
var scanTypes = MetadataScanTypes.None;
if (Settings.ScanWithTagLib)
scanTypes |= MetadataScanTypes.TagLib;
if (Settings.ScanWithFileProperties)
scanTypes |= MetadataScanTypes.FileProperties;
FileMetadataManager.ScanTypes = scanTypes;
FileMetadataManager.DegreesOfParallelism = 8;
await FileMetadataManager.InitAsync(cancellationToken);
_ = FileMetadataManager.ScanAsync(cancellationToken);
ChangeCoreState(CoreState.Loaded);
Logger.LogInformation("Post config task: setting up generic config UI.");
var doneButton = new AbstractButton($"{nameof(GeneralCoreConfigPanel)}DoneButton", "Done");
doneButton.Clicked += GeneralConfigPanelDoneButtonClicked;
var ui = new GeneralCoreConfigPanel(Settings, NotificationService)
{
doneButton
};
_configPanel = ui;
AbstractConfigPanelChanged?.Invoke(this, EventArgs.Empty);
Logger.LogInformation("Initializing library");
await Library.Cast().InitAsync();
void RaiseFailedConnectionState()
{
NotificationService?.RaiseNotification("Connection failed", "We weren't able to contact OneDrive");
ChangeInstanceDescriptor("Login failed");
}
}
private void GeneralConfigPanelDoneButtonClicked(object sender, EventArgs e)
{
ChangeCoreState(CoreState.Configured);
ChangeCoreState(CoreState.Loaded);
}
private async Task AcquireUserSelectedFolderAsync(CancellationToken cancellationToken)
{
var authManager = new AuthenticationManager(Settings.ClientId, Settings.TenantId, Settings.RedirectUri)
{
HttpMessageHandler = HttpMessageHandler,
};
authManager.MsalAcquireTokenInteractiveParameterBuilderCreated += OnInteractiveParamBuilderCreated;
authManager.MsalPublicClientApplicationBuilderCreated += OnPublicClientApplicationBuilderCreated;
var authenticationToken = await authManager.TryAcquireCachedTokenAsync(Settings.AccountIdentifier, cancellationToken);
if (authenticationToken is null)
return null;
var graphClient = authManager.CreateGraphClient(authenticationToken.AccessToken);
authManager.MsalAcquireTokenInteractiveParameterBuilderCreated -= OnInteractiveParamBuilderCreated;
authManager.MsalPublicClientApplicationBuilderCreated -= OnPublicClientApplicationBuilderCreated;
var user = await graphClient.Users.Request().GetAsync(cancellationToken);
var driveItem = await graphClient.Drive.Root.Request().Expand("children").GetAsync(cancellationToken);
var rootFolder = new OneDriveFolderData(graphClient, driveItem);
var oobePanel = new OutOfBoxExperiencePanel(Settings, NotificationService, cancellationToken);
_configPanel = oobePanel;
AbstractConfigPanelChanged?.Invoke(this, EventArgs.Empty);
return await oobePanel.ExecuteFolderPickerStageAsync(rootFolder, user.FirstOrDefault()?.DisplayName, Settings.AccountIdentifier);
}
private async Task AcquireLoginAsync(CancellationToken cancellationToken)
{
var authManager = new AuthenticationManager(Settings.ClientId, Settings.TenantId, Settings.RedirectUri)
{
HttpMessageHandler = HttpMessageHandler,
};
authManager.MsalAcquireTokenInteractiveParameterBuilderCreated += OnInteractiveParamBuilderCreated;
authManager.MsalPublicClientApplicationBuilderCreated += OnPublicClientApplicationBuilderCreated;
var authenticationToken = await authManager.TryAcquireCachedTokenAsync(Settings.AccountIdentifier, cancellationToken);
if (authenticationToken is null)
{
var oobePanel = new OutOfBoxExperiencePanel(Settings, NotificationService, cancellationToken);
_configPanel = oobePanel;
AbstractConfigPanelChanged?.Invoke(this, EventArgs.Empty);
if (LoginMethod == LoginMethod.Interactive)
{
oobePanel.DisplayInteractiveLoginStageAsync();
authenticationToken = await authManager.TryAcquireTokenViaInteractiveLoginAsync(cancellationToken);
}
else if (LoginMethod == LoginMethod.DeviceCode)
{
var deviceCodePanel = oobePanel.DisplayDeviceCodeLoginStageAsync();
deviceCodePanel.AuthenticateButton.Clicked += AuthenticateButtonOnClicked;
authenticationToken = await authManager.TryAcquireTokenViaDeviceCodeLoginAsync(x =>
{
deviceCodePanel.VerificationUri = new Uri(x.VerificationUrl);
deviceCodePanel.Code = x.UserCode;
return Task.CompletedTask;
}, cancellationToken);
deviceCodePanel.AuthenticateButton.Clicked -= AuthenticateButtonOnClicked;
void AuthenticateButtonOnClicked(object sender, EventArgs e) => LoginNavigationRequested?.Invoke(this, deviceCodePanel.VerificationUri ?? ThrowHelper.ThrowArgumentNullException(nameof(deviceCodePanel.VerificationUri)));
}
else
throw new ArgumentOutOfRangeException();
}
Guard.IsNotNull(authenticationToken, nameof(authenticationToken));
Guard.IsNotNullOrWhiteSpace(authenticationToken.Account.Username, nameof(authenticationToken.Account.Username));
authManager.MsalAcquireTokenInteractiveParameterBuilderCreated -= OnInteractiveParamBuilderCreated;
authManager.MsalPublicClientApplicationBuilderCreated -= OnPublicClientApplicationBuilderCreated;
return authenticationToken.Account.HomeAccountId.Identifier;
}
internal void ChangeCoreState(CoreState state)
{
CoreState = state;
CoreStateChanged?.Invoke(this, state);
}
internal void ChangeInstanceDescriptor(string instanceDescriptor)
{
InstanceDescriptor = instanceDescriptor;
InstanceDescriptorChanged?.Invoke(this, InstanceDescriptor);
}
private void CompleteGenericSetupButton_Clicked(object sender, EventArgs e)
{
ChangeCoreState(CoreState.Configured);
ChangeCoreState(CoreState.Loaded);
}
public override ValueTask DisposeAsync()
{
_completeGenericSetupButton.Clicked -= CompleteGenericSetupButton_Clicked;
return base.DisposeAsync();
}
private void OnInteractiveParamBuilderCreated(object sender, AcquireTokenInteractiveParameterBuilderCreatedEventArgs args) => MsalAcquireTokenInteractiveParameterBuilderCreated?.Invoke(this, args);
private void OnPublicClientApplicationBuilderCreated(object sender, MsalPublicClientApplicationBuilderCreatedEventArgs args) => MsalPublicClientApplicationBuilderCreated?.Invoke(this, args);
}
}