From 5250b9f3f977e114f21df1df09c225fcd4b65c09 Mon Sep 17 00:00:00 2001 From: max Date: Thu, 18 Sep 2025 02:01:45 +0200 Subject: [PATCH] [REWORK] Changes saving client and channel info --- .../Components/Pages/Channels.razor.cs | 35 +++- Manager.App/Services/ILibraryService.cs | 6 +- Manager.App/Services/LibraryService.cs | 96 ++++++++- Manager.App/Services/System/ClientService.cs | 194 +++++++----------- Manager.Data/Contexts/LibraryDbContext.cs | 2 +- .../LibraryContext/HttpCookieEntity.cs | 2 +- .../{Channel.cs => InnertubeChannel.cs} | 2 +- .../Parsers/Json/ChannelJsonParser.cs | 4 +- Manager.YouTube/YouTubeClient.cs | 2 +- Manager.YouTube/YouTubeClientCollection.cs | 11 + 10 files changed, 201 insertions(+), 153 deletions(-) rename Manager.YouTube/Models/Innertube/{Channel.cs => InnertubeChannel.cs} (94%) create mode 100644 Manager.YouTube/YouTubeClientCollection.cs diff --git a/Manager.App/Components/Pages/Channels.razor.cs b/Manager.App/Components/Pages/Channels.razor.cs index 36126f0..968e75d 100644 --- a/Manager.App/Components/Pages/Channels.razor.cs +++ b/Manager.App/Components/Pages/Channels.razor.cs @@ -29,24 +29,39 @@ public partial class Channels : ComponentBase return; } - var clientPrep = (ClientChannel)result.Data; - if (clientPrep?.YouTubeClient == null) + var clientChannel = (ClientChannel)result.Data; + if (clientChannel?.YouTubeClient == null) { + Snackbar.Add("No YouTube client received.", Severity.Error); return; } - - var savedResult = await ClientService.SaveClientAsync(clientPrep.YouTubeClient, clientPrep.Channel); - if (!savedResult.IsSuccess) - { - Snackbar.Add($"Failed to store client: {savedResult.Error?.Description ?? "Unknown!"}", Severity.Error); - } - else + + var savedClientResult = await ClientService.SaveClientAsync(clientChannel.YouTubeClient); + if (savedClientResult.IsSuccess) { if (_table != null) { await _table.ReloadServerData(); } - Snackbar.Add($"Client {clientPrep.Channel?.Handle ?? clientPrep.YouTubeClient.Id} saved!", Severity.Success); + Snackbar.Add($"Client {clientChannel.Channel?.Handle ?? clientChannel.YouTubeClient.Id} saved!", Severity.Success); + ClientService.AddClient(clientChannel.YouTubeClient); + } + else + { + Snackbar.Add($"Failed to store client: {savedClientResult.Error?.Description ?? "Unknown!"}", Severity.Error); + } + + if (clientChannel.Channel == null) + { + Snackbar.Add("No channel information received!", Severity.Warning); + } + else + { + var saveChannelResult = await LibraryService.SaveChannelAsync(clientChannel.Channel); + if (!saveChannelResult.IsSuccess) + { + Snackbar.Add("Failed to save channel information", Severity.Warning); + } } } } \ No newline at end of file diff --git a/Manager.App/Services/ILibraryService.cs b/Manager.App/Services/ILibraryService.cs index ee4f5ff..2e26de2 100644 --- a/Manager.App/Services/ILibraryService.cs +++ b/Manager.App/Services/ILibraryService.cs @@ -8,9 +8,11 @@ namespace Manager.App.Services; public interface ILibraryService { - public Task FetchChannelImagesAsync(Channel channel); + public Task FetchChannelImagesAsync(InnertubeChannel innertubeChannel); + public Task SaveClientAsync(ClientAccountEntity client, CancellationToken cancellationToken = default); public Task> GetChannelByIdAsync(string id, CancellationToken cancellationToken = default); - public Task SaveChannelAsync(ChannelEntity channel, CancellationToken cancellationToken = default); + + public Task SaveChannelAsync(InnertubeChannel innertubeChannel, CancellationToken cancellationToken = default); public Task> GetLibraryInfoAsync(CancellationToken cancellationToken = default); public Task> GetChannelsAsync(int total = 20, int offset = 0, CancellationToken cancellationToken = default); diff --git a/Manager.App/Services/LibraryService.cs b/Manager.App/Services/LibraryService.cs index 75f69f8..b47065e 100644 --- a/Manager.App/Services/LibraryService.cs +++ b/Manager.App/Services/LibraryService.cs @@ -32,14 +32,14 @@ public class LibraryService : ILibraryService Directory.CreateDirectory(Path.Combine(_librarySettings.Path, LibraryConstants.Directories.SubDirChannels)); } - public async Task FetchChannelImagesAsync(Channel channel) + public async Task FetchChannelImagesAsync(InnertubeChannel innertubeChannel) { try { await using var context = await _dbContextFactory.CreateDbContextAsync(); - await AddWebImagesAsync(context, channel.AvatarImages, channel.Id, "avatars", LibraryConstants.FileTypes.ChannelAvatar, LibraryConstants.Directories.SubDirChannels); - await AddWebImagesAsync(context, channel.BannerImages, channel.Id, "banners", LibraryConstants.FileTypes.ChannelBanner, LibraryConstants.Directories.SubDirChannels); + await AddWebImagesAsync(context, innertubeChannel.AvatarImages, innertubeChannel.Id, "avatars", LibraryConstants.FileTypes.ChannelAvatar, LibraryConstants.Directories.SubDirChannels); + await AddWebImagesAsync(context, innertubeChannel.BannerImages, innertubeChannel.Id, "banners", LibraryConstants.FileTypes.ChannelBanner, LibraryConstants.Directories.SubDirChannels); if (!context.ChangeTracker.HasChanges()) { @@ -101,6 +101,43 @@ public class LibraryService : ILibraryService } } + public async Task SaveClientAsync(ClientAccountEntity client, CancellationToken cancellationToken = default) + { + try + { + await using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + var updateEntity = false; + var dbClient = context.ClientAccounts.FirstOrDefault(c => c.Id == client.Id); + if (dbClient == null) + { + dbClient = client; + } + else + { + updateEntity = true; + dbClient.HttpCookies = client.HttpCookies; + dbClient.UserAgent = client.UserAgent; + } + + if (updateEntity) + { + context.ClientAccounts.Update(dbClient); + } + else + { + context.ClientAccounts.Add(dbClient); + } + + var savedResult= await context.SaveChangesAsync(cancellationToken); + return savedResult <= 0 ? ResultError.Fail("Could not save changes!") : Result.Success(); + } + catch (Exception e) + { + return ResultError.Error(e); + } + } + public async Task> GetChannelByIdAsync(string id, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(id)) @@ -111,7 +148,11 @@ public class LibraryService : ILibraryService try { await using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - var channel = await context.Channels.Include(c => c.ClientAccount).FirstOrDefaultAsync(c => c.Id == id, cancellationToken); + var channel = await context.Channels + .Include(c => c.ClientAccount) + .ThenInclude(p => p!.HttpCookies) + .FirstOrDefaultAsync(c => c.Id == id, cancellationToken); + if (channel == null) { return ResultError.Fail("Channel not found!"); @@ -125,18 +166,53 @@ public class LibraryService : ILibraryService } } - public async Task SaveChannelAsync(ChannelEntity channel, CancellationToken cancellationToken = default) + public async Task SaveChannelAsync(InnertubeChannel innertubeChannel, CancellationToken cancellationToken = default) { try { - await using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - if (context.Channels.Any(c => c.Id == channel.Id)) + var imagesResult = await FetchChannelImagesAsync(innertubeChannel); + if (!imagesResult.IsSuccess) { - context.Channels.Update(channel); + return ResultError.Fail("Failed to fetch channel images!"); + } + + await using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + var channelResult = await GetChannelByIdAsync(innertubeChannel.Id, cancellationToken); + + ChannelEntity? channelEntity; + try + { + if (channelResult.IsSuccess) + { + channelEntity = channelResult.Value; + channelEntity.Name = innertubeChannel.ChannelName; + channelEntity.Handle = innertubeChannel.Handle; + channelEntity.Description = innertubeChannel.Description; + } + else + { + channelEntity = new ChannelEntity + { + Id = innertubeChannel.Id, + Name = innertubeChannel.ChannelName, + Handle = innertubeChannel.Handle, + Description = innertubeChannel.Description + }; + } + } + catch (Exception e) + { + return ResultError.Error(e); + } + + if (context.Channels.Any(c => c.Id == innertubeChannel.Id)) + { + context.Channels.Update(channelEntity); } else { - context.Channels.Add(channel); + context.Channels.Add(channelEntity); } var changed = await context.SaveChangesAsync(cancellationToken); @@ -144,7 +220,7 @@ public class LibraryService : ILibraryService } catch (Exception e) { - return ResultError.Error(e); + return HandleException(e); } } diff --git a/Manager.App/Services/System/ClientService.cs b/Manager.App/Services/System/ClientService.cs index 24aec88..4129087 100644 --- a/Manager.App/Services/System/ClientService.cs +++ b/Manager.App/Services/System/ClientService.cs @@ -1,23 +1,19 @@ using System.Net; using DotBased.Logging; using DotBased.Monads; -using Manager.App.Models.Library; using Manager.Data.Entities.LibraryContext; using Manager.YouTube; -using Manager.YouTube.Models.Innertube; namespace Manager.App.Services.System; public class ClientService(IServiceScopeFactory scopeFactory, ILogger logger) : ExtendedBackgroundService(nameof(ClientService), "Managing YouTube clients", logger, TimeSpan.FromMinutes(10)) { - private readonly List _loadedClients = []; - private CancellationToken _cancellationToken; + private readonly YouTubeClientCollection _loadedClients = []; private ILibraryService? _libraryService; protected override Task InitializeAsync(CancellationToken stoppingToken) { - _cancellationToken = stoppingToken; stoppingToken.Register(CancellationRequested); using var scope = scopeFactory.CreateScope(); _libraryService = scope.ServiceProvider.GetRequiredService(); @@ -25,17 +21,79 @@ public class ClientService(IServiceScopeFactory scopeFactory, ILogger SaveClientAsync(YouTubeClient client, Channel? channelInfo = null, CancellationToken cancellationToken = default) + public async Task AddClientByIdAsync(string id, CancellationToken stoppingToken = default) + { + if (_libraryService == null) + { + return ResultError.Fail("Library service is not initialized!."); + } + + var clientResult = await _libraryService.GetChannelByIdAsync(id, stoppingToken); + if (!clientResult.IsSuccess) + { + return clientResult; + } + + var clientAcc = clientResult.Value.ClientAccount; + if (clientAcc == null) + { + return ResultError.Fail("Client account is not initialized!."); + } + + var cookieCollection = new CookieCollection(); + foreach (var httpCookie in clientAcc.HttpCookies) + { + var cookie = new Cookie + { + Name = httpCookie.Name, + Value = httpCookie.Value, + Domain = httpCookie.Domain, + Path = httpCookie.Path, + Secure = httpCookie.Secure, + HttpOnly = httpCookie.HttpOnly, + Expires = httpCookie.ExpiresUtc ?? DateTime.MinValue + }; + cookieCollection.Add(cookie); + } + var ytClientResult = await YouTubeClient.CreateAsync(cookieCollection, clientAcc.UserAgent ?? ""); + if (!ytClientResult.IsSuccess) + { + return ytClientResult; + } + + AddClient(ytClientResult.Value); + return Result.Success(); + } + + public void AddClient(YouTubeClient client) + { + if (_loadedClients.Contains(client)) + { + return; + } + + _loadedClients.Add(client); + } + + public async Task SaveClientAsync(YouTubeClient client, CancellationToken cancellationToken = default) { if (_libraryService == null) { @@ -48,121 +106,7 @@ public class ClientService(IServiceScopeFactory scopeFactory, ILogger(); - 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 = channelInfo.Id, - Name = channelInfo.ChannelName, - Handle = channelInfo.Handle, - Description = channelInfo.Description, - ClientAccount = clientAcc - }; - return channel; - } } \ No newline at end of file diff --git a/Manager.Data/Contexts/LibraryDbContext.cs b/Manager.Data/Contexts/LibraryDbContext.cs index 4c18984..c642195 100644 --- a/Manager.Data/Contexts/LibraryDbContext.cs +++ b/Manager.Data/Contexts/LibraryDbContext.cs @@ -18,7 +18,7 @@ public sealed class LibraryDbContext : DbContext public DbSet Captions { get; set; } public DbSet Channels { get; set; } - public DbSet Accounts { get; set; } + public DbSet ClientAccounts { get; set; } public DbSet HttpCookies { get; set; } public DbSet Media { get; set; } public DbSet MediaFormats { get; set; } diff --git a/Manager.Data/Entities/LibraryContext/HttpCookieEntity.cs b/Manager.Data/Entities/LibraryContext/HttpCookieEntity.cs index 417dcf0..fcaf716 100644 --- a/Manager.Data/Entities/LibraryContext/HttpCookieEntity.cs +++ b/Manager.Data/Entities/LibraryContext/HttpCookieEntity.cs @@ -16,7 +16,7 @@ public class HttpCookieEntity : DateTimeBase public string? Domain { get; set; } [MaxLength(DataConstants.DbContext.DefaultDbStringSize)] public string? Path { get; set; } - public DateTimeOffset? ExpiresUtc { get; set; } + public DateTime? ExpiresUtc { get; set; } public bool Secure { get; set; } public bool HttpOnly { get; set; } } diff --git a/Manager.YouTube/Models/Innertube/Channel.cs b/Manager.YouTube/Models/Innertube/InnertubeChannel.cs similarity index 94% rename from Manager.YouTube/Models/Innertube/Channel.cs rename to Manager.YouTube/Models/Innertube/InnertubeChannel.cs index ceac828..b6f7301 100644 --- a/Manager.YouTube/Models/Innertube/Channel.cs +++ b/Manager.YouTube/Models/Innertube/InnertubeChannel.cs @@ -1,6 +1,6 @@ namespace Manager.YouTube.Models.Innertube; -public class Channel +public class InnertubeChannel { public required string Id { get; set; } public bool NoIndex { get; set; } diff --git a/Manager.YouTube/Parsers/Json/ChannelJsonParser.cs b/Manager.YouTube/Parsers/Json/ChannelJsonParser.cs index 6e11ab5..704588a 100644 --- a/Manager.YouTube/Parsers/Json/ChannelJsonParser.cs +++ b/Manager.YouTube/Parsers/Json/ChannelJsonParser.cs @@ -6,7 +6,7 @@ namespace Manager.YouTube.Parsers.Json; public static class ChannelJsonParser { - public static Result ParseJsonToChannelData(string json) + public static Result ParseJsonToChannelData(string json) { try { @@ -23,7 +23,7 @@ public static class ChannelJsonParser throw new InvalidOperationException("No channel id found."); } - var channel = new Channel + var channel = new InnertubeChannel { Id = channelId, ChannelName = channelMetadata.GetProperty("title").ToString(), diff --git a/Manager.YouTube/YouTubeClient.cs b/Manager.YouTube/YouTubeClient.cs index daaf66b..e16f28a 100644 --- a/Manager.YouTube/YouTubeClient.cs +++ b/Manager.YouTube/YouTubeClient.cs @@ -148,7 +148,7 @@ public sealed class YouTubeClient : IDisposable return Result.Success(); } - public async Task> GetChannelByIdAsync(string channelId) + public async Task> GetChannelByIdAsync(string channelId) { if (State == null) { diff --git a/Manager.YouTube/YouTubeClientCollection.cs b/Manager.YouTube/YouTubeClientCollection.cs new file mode 100644 index 0000000..771caae --- /dev/null +++ b/Manager.YouTube/YouTubeClientCollection.cs @@ -0,0 +1,11 @@ +using System.Collections.ObjectModel; + +namespace Manager.YouTube; + +public class YouTubeClientCollection : KeyedCollection +{ + protected override string GetKeyForItem(YouTubeClient item) + { + return item.Id; + } +} \ No newline at end of file