[CHANGE] Reworked client creation

This commit is contained in:
max
2025-09-08 21:28:33 +02:00
parent b2c9fc2c52
commit 680b6d2cc9
19 changed files with 563 additions and 1347 deletions

View File

@@ -24,7 +24,6 @@ public partial class AuthenticationHasher : ComponentBase
private void Hash()
{
var hashedValue= AuthenticationUtilities.GetSapisidHash(DatasyncId, SecureCookie, Origin, Time);
OutputHash = hashedValue ?? "Hash failed!";
OutputHash = AuthenticationUtilities.GetSapisidHash(DatasyncId, SecureCookie, Origin, Time);
}
}

View File

@@ -2,112 +2,195 @@
<ForcedLoadingOverlay Visible="_isLoading"/>
@{
var client = PreparingClient?.YouTubeClient;
var clientState = client?.State;
var channel = PreparingClient?.Channel;
var avatar = channel?.AvatarImages.FirstOrDefault();
var banner = channel?.BannerImages.FirstOrDefault();
}
<MudDialog>
<TitleContent>
<MudText Typo="Typo.h6">Add new account</MudText>
</TitleContent>
<DialogContent>
<MudStack Spacing="2">
<MudStack Row Spacing="2" AlignItems="AlignItems.Start" Justify="Justify.SpaceEvenly" StretchItems="StretchItems.All">
@switch (_steps)
{
case AccountImportSteps.Authenticate:
<MudStack Spacing="2">
<MudTextField Label="UserAgent" Required @bind-Value="@Client.UserAgent"/>
</MudStack>
<MudSimpleTable Bordered Dense Elevation="0" Outlined Square Hover>
<tbody>
<tr>
<td>Account id:</td>
<td>@Client.Id</td>
</tr>
<tr>
<td>Account name:</td>
<td>@Client.External.Channel?.ChannelName</td>
</tr>
<tr>
<td>Account handle:</td>
<td>@Client.External.Channel?.Handle</td>
</tr>
<tr>
<td>Logged in:</td>
<td style="@($"color: {(Client.External.State?.LoggedIn ?? false ? "green" : "red")}")">@Client.External.State?.LoggedIn</td>
</tr>
<tr>
<td>YouTube Premium:</td>
<td style="@($"color: {(Client.External.State?.IsPremiumUser ?? false ? "green" : "red")}")">@Client.External.State?.IsPremiumUser</td>
</tr>
<tr>
<td>User agent:</td>
<td>@Client.UserAgent</td>
</tr>
<tr>
<td>InnerTube client:</td>
<td>@Client.External.State?.InnerTubeClient</td>
</tr>
<tr>
<td>InnerTube client version:</td>
<td>@Client.External.State?.InnerTubeClientVersion</td>
</tr>
<tr>
<td>InnerTube API key:</td>
<td>@Client.External.State?.InnertubeApiKey</td>
</tr>
<tr>
<td>Language:</td>
<td>@Client.External.State?.InnerTubeContext?.InnerTubeClient?.HLanguage</td>
</tr>
</tbody>
</MudSimpleTable>
@{
var avatar = Client.External.Channel?.AvatarImages.FirstOrDefault();
}
@if (avatar != null)
{
<MudImage Src="@avatar.Url" Elevation="0" ObjectFit="ObjectFit.ScaleDown" Width="75"/>
}
</MudStack>
<MudPaper Elevation="0" Outlined Class="pa-2">
<MudSwitch @bind-Value="@IsAnonymous" Color="Color.Info">Anonymous client</MudSwitch>
<MudTextField @bind-Value="@DefaultUserAgent" Required Label="User agent"
HelperText="Use an WEB client user agent."/>
</MudPaper>
<MudPaper Elevation="0" Outlined Class="pa-2">
<MudText>Import cookies</MudText>
<MudForm @bind-IsValid="@_cookieTextValid" Disabled="@(Client.CookieContainer.Count != 0)">
<MudTextField @bind-Value="@_cookieDomain" Immediate Required Label="Domain" RequiredError="Domain is required."/>
<MudTextField Class="my-2" Lines="4" AutoGrow @bind-Value="@_cookieText" Immediate Required Label="Cookies" Placeholder="EXAMPLE: Cookie1=Value1; Cookie2=Value2;"
Validation="@(new Func<string, string?>(ValidateCookieText))"/>
<MudButton Variant="Variant.Outlined" Disabled="@(!_cookieTextValid)" OnClick="ParseCookies">Load</MudButton>
</MudForm>
</MudPaper>
<MudStack Row Spacing="2" Style="height: 100%">
<MudPaper Elevation="0" Outlined Class="pa-2" Style="width: 50%;">
<MudText>Import cookies</MudText>
<MudText Typo="Typo.caption">@($"{ImportCookies.Count} cookie(s) imported")</MudText>
<MudForm @bind-IsValid="@_cookieImportTextValid" Disabled="@(IsAnonymous)">
<MudTextField @bind-Value="@_cookieDomain" Immediate Required Label="Domain"
RequiredError="Domain is required."/>
<MudTextField Class="my-2" Lines="4" AutoGrow @bind-Value="@_cookieText" Immediate
Required Label="Cookies" Variant="Variant.Outlined"
Placeholder="EXAMPLE: Cookie1=Value1; Cookie2=Value2;"
Validation="@(new Func<string, string?>(ValidateCookieText))"/>
<MudButton Variant="Variant.Outlined" Disabled="@(!_cookieImportTextValid)"
OnClick="ParseCookies">Import
</MudButton>
</MudForm>
</MudPaper>
<MudDataGrid Items="Client.CookieContainer.GetAllCookies()" Dense Elevation="0" Outlined>
<Header>
<MudStack Class="ma-2">
<MudText>Cookies</MudText>
<MudDataGrid Items="ImportCookies" Dense Elevation="0" Outlined Style="width: 50%;">
<Header>
<MudStack Class="ma-2">
<MudText>Cookies</MudText>
</MudStack>
</Header>
<Columns>
<TemplateColumn Title="Name">
<CellTemplate>
<MudTextField Variant="Variant.Text" @bind-Value="@context.Item.Name"
Immediate/>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="Domain">
<CellTemplate>
<MudTextField Variant="Variant.Text" @bind-Value="@context.Item.Domain"
Immediate/>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="Value">
<CellTemplate>
<MudTextField Variant="Variant.Text" @bind-Value="@context.Item.Value"
Immediate/>
</CellTemplate>
</TemplateColumn>
</Columns>
</MudDataGrid>
</MudStack>
</Header>
<Columns>
<TemplateColumn Title="Name">
<CellTemplate>
<MudTextField Variant="Variant.Text" @bind-Value="@context.Item.Name" Immediate/>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="Domain">
<CellTemplate>
<MudTextField Variant="Variant.Text" @bind-Value="@context.Item.Domain" Immediate/>
</CellTemplate>
</TemplateColumn>
<TemplateColumn Title="Value">
<CellTemplate>
<MudTextField Variant="Variant.Text" @bind-Value="@context.Item.Value" Immediate/>
</CellTemplate>
</TemplateColumn>
<PropertyColumn Title="Expires" Property="x => x.Expires"/>
</Columns>
</MudDataGrid>
</MudStack>
</MudStack>
break;
case AccountImportSteps.Validate:
<MudStack Spacing="3">
<MudPaper Elevation="0">
@if (banner != null)
{
<MudImage Src="@banner.Url" Height="250" Style="width: 100%;"/>
}
else
{
<div Class="account-banner" Style="height: 250px; width: 100%;"></div>
}
<MudStack Row Spacing="3" Class="px-4">
@if (avatar != null)
{
<MudImage Src="@avatar.Url" Class="mt-n5" Height="100" Width="100"/>
}
else
{
<div Class="avatar-pattern mt-n6" Style="height: 100px; width: 100px;"></div>
}
<MudStack Spacing="0">
<MudText Typo="Typo.h5">@(channel?.ChannelName ?? client?.Id)</MudText>
<MudText Typo="Typo.caption">@(string.IsNullOrWhiteSpace(channel?.Description) ? "No description!" : channel.Description)</MudText>
</MudStack>
</MudStack>
</MudPaper>
<MudSimpleTable Bordered Dense Elevation="0" Outlined Square Hover>
<tbody>
<tr>
<td>Account id:</td>
<td>@client?.Id</td>
</tr>
<tr>
<td>Account name:</td>
<td>@channel?.ChannelName</td>
</tr>
<tr>
<td>Account handle:</td>
<td>@channel?.Handle</td>
</tr>
<tr>
<td>Logged in:</td>
<td style="@($"color: {(clientState?.LoggedIn ?? false ? "green" : "red")}")">@clientState?.LoggedIn</td>
</tr>
<tr>
<td>YouTube Premium:</td>
<td style="@($"color: {(clientState?.IsPremiumUser ?? false ? "green" : "red")}")">@clientState?.IsPremiumUser</td>
</tr>
<tr>
<td>User agent:</td>
<td>@client?.UserAgent</td>
</tr>
<tr>
<td>InnerTube client:</td>
<td>@clientState?.InnerTubeClient</td>
</tr>
<tr>
<td>InnerTube client version:</td>
<td>@clientState?.InnerTubeClientVersion</td>
</tr>
<tr>
<td>InnerTube API key:</td>
<td>@clientState?.InnertubeApiKey</td>
</tr>
<tr>
<td>Language:</td>
<td>@clientState?.InnerTubeContext?.InnerTubeClient?.HLanguage</td>
</tr>
</tbody>
</MudSimpleTable>
</MudStack>
break;
}
</DialogContent>
<DialogActions>
<MudStack Spacing="2" Row>
<MudButton Color="Color.Error" OnClick="() => MudDialog?.Cancel()" Variant="Variant.Outlined">Cancel</MudButton>
<MudButton Color="Color.Info" Variant="Variant.Outlined" OnClick="ValidateAccount" Disabled="@(!CanValidate())">Validate</MudButton>
<MudButton Color="Color.Primary" Variant="Variant.Outlined" Disabled="@(!CanSave())" OnClick="OnSave">Save</MudButton>
<MudButton Color="Color.Error" OnClick="() => MudDialog?.Cancel()" Variant="Variant.Outlined">Cancel
</MudButton>
<MudButton Color="Color.Info" OnClick="ClearPreparedClient" Variant="Variant.Outlined">Reset</MudButton>
<MudButton Color="Color.Primary" OnClick="OnNextStep" Disabled="@(!CanContinue())" Variant="Variant.Outlined">@(_steps == AccountImportSteps.Validate ? "Save" : "Next")</MudButton>
</MudStack>
</DialogActions>
</MudDialog>
</MudDialog>
<style>
.account-banner {
width: 100%;
height: 180px;
display: flex;
align-items: flex-end;
padding: 1rem;
border-radius: 12px;
/* Pattern background */
background: repeating-linear-gradient(
135deg,
#1976d2, /* MudBlazor Primary */
#1976d2 20px,
#1565c0 20px,
#1565c0 40px
);
color: white;
}
.avatar-pattern {
width: 80px;
height: 80px;
font-weight: bold;
font-size: 1.5rem;
color: white;
/* Patterned background */
background: repeating-linear-gradient(
45deg,
#1976d2,
#1976d2 10px,
#1565c0 10px,
#1565c0 20px
);
}
</style>

View File

@@ -1,4 +1,5 @@
using System.Net;
using Manager.App.Models.Library;
using Manager.YouTube;
using Microsoft.AspNetCore.Components;
using MudBlazor;
@@ -9,27 +10,79 @@ namespace Manager.App.Components.Dialogs
{
[CascadingParameter] private IMudDialogInstance? MudDialog { get; set; }
[Parameter] public string DefaultUserAgent { get; set; } = "";
public YouTubeClient Client { get; set; } = new();
private bool IsAnonymous { get; set; }
private ClientPrep? PreparingClient { get; set; }
private CookieCollection ImportCookies { get; set; } = [];
private bool _isLoading;
private AccountImportSteps _steps = AccountImportSteps.Authenticate;
private bool _cookieTextValid;
private bool _cookieImportTextValid;
private string _cookieText = "";
private string _cookieDomain = ".youtube.com";
protected override void OnInitialized()
{
Client.UserAgent = DefaultUserAgent;
base.OnInitialized();
}
private bool CanSave()
{
if (IsAnonymous || PreparingClient?.YouTubeClient?.State?.LoggedIn == true)
{
return true;
}
return false;
}
private bool CanContinue()
{
switch (_steps)
{
case AccountImportSteps.Authenticate:
if (IsAnonymous || ImportCookies.Count != 0)
{
return true;
}
break;
case AccountImportSteps.Validate:
if (IsAnonymous || PreparingClient?.YouTubeClient?.State?.LoggedIn == true)
{
return true;
}
break;
}
return false;
}
private async Task OnNextStep()
{
switch (_steps)
{
case AccountImportSteps.Authenticate:
if (CanContinue())
{
_steps = AccountImportSteps.Validate;
await BuildClient();
await InvokeAsync(StateHasChanged);
return;
}
SnackbarService.Add("Cannot continue!", Severity.Warning);
break;
case AccountImportSteps.Validate:
if (CanSave())
{
MudDialog?.Close(DialogResult.Ok(PreparingClient));
await InvokeAsync(StateHasChanged);
return;
}
SnackbarService.Add("Cannot save!", Severity.Warning);
break;
}
await InvokeAsync(StateHasChanged);
}
private void ParseCookies()
{
try
{
var cookies = ParseCookieHeader(_cookieText, _cookieDomain);
Client.CookieContainer.Add(cookies);
ImportCookies.Clear();
ImportCookies.Add(ParseCookieHeader(_cookieText, _cookieDomain));
_cookieText = string.Empty;
}
catch (Exception e)
@@ -39,6 +92,16 @@ namespace Manager.App.Components.Dialogs
StateHasChanged();
}
private void ClearPreparedClient()
{
PreparingClient?.YouTubeClient?.Dispose();
PreparingClient = null;
IsAnonymous = false;
ImportCookies.Clear();
_steps = AccountImportSteps.Authenticate;
StateHasChanged();
}
private static string? ValidateCookieText(string text)
{
if (string.IsNullOrWhiteSpace(text))
@@ -82,46 +145,41 @@ namespace Manager.App.Components.Dialogs
return collection;
}
private bool CanValidate()
{
if (string.IsNullOrWhiteSpace(Client.UserAgent) || Client.CookieContainer.Count <= 0)
{
return false;
}
return true;
}
private bool CanSave()
{
if (Client.External.State == null)
{
return false;
}
if (string.IsNullOrWhiteSpace(Client.Id))
{
return false;
}
return Client.SapisidCookie != null && Client.External.State.LoggedIn;
}
private async Task ValidateAccount()
private async Task BuildClient()
{
_isLoading = true;
var result = await Client.BuildClientAsync();
if (!result.IsSuccess)
PreparingClient = new ClientPrep();
if (IsAnonymous)
{
SnackbarService.Add(result.Error?.Description ?? "Error validating account.", Severity.Error);
ImportCookies.Clear();
}
var clientResult = await YouTubeClient.CreateAsync(ImportCookies, DefaultUserAgent);
if (clientResult.IsSuccess)
{
PreparingClient.YouTubeClient = clientResult.Value;
}
if (PreparingClient.YouTubeClient == null)
{
SnackbarService.Add("Failed to get client!", Severity.Error);
_isLoading = false;
return;
}
var accountResult = await PreparingClient.YouTubeClient.GetChannelByIdAsync(PreparingClient.YouTubeClient.Id);
if (accountResult.IsSuccess)
{
PreparingClient.Channel = accountResult.Value;
}
_isLoading = false;
}
private void OnSave()
{
MudDialog?.Close(DialogResult.Ok(Client));
await InvokeAsync(StateHasChanged);
}
}
public enum AccountImportSteps
{
Authenticate,
Validate
}
}

View File

@@ -1,5 +1,6 @@
@page "/Channels"
@using Manager.App.Models.Settings
@using Manager.App.Services.System
@using Microsoft.Extensions.Options
@inject ILibraryService LibraryService

View File

@@ -1,4 +1,5 @@
using Manager.App.Components.Dialogs;
using Manager.App.Models.Library;
using Manager.Data.Entities.LibraryContext;
using Manager.YouTube;
using Microsoft.AspNetCore.Components;
@@ -28,6 +29,20 @@ public partial class Channels : ComponentBase
return;
}
var client = (YouTubeClient)result.Data;
var client = (ClientPrep)result.Data;
if (client == null)
{
return;
}
/*var savedResult = await ClientManager.SaveClientAsync(client);
if (!savedResult.IsSuccess)
{
Snackbar.Add($"Failed to store client: {savedResult.Error?.Description ?? "Unknown!"}", Severity.Error);
}
else
{
Snackbar.Add($"Client {client.External.Channel?.Handle ?? client.Id} saved!", Severity.Success);
}*/
}
}

View File

@@ -3,6 +3,7 @@ using DotBased.Logging.MEL;
using DotBased.Logging.Serilog;
using Manager.App.Models.Settings;
using Manager.App.Services;
using Manager.App.Services.System;
using Manager.Data.Contexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
@@ -25,6 +26,8 @@ public static class DependencyInjection
});
builder.Services.AddScoped<ILibraryService, LibraryService>();
/*builder.Services.AddHostedService<ClientManager>();*/
}
public static void SetupSettings(this WebApplicationBuilder builder)

View File

@@ -0,0 +1,10 @@
using Manager.YouTube;
using Manager.YouTube.Models.Innertube;
namespace Manager.App.Models.Library;
public class ClientPrep
{
public YouTubeClient? YouTubeClient { get; set; }
public Channel? Channel { get; set; }
}

View File

@@ -7,6 +7,8 @@ namespace Manager.App.Services;
public interface ILibraryService
{
public Task<Result<ChannelEntity>> GetChannelByIdAsync(string id, CancellationToken cancellationToken = default);
public Task<Result> SaveChannelAsync(ChannelEntity channel, CancellationToken cancellationToken = default);
public Task<Result<LibraryInformation>> GetLibraryInfoAsync(CancellationToken cancellationToken = default);
public Task<ListResult<ChannelEntity>> GetChannelAccountsAsync(int total = 20, int offset = 0, CancellationToken cancellationToken = default);

View File

@@ -29,6 +29,53 @@ public class LibraryService : ILibraryService
Directory.CreateDirectory(Path.Combine(_librarySettings.Path, SubDirChannels));
}
public async Task<Result<ChannelEntity>> GetChannelByIdAsync(string id, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(id))
{
return ResultError.Fail("Channel id cannot be null or empty!");
}
try
{
await using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
var channel = await context.Channels.Include(c => c.ClientAccount).FirstOrDefaultAsync(c => c.Id == id, cancellationToken);
if (channel == null)
{
return ResultError.Fail("Channel not found!");
}
return channel;
}
catch (Exception e)
{
return HandleException(e);
}
}
public async Task<Result> SaveChannelAsync(ChannelEntity channel, CancellationToken cancellationToken = default)
{
try
{
await using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
if (context.Channels.Any(c => c.Id == channel.Id))
{
context.Channels.Update(channel);
}
else
{
context.Channels.Add(channel);
}
var changed = await context.SaveChangesAsync(cancellationToken);
return changed <= 0 ? Result.Success() : ResultError.Fail("Failed to save channel!");
}
catch (Exception e)
{
return ResultError.Error(e);
}
}
public async Task<Result<LibraryInformation>> GetLibraryInfoAsync(CancellationToken cancellationToken = default)
{
try
@@ -57,7 +104,7 @@ public class LibraryService : ILibraryService
{
await using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
var orderedAccounts = context.Channels.Include(x => x.ClientAccount).Where(x => x.ClientAccount != null).OrderBy(x => x.Id);
return new ListResultReturn<ChannelEntity>(orderedAccounts.Skip(offset).Take(total).ToList(), orderedAccounts.Count());
return new ListResultReturn<ChannelEntity>(orderedAccounts.Skip(offset).Take(total).ToList(),orderedAccounts.Count());
}
catch (Exception e)
{

View File

@@ -1,4 +1,7 @@
using System.Net;
using DotBased.Monads;
using Manager.App.Models.Library;
using Manager.Data.Entities.LibraryContext;
using Manager.YouTube;
namespace Manager.App.Services.System;
@@ -19,17 +22,118 @@ public class ClientManager : BackgroundService
// Clear up
}
public async Task<Result> SaveClientAsync(YouTubeClient client)
public async Task<Result<ClientPrep>> PrepareClient()
{
return ResultError.Fail("Not implemented");
return ResultError.Fail("Not implemented!");
}
/*public async Task<Result> SaveClientAsync(YouTubeClient client, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(client.Id))
{
return ResultError.Fail("Client does not have an ID, cannot save to library database!");
}
var channelResult = await libraryService.GetChannelByIdAsync(client.Id, cancellationToken);
ChannelEntity? channel;
if (channelResult.IsSuccess)
{
channel = channelResult.Value;
UpdateChannelEntity(channel, client);
}
else
{
channel = CreateNewChannelFromClient(client);
}
var saveResult = await libraryService.SaveChannelAsync(channel, cancellationToken);
return saveResult;
}*/
/*private void UpdateChannelEntity(ChannelEntity channel, YouTubeClient client)
{
channel.Name = client.External.Channel?.ChannelName;
channel.Handle = client.External.Channel?.Handle;
channel.Description = client.External.Channel?.Description;
var clientAcc = channel.ClientAccount;
if (clientAcc != null)
{
clientAcc.UserAgent = clientAcc.UserAgent;
var currentCookies = client.CookieContainer.GetAllCookies();
foreach (var cookieEntity in clientAcc.HttpCookies.ToList())
{
var cookie = currentCookies[cookieEntity.Name];
if (cookie == null)
{
clientAcc.HttpCookies.Remove(cookieEntity);
continue;
}
if (!cookie.Domain.Equals(cookieEntity.Domain, StringComparison.InvariantCultureIgnoreCase))
{
continue;
}
cookieEntity.Value = cookie.Value;
cookieEntity.Path = cookie.Path;
cookieEntity.Secure = cookie.Secure;
cookieEntity.HttpOnly = cookie.HttpOnly;
cookieEntity.ExpiresUtc = cookie.Expires == DateTime.MinValue ? null : cookie.Expires;
}
}
}
private ChannelEntity CreateNewChannelFromClient(YouTubeClient client)
{
var cookies = new List<HttpCookieEntity>();
foreach (var cookieObj in client.CookieContainer.GetAllCookies())
{
if (cookieObj is not Cookie cookie)
{
continue;
}
var cookieEntity = new HttpCookieEntity
{
ClientId = client.Id,
Name = cookie.Name,
Value = cookie.Value,
Domain = cookie.Domain,
Path = cookie.Path,
Secure = cookie.Secure,
HttpOnly = cookie.HttpOnly,
ExpiresUtc = cookie.Expires == DateTime.MinValue ? null : cookie.Expires
};
cookies.Add(cookieEntity);
}
var clientAcc = new ClientAccountEntity
{
Id = client.Id,
UserAgent = client.UserAgent,
HttpCookies = cookies
};
var channel = new ChannelEntity
{
Id = client.Id,
Name = client.External.Channel?.ChannelName,
Handle = client.External.Channel?.Handle,
Description = client.External.Channel?.Description,
ClientAccount = clientAcc
};
return channel;
}*/
public async Task<Result<YouTubeClient>> LoadClientByIdAsync(string id)
{
if (string.IsNullOrWhiteSpace(id))
{
return ResultError.Fail("Client ID is empty!");
}
return ResultError.Fail("Not implemented");
}