diff --git a/Manager.App/Components/Application/Dev/AuthenticationHasher.razor.cs b/Manager.App/Components/Application/Dev/AuthenticationHasher.razor.cs index 2ef14fb..cd39822 100644 --- a/Manager.App/Components/Application/Dev/AuthenticationHasher.razor.cs +++ b/Manager.App/Components/Application/Dev/AuthenticationHasher.razor.cs @@ -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); } } \ No newline at end of file diff --git a/Manager.App/Components/Dialogs/AccountDialog.razor b/Manager.App/Components/Dialogs/AccountDialog.razor index f75e676..dbabe9b 100644 --- a/Manager.App/Components/Dialogs/AccountDialog.razor +++ b/Manager.App/Components/Dialogs/AccountDialog.razor @@ -2,112 +2,195 @@ +@{ + var client = PreparingClient?.YouTubeClient; + var clientState = client?.State; + var channel = PreparingClient?.Channel; + var avatar = channel?.AvatarImages.FirstOrDefault(); + var banner = channel?.BannerImages.FirstOrDefault(); +} Add new account - - + @switch (_steps) + { + case AccountImportSteps.Authenticate: - - - - - - Account id: - @Client.Id - - - Account name: - @Client.External.Channel?.ChannelName - - - Account handle: - @Client.External.Channel?.Handle - - - Logged in: - @Client.External.State?.LoggedIn - - - YouTube Premium: - @Client.External.State?.IsPremiumUser - - - - User agent: - @Client.UserAgent - - - InnerTube client: - @Client.External.State?.InnerTubeClient - - - InnerTube client version: - @Client.External.State?.InnerTubeClientVersion - - - InnerTube API key: - @Client.External.State?.InnertubeApiKey - - - Language: - @Client.External.State?.InnerTubeContext?.InnerTubeClient?.HLanguage - - - - @{ - var avatar = Client.External.Channel?.AvatarImages.FirstOrDefault(); - } - @if (avatar != null) - { - - } - + + Anonymous client + + - - Import cookies - - - - Load - - + + + Import cookies + @($"{ImportCookies.Count} cookie(s) imported") + + + + Import + + + - -
- - Cookies + +
+ + Cookies + +
+ + + + + + + + + + + + + + + + + +
-
- - - - - - - - - - - - - - - - - - -
-
+
+ break; + case AccountImportSteps.Validate: + + + @if (banner != null) + { + + } + else + { + + } + + @if (avatar != null) + { + + } + else + { +
+ } + + @(channel?.ChannelName ?? client?.Id) + @(string.IsNullOrWhiteSpace(channel?.Description) ? "No description!" : channel.Description) + +
+
+ + + + + Account id: + @client?.Id + + + Account name: + @channel?.ChannelName + + + Account handle: + @channel?.Handle + + + Logged in: + @clientState?.LoggedIn + + + YouTube Premium: + @clientState?.IsPremiumUser + + + + User agent: + @client?.UserAgent + + + InnerTube client: + @clientState?.InnerTubeClient + + + InnerTube client version: + @clientState?.InnerTubeClientVersion + + + InnerTube API key: + @clientState?.InnertubeApiKey + + + Language: + @clientState?.InnerTubeContext?.InnerTubeClient?.HLanguage + + + +
+ break; + }
- Cancel - Validate - Save + Cancel + + Reset + @(_steps == AccountImportSteps.Validate ? "Save" : "Next") -
\ No newline at end of file + + + \ No newline at end of file diff --git a/Manager.App/Components/Dialogs/AccountDialog.razor.cs b/Manager.App/Components/Dialogs/AccountDialog.razor.cs index 363f816..f2809a8 100644 --- a/Manager.App/Components/Dialogs/AccountDialog.razor.cs +++ b/Manager.App/Components/Dialogs/AccountDialog.razor.cs @@ -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 + } } \ No newline at end of file diff --git a/Manager.App/Components/Pages/Channels.razor b/Manager.App/Components/Pages/Channels.razor index f508973..80bbd3e 100644 --- a/Manager.App/Components/Pages/Channels.razor +++ b/Manager.App/Components/Pages/Channels.razor @@ -1,5 +1,6 @@ @page "/Channels" @using Manager.App.Models.Settings +@using Manager.App.Services.System @using Microsoft.Extensions.Options @inject ILibraryService LibraryService diff --git a/Manager.App/Components/Pages/Channels.razor.cs b/Manager.App/Components/Pages/Channels.razor.cs index e31e2f6..fe84519 100644 --- a/Manager.App/Components/Pages/Channels.razor.cs +++ b/Manager.App/Components/Pages/Channels.razor.cs @@ -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); + }*/ } } \ No newline at end of file diff --git a/Manager.App/DependencyInjection.cs b/Manager.App/DependencyInjection.cs index fbe9961..0fae928 100644 --- a/Manager.App/DependencyInjection.cs +++ b/Manager.App/DependencyInjection.cs @@ -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(); + + /*builder.Services.AddHostedService();*/ } public static void SetupSettings(this WebApplicationBuilder builder) diff --git a/Manager.App/Models/Library/ClientPrep.cs b/Manager.App/Models/Library/ClientPrep.cs new file mode 100644 index 0000000..6a965e4 --- /dev/null +++ b/Manager.App/Models/Library/ClientPrep.cs @@ -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; } +} \ No newline at end of file diff --git a/Manager.App/Services/ILibraryService.cs b/Manager.App/Services/ILibraryService.cs index 5ef840a..b41a4fe 100644 --- a/Manager.App/Services/ILibraryService.cs +++ b/Manager.App/Services/ILibraryService.cs @@ -7,6 +7,8 @@ namespace Manager.App.Services; public interface ILibraryService { + public Task> GetChannelByIdAsync(string id, CancellationToken cancellationToken = default); + public Task SaveChannelAsync(ChannelEntity channel, CancellationToken cancellationToken = default); public Task> GetLibraryInfoAsync(CancellationToken cancellationToken = default); public Task> GetChannelAccountsAsync(int total = 20, int offset = 0, CancellationToken cancellationToken = default); diff --git a/Manager.App/Services/LibraryService.cs b/Manager.App/Services/LibraryService.cs index 8c8dce0..6b4e9d5 100644 --- a/Manager.App/Services/LibraryService.cs +++ b/Manager.App/Services/LibraryService.cs @@ -29,6 +29,53 @@ public class LibraryService : ILibraryService Directory.CreateDirectory(Path.Combine(_librarySettings.Path, SubDirChannels)); } + public async Task> 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 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> 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(orderedAccounts.Skip(offset).Take(total).ToList(), orderedAccounts.Count()); + return new ListResultReturn(orderedAccounts.Skip(offset).Take(total).ToList(),orderedAccounts.Count()); } catch (Exception e) { diff --git a/Manager.App/Services/System/ClientManager.cs b/Manager.App/Services/System/ClientManager.cs index 91a7cc8..2171a67 100644 --- a/Manager.App/Services/System/ClientManager.cs +++ b/Manager.App/Services/System/ClientManager.cs @@ -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 SaveClientAsync(YouTubeClient client) + public async Task> PrepareClient() { - return ResultError.Fail("Not implemented"); + + return ResultError.Fail("Not implemented!"); } - + + /*public async Task 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(); + 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> LoadClientByIdAsync(string id) { if (string.IsNullOrWhiteSpace(id)) { return ResultError.Fail("Client ID is empty!"); } + return ResultError.Fail("Not implemented"); } diff --git a/Manager.Data/Entities/LibraryContext/ChannelEntity.cs b/Manager.Data/Entities/LibraryContext/ChannelEntity.cs index b437cb8..555358e 100644 --- a/Manager.Data/Entities/LibraryContext/ChannelEntity.cs +++ b/Manager.Data/Entities/LibraryContext/ChannelEntity.cs @@ -12,10 +12,6 @@ public class ChannelEntity : DateTimeBase public string? Handle { get; set; } [MaxLength(DataConstants.DbContext.DefaultDbDescriptionStringSize)] public string? Description { get; set; } - public DateTime JoinedDate { get; set; } - public long Subscribers { get; set; } - public long TotalVideos { get; set; } - public long TotalViews { get; set; } public List Media { get; set; } = []; public List Playlists { get; set; } = []; public ClientAccountEntity? ClientAccount { get; set; } diff --git a/Manager.Data/Entities/LibraryContext/HttpCookieEntity.cs b/Manager.Data/Entities/LibraryContext/HttpCookieEntity.cs index af3c4a6..c995e4c 100644 --- a/Manager.Data/Entities/LibraryContext/HttpCookieEntity.cs +++ b/Manager.Data/Entities/LibraryContext/HttpCookieEntity.cs @@ -16,7 +16,5 @@ public class HttpCookieEntity : DateTimeBase public bool Secure { get; set; } public bool HttpOnly { get; set; } [MaxLength(DataConstants.DbContext.DefaultDbStringSize)] - public string? SameSite { get; set; } - [MaxLength(DataConstants.DbContext.DefaultDbStringSize)] public required string ClientId { get; set; } } diff --git a/Manager.Data/Migrations/20250902141251_InitialLibrary.Designer.cs b/Manager.Data/Migrations/20250902141251_InitialLibrary.Designer.cs deleted file mode 100644 index b850f99..0000000 --- a/Manager.Data/Migrations/20250902141251_InitialLibrary.Designer.cs +++ /dev/null @@ -1,424 +0,0 @@ -// -using System; -using Manager.Data.Contexts; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Manager.Data.Migrations -{ - [DbContext(typeof(LibraryDbContext))] - [Migration("20250902141251_InitialLibrary")] - partial class InitialLibrary - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.19"); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.CaptionEntity", b => - { - b.Property("MediaId") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("LanguageCode") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("CreatedAtUtc") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.HasKey("MediaId", "LanguageCode"); - - b.ToTable("captions", (string)null); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.ChannelEntity", b => - { - b.Property("Id") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("CreatedAtUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasMaxLength(500) - .HasColumnType("TEXT"); - - b.Property("JoinedDate") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("Subscribers") - .HasColumnType("INTEGER"); - - b.Property("TotalVideos") - .HasColumnType("INTEGER"); - - b.Property("TotalViews") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("channels", (string)null); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.ClientAccountEntity", b => - { - b.Property("Id") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("CreatedAtUtc") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("UserAgent") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("client_accounts", (string)null); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.HttpCookieEntity", b => - { - b.Property("Name") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("ClientId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("CreatedAtUtc") - .HasColumnType("TEXT"); - - b.Property("Domain") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("ExpiresUtc") - .HasColumnType("TEXT"); - - b.Property("HttpOnly") - .HasColumnType("INTEGER"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Path") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("SameSite") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("Secure") - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.HasKey("Name"); - - b.HasIndex("ClientId"); - - b.ToTable("http_cookies", (string)null); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.Join.PlaylistMedia", b => - { - b.Property("PlaylistId") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("MediaId") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("CreatedAtUtc") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("PlaylistId", "MediaId"); - - b.HasIndex("MediaId"); - - b.ToTable("join_playlist_media", (string)null); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.MediaEntity", b => - { - b.Property("Id") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("ChannelId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("CreatedAtUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasMaxLength(500) - .HasColumnType("TEXT"); - - b.Property("ExternalState") - .HasColumnType("INTEGER"); - - b.Property("IsDownloaded") - .HasColumnType("INTEGER"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("State") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("UploadDateUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ChannelId"); - - b.ToTable("media", (string)null); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.MediaFormatEntity", b => - { - b.Property("MediaId") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("Itag") - .HasColumnType("INTEGER"); - - b.Property("ApproxDurationMs") - .HasColumnType("INTEGER"); - - b.Property("AudioChannels") - .HasColumnType("INTEGER"); - - b.Property("AudioSampleRate") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("AverageBitrate") - .HasColumnType("INTEGER"); - - b.Property("Bitrate") - .HasColumnType("INTEGER"); - - b.Property("ContentLengthBytes") - .HasColumnType("INTEGER"); - - b.Property("CreatedAtUtc") - .HasColumnType("TEXT"); - - b.Property("Framerate") - .HasColumnType("REAL"); - - b.Property("Height") - .HasColumnType("INTEGER"); - - b.Property("IsAdaptive") - .HasColumnType("INTEGER"); - - b.Property("LastModifiedUnixEpoch") - .HasColumnType("INTEGER"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LoudnessDb") - .HasColumnType("REAL"); - - b.Property("MimeType") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("Quality") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("QualityLabel") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("Width") - .HasColumnType("INTEGER"); - - b.HasKey("MediaId", "Itag"); - - b.ToTable("media_formats", (string)null); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.PlaylistEntity", b => - { - b.Property("Id") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("ChannelId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("CreatedAtUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasMaxLength(500) - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ChannelId"); - - b.ToTable("playlists", (string)null); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.CaptionEntity", b => - { - b.HasOne("Manager.Data.Entities.LibraryContext.MediaEntity", null) - .WithMany("Captions") - .HasForeignKey("MediaId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.ClientAccountEntity", b => - { - b.HasOne("Manager.Data.Entities.LibraryContext.ChannelEntity", null) - .WithOne("ClientAccount") - .HasForeignKey("Manager.Data.Entities.LibraryContext.ClientAccountEntity", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.HttpCookieEntity", b => - { - b.HasOne("Manager.Data.Entities.LibraryContext.ClientAccountEntity", null) - .WithMany("HttpCookies") - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.Join.PlaylistMedia", b => - { - b.HasOne("Manager.Data.Entities.LibraryContext.MediaEntity", null) - .WithMany("PlaylistMedias") - .HasForeignKey("MediaId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Manager.Data.Entities.LibraryContext.PlaylistEntity", null) - .WithMany("PlaylistMedias") - .HasForeignKey("PlaylistId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.MediaEntity", b => - { - b.HasOne("Manager.Data.Entities.LibraryContext.ChannelEntity", null) - .WithMany("Media") - .HasForeignKey("ChannelId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.MediaFormatEntity", b => - { - b.HasOne("Manager.Data.Entities.LibraryContext.MediaEntity", null) - .WithMany("Formats") - .HasForeignKey("MediaId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.PlaylistEntity", b => - { - b.HasOne("Manager.Data.Entities.LibraryContext.ChannelEntity", null) - .WithMany("Playlists") - .HasForeignKey("ChannelId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.ChannelEntity", b => - { - b.Navigation("ClientAccount"); - - b.Navigation("Media"); - - b.Navigation("Playlists"); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.ClientAccountEntity", b => - { - b.Navigation("HttpCookies"); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.MediaEntity", b => - { - b.Navigation("Captions"); - - b.Navigation("Formats"); - - b.Navigation("PlaylistMedias"); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.PlaylistEntity", b => - { - b.Navigation("PlaylistMedias"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Manager.Data/Migrations/20250902141251_InitialLibrary.cs b/Manager.Data/Migrations/20250902141251_InitialLibrary.cs deleted file mode 100644 index be499c7..0000000 --- a/Manager.Data/Migrations/20250902141251_InitialLibrary.cs +++ /dev/null @@ -1,259 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Manager.Data.Migrations -{ - /// - public partial class InitialLibrary : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "channels", - columns: table => new - { - Id = table.Column(type: "TEXT", maxLength: 100, nullable: false), - Name = table.Column(type: "TEXT", maxLength: 100, nullable: true), - Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), - JoinedDate = table.Column(type: "TEXT", nullable: false), - Subscribers = table.Column(type: "INTEGER", nullable: false), - TotalVideos = table.Column(type: "INTEGER", nullable: false), - TotalViews = table.Column(type: "INTEGER", nullable: false), - CreatedAtUtc = table.Column(type: "TEXT", nullable: false), - LastModifiedUtc = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_channels", x => x.Id); - }); - - migrationBuilder.CreateTable( - name: "client_accounts", - columns: table => new - { - Id = table.Column(type: "TEXT", maxLength: 100, nullable: false), - UserAgent = table.Column(type: "TEXT", maxLength: 100, nullable: true), - CreatedAtUtc = table.Column(type: "TEXT", nullable: false), - LastModifiedUtc = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_client_accounts", x => x.Id); - table.ForeignKey( - name: "FK_client_accounts_channels_Id", - column: x => x.Id, - principalTable: "channels", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "media", - columns: table => new - { - Id = table.Column(type: "TEXT", maxLength: 100, nullable: false), - Title = table.Column(type: "TEXT", maxLength: 100, nullable: true), - Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), - UploadDateUtc = table.Column(type: "TEXT", nullable: false), - ChannelId = table.Column(type: "TEXT", maxLength: 100, nullable: false), - ExternalState = table.Column(type: "INTEGER", nullable: false), - IsDownloaded = table.Column(type: "INTEGER", nullable: false), - State = table.Column(type: "INTEGER", nullable: false), - CreatedAtUtc = table.Column(type: "TEXT", nullable: false), - LastModifiedUtc = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_media", x => x.Id); - table.ForeignKey( - name: "FK_media_channels_ChannelId", - column: x => x.ChannelId, - principalTable: "channels", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "playlists", - columns: table => new - { - Id = table.Column(type: "TEXT", maxLength: 100, nullable: false), - Name = table.Column(type: "TEXT", maxLength: 100, nullable: false), - Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), - ChannelId = table.Column(type: "TEXT", maxLength: 100, nullable: false), - CreatedAtUtc = table.Column(type: "TEXT", nullable: false), - LastModifiedUtc = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_playlists", x => x.Id); - table.ForeignKey( - name: "FK_playlists_channels_ChannelId", - column: x => x.ChannelId, - principalTable: "channels", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "http_cookies", - columns: table => new - { - Name = table.Column(type: "TEXT", maxLength: 100, nullable: false), - Value = table.Column(type: "TEXT", maxLength: 100, nullable: true), - Domain = table.Column(type: "TEXT", maxLength: 100, nullable: true), - Path = table.Column(type: "TEXT", maxLength: 100, nullable: true), - ExpiresUtc = table.Column(type: "TEXT", nullable: true), - Secure = table.Column(type: "INTEGER", nullable: false), - HttpOnly = table.Column(type: "INTEGER", nullable: false), - SameSite = table.Column(type: "TEXT", maxLength: 100, nullable: true), - ClientId = table.Column(type: "TEXT", maxLength: 100, nullable: false), - CreatedAtUtc = table.Column(type: "TEXT", nullable: false), - LastModifiedUtc = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_http_cookies", x => x.Name); - table.ForeignKey( - name: "FK_http_cookies_client_accounts_ClientId", - column: x => x.ClientId, - principalTable: "client_accounts", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "captions", - columns: table => new - { - MediaId = table.Column(type: "TEXT", maxLength: 100, nullable: false), - LanguageCode = table.Column(type: "TEXT", maxLength: 100, nullable: false), - Name = table.Column(type: "TEXT", maxLength: 100, nullable: false), - CreatedAtUtc = table.Column(type: "TEXT", nullable: false), - LastModifiedUtc = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_captions", x => new { x.MediaId, x.LanguageCode }); - table.ForeignKey( - name: "FK_captions_media_MediaId", - column: x => x.MediaId, - principalTable: "media", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "media_formats", - columns: table => new - { - MediaId = table.Column(type: "TEXT", maxLength: 100, nullable: false), - Itag = table.Column(type: "INTEGER", nullable: false), - Quality = table.Column(type: "TEXT", maxLength: 100, nullable: true), - IsAdaptive = table.Column(type: "INTEGER", nullable: false), - MimeType = table.Column(type: "TEXT", maxLength: 100, nullable: true), - Bitrate = table.Column(type: "INTEGER", nullable: false), - AverageBitrate = table.Column(type: "INTEGER", nullable: false), - LastModifiedUnixEpoch = table.Column(type: "INTEGER", nullable: false), - ContentLengthBytes = table.Column(type: "INTEGER", nullable: false), - ApproxDurationMs = table.Column(type: "INTEGER", nullable: false), - Width = table.Column(type: "INTEGER", nullable: true), - Height = table.Column(type: "INTEGER", nullable: true), - Framerate = table.Column(type: "REAL", nullable: true), - QualityLabel = table.Column(type: "TEXT", maxLength: 100, nullable: true), - AudioChannels = table.Column(type: "INTEGER", nullable: true), - AudioSampleRate = table.Column(type: "TEXT", maxLength: 100, nullable: true), - LoudnessDb = table.Column(type: "REAL", nullable: true), - CreatedAtUtc = table.Column(type: "TEXT", nullable: false), - LastModifiedUtc = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_media_formats", x => new { x.MediaId, x.Itag }); - table.ForeignKey( - name: "FK_media_formats_media_MediaId", - column: x => x.MediaId, - principalTable: "media", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateTable( - name: "join_playlist_media", - columns: table => new - { - PlaylistId = table.Column(type: "TEXT", maxLength: 100, nullable: false), - MediaId = table.Column(type: "TEXT", maxLength: 100, nullable: false), - CreatedAtUtc = table.Column(type: "TEXT", nullable: false), - LastModifiedUtc = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_join_playlist_media", x => new { x.PlaylistId, x.MediaId }); - table.ForeignKey( - name: "FK_join_playlist_media_media_MediaId", - column: x => x.MediaId, - principalTable: "media", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_join_playlist_media_playlists_PlaylistId", - column: x => x.PlaylistId, - principalTable: "playlists", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); - - migrationBuilder.CreateIndex( - name: "IX_http_cookies_ClientId", - table: "http_cookies", - column: "ClientId"); - - migrationBuilder.CreateIndex( - name: "IX_join_playlist_media_MediaId", - table: "join_playlist_media", - column: "MediaId"); - - migrationBuilder.CreateIndex( - name: "IX_media_ChannelId", - table: "media", - column: "ChannelId"); - - migrationBuilder.CreateIndex( - name: "IX_playlists_ChannelId", - table: "playlists", - column: "ChannelId"); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "captions"); - - migrationBuilder.DropTable( - name: "http_cookies"); - - migrationBuilder.DropTable( - name: "join_playlist_media"); - - migrationBuilder.DropTable( - name: "media_formats"); - - migrationBuilder.DropTable( - name: "client_accounts"); - - migrationBuilder.DropTable( - name: "playlists"); - - migrationBuilder.DropTable( - name: "media"); - - migrationBuilder.DropTable( - name: "channels"); - } - } -} diff --git a/Manager.Data/Migrations/LibraryDbContextModelSnapshot.cs b/Manager.Data/Migrations/LibraryDbContextModelSnapshot.cs deleted file mode 100644 index 773b010..0000000 --- a/Manager.Data/Migrations/LibraryDbContextModelSnapshot.cs +++ /dev/null @@ -1,421 +0,0 @@ -// -using System; -using Manager.Data.Contexts; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Manager.Data.Migrations -{ - [DbContext(typeof(LibraryDbContext))] - partial class LibraryDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.19"); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.CaptionEntity", b => - { - b.Property("MediaId") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("LanguageCode") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("CreatedAtUtc") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.HasKey("MediaId", "LanguageCode"); - - b.ToTable("captions", (string)null); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.ChannelEntity", b => - { - b.Property("Id") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("CreatedAtUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasMaxLength(500) - .HasColumnType("TEXT"); - - b.Property("JoinedDate") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("Subscribers") - .HasColumnType("INTEGER"); - - b.Property("TotalVideos") - .HasColumnType("INTEGER"); - - b.Property("TotalViews") - .HasColumnType("INTEGER"); - - b.HasKey("Id"); - - b.ToTable("channels", (string)null); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.ClientAccountEntity", b => - { - b.Property("Id") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("CreatedAtUtc") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("UserAgent") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.ToTable("client_accounts", (string)null); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.HttpCookieEntity", b => - { - b.Property("Name") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("ClientId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("CreatedAtUtc") - .HasColumnType("TEXT"); - - b.Property("Domain") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("ExpiresUtc") - .HasColumnType("TEXT"); - - b.Property("HttpOnly") - .HasColumnType("INTEGER"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Path") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("SameSite") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("Secure") - .HasColumnType("INTEGER"); - - b.Property("Value") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.HasKey("Name"); - - b.HasIndex("ClientId"); - - b.ToTable("http_cookies", (string)null); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.Join.PlaylistMedia", b => - { - b.Property("PlaylistId") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("MediaId") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("CreatedAtUtc") - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.HasKey("PlaylistId", "MediaId"); - - b.HasIndex("MediaId"); - - b.ToTable("join_playlist_media", (string)null); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.MediaEntity", b => - { - b.Property("Id") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("ChannelId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("CreatedAtUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasMaxLength(500) - .HasColumnType("TEXT"); - - b.Property("ExternalState") - .HasColumnType("INTEGER"); - - b.Property("IsDownloaded") - .HasColumnType("INTEGER"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("State") - .HasColumnType("INTEGER"); - - b.Property("Title") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("UploadDateUtc") - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ChannelId"); - - b.ToTable("media", (string)null); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.MediaFormatEntity", b => - { - b.Property("MediaId") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("Itag") - .HasColumnType("INTEGER"); - - b.Property("ApproxDurationMs") - .HasColumnType("INTEGER"); - - b.Property("AudioChannels") - .HasColumnType("INTEGER"); - - b.Property("AudioSampleRate") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("AverageBitrate") - .HasColumnType("INTEGER"); - - b.Property("Bitrate") - .HasColumnType("INTEGER"); - - b.Property("ContentLengthBytes") - .HasColumnType("INTEGER"); - - b.Property("CreatedAtUtc") - .HasColumnType("TEXT"); - - b.Property("Framerate") - .HasColumnType("REAL"); - - b.Property("Height") - .HasColumnType("INTEGER"); - - b.Property("IsAdaptive") - .HasColumnType("INTEGER"); - - b.Property("LastModifiedUnixEpoch") - .HasColumnType("INTEGER"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("LoudnessDb") - .HasColumnType("REAL"); - - b.Property("MimeType") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("Quality") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("QualityLabel") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("Width") - .HasColumnType("INTEGER"); - - b.HasKey("MediaId", "Itag"); - - b.ToTable("media_formats", (string)null); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.PlaylistEntity", b => - { - b.Property("Id") - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("ChannelId") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.Property("CreatedAtUtc") - .HasColumnType("TEXT"); - - b.Property("Description") - .HasMaxLength(500) - .HasColumnType("TEXT"); - - b.Property("LastModifiedUtc") - .HasColumnType("TEXT"); - - b.Property("Name") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("TEXT"); - - b.HasKey("Id"); - - b.HasIndex("ChannelId"); - - b.ToTable("playlists", (string)null); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.CaptionEntity", b => - { - b.HasOne("Manager.Data.Entities.LibraryContext.MediaEntity", null) - .WithMany("Captions") - .HasForeignKey("MediaId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.ClientAccountEntity", b => - { - b.HasOne("Manager.Data.Entities.LibraryContext.ChannelEntity", null) - .WithOne("ClientAccount") - .HasForeignKey("Manager.Data.Entities.LibraryContext.ClientAccountEntity", "Id") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.HttpCookieEntity", b => - { - b.HasOne("Manager.Data.Entities.LibraryContext.ClientAccountEntity", null) - .WithMany("HttpCookies") - .HasForeignKey("ClientId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.Join.PlaylistMedia", b => - { - b.HasOne("Manager.Data.Entities.LibraryContext.MediaEntity", null) - .WithMany("PlaylistMedias") - .HasForeignKey("MediaId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.HasOne("Manager.Data.Entities.LibraryContext.PlaylistEntity", null) - .WithMany("PlaylistMedias") - .HasForeignKey("PlaylistId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.MediaEntity", b => - { - b.HasOne("Manager.Data.Entities.LibraryContext.ChannelEntity", null) - .WithMany("Media") - .HasForeignKey("ChannelId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.MediaFormatEntity", b => - { - b.HasOne("Manager.Data.Entities.LibraryContext.MediaEntity", null) - .WithMany("Formats") - .HasForeignKey("MediaId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.PlaylistEntity", b => - { - b.HasOne("Manager.Data.Entities.LibraryContext.ChannelEntity", null) - .WithMany("Playlists") - .HasForeignKey("ChannelId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.ChannelEntity", b => - { - b.Navigation("ClientAccount"); - - b.Navigation("Media"); - - b.Navigation("Playlists"); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.ClientAccountEntity", b => - { - b.Navigation("HttpCookies"); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.MediaEntity", b => - { - b.Navigation("Captions"); - - b.Navigation("Formats"); - - b.Navigation("PlaylistMedias"); - }); - - modelBuilder.Entity("Manager.Data.Entities.LibraryContext.PlaylistEntity", b => - { - b.Navigation("PlaylistMedias"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Manager.YouTube/Models/ClientExternalData.cs b/Manager.YouTube/Models/ClientExternalData.cs deleted file mode 100644 index 36d2128..0000000 --- a/Manager.YouTube/Models/ClientExternalData.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Manager.YouTube.Models.Innertube; - -namespace Manager.YouTube.Models; - -public class ClientExternalData -{ - public ClientState? State { get; set; } - public Channel? Channel { get; set; } - public List DatasyncIds { get; set; } = []; - - public string GetDatasyncId() - { - if (!string.IsNullOrWhiteSpace(State?.WebPlayerContextConfig?.WebPlayerContext?.DatasyncId)) - { - return State.WebPlayerContextConfig.WebPlayerContext.DatasyncId; - } - - var tempDatasyncId = ""; - foreach (var datasyncId in DatasyncIds) - { - var split = datasyncId.Split("||", StringSplitOptions.RemoveEmptyEntries); - switch (split.Length) - { - case 0: - case 2 when tempDatasyncId.Equals(split[1]): - continue; - case 2: - tempDatasyncId = split[1]; - break; - } - } - - return tempDatasyncId; - } -} \ No newline at end of file diff --git a/Manager.YouTube/NetworkService.cs b/Manager.YouTube/NetworkService.cs index e4bcf87..a37b3e2 100644 --- a/Manager.YouTube/NetworkService.cs +++ b/Manager.YouTube/NetworkService.cs @@ -7,24 +7,18 @@ public static class NetworkService { public const string Origin = "https://www.youtube.com"; - public static async Task> MakeRequestAsync(HttpRequestMessage request, YouTubeClient client) + public static async Task> MakeRequestAsync(HttpRequestMessage request, YouTubeClient client, bool skipAuthenticationHeader = false) { request.Headers.Add("Origin", Origin); request.Headers.UserAgent.ParseAdd(client.UserAgent); - if (client.SapisidCookie != null) + if (client.SapisidCookie != null && !skipAuthenticationHeader) { - request.Headers.Authorization = AuthenticationUtilities.GetSapisidHashHeader(client.External.GetDatasyncId(), client.SapisidCookie.Value, Origin); - } - - var httpClient = client.GetHttpClient(); - if (httpClient == null) - { - return ResultError.Fail("Failed getting http client!"); + request.Headers.Authorization = AuthenticationUtilities.GetSapisidHashHeader(client.GetDatasyncId(), client.SapisidCookie.Value, Origin); } try { - var response = await httpClient.SendAsync(request); + var response = await client.HttpClient.SendAsync(request); var contentString = await response.Content.ReadAsStringAsync(); if (!response.IsSuccessStatusCode) { diff --git a/Manager.YouTube/Util/AuthenticationUtilities.cs b/Manager.YouTube/Util/AuthenticationUtilities.cs index 918fd59..01cc3b6 100644 --- a/Manager.YouTube/Util/AuthenticationUtilities.cs +++ b/Manager.YouTube/Util/AuthenticationUtilities.cs @@ -10,16 +10,27 @@ public static class AuthenticationUtilities private const string HeaderScheme = "SAPISIDHASH"; // Dave Thomas & windy for updated answer @ https://stackoverflow.com/a/32065323/9948300 - public static AuthenticationHeaderValue? GetSapisidHashHeader(string datasyncId, string sapisid, string origin) + public static AuthenticationHeaderValue GetSapisidHashHeader(string datasyncId, string sapisid, string origin) { var strHash = GetSapisidHash(datasyncId, sapisid, origin); - return strHash == null ? null : new AuthenticationHeaderValue(HeaderScheme, strHash); + return new AuthenticationHeaderValue(HeaderScheme, strHash); } - public static string? GetSapisidHash(string datasyncId, string sapisid, string origin, string? time = null) + public static string GetSapisidHash(string datasyncId, string sapisid, string origin, string? time = null) { - if (string.IsNullOrWhiteSpace(datasyncId) || string.IsNullOrWhiteSpace(sapisid) || string.IsNullOrWhiteSpace(origin)) - return null; + if (string.IsNullOrWhiteSpace(datasyncId)) + { + throw new ArgumentNullException(nameof(datasyncId)); + } + if (string.IsNullOrWhiteSpace(sapisid)) + { + throw new ArgumentNullException(nameof(sapisid)); + } + if (string.IsNullOrWhiteSpace(origin)) + { + throw new ArgumentNullException(nameof(origin)); + } + datasyncId = datasyncId.Replace("||", ""); sapisid = Uri.UnescapeDataString(sapisid); if (string.IsNullOrWhiteSpace(time)) diff --git a/Manager.YouTube/YouTubeClient.cs b/Manager.YouTube/YouTubeClient.cs index e402ea5..7835e4b 100644 --- a/Manager.YouTube/YouTubeClient.cs +++ b/Manager.YouTube/YouTubeClient.cs @@ -4,7 +4,6 @@ using System.Text; using System.Text.Json; using System.Text.Json.Nodes; using DotBased.Monads; -using Manager.YouTube.Models; using Manager.YouTube.Models.Innertube; using Manager.YouTube.Parsers; using Manager.YouTube.Parsers.Json; @@ -16,44 +15,62 @@ public sealed class YouTubeClient : IDisposable public string Id { get; private set; } = ""; public string? UserAgent { get; set; } public CookieContainer CookieContainer { get; } = new() { PerDomainCapacity = 50 }; - public ClientExternalData External { get; set; } = new(); + public ClientState? State { get; private set; } + public List DatasyncIds { get; } = []; public Cookie? SapisidCookie => CookieContainer.GetAllCookies()["SAPISID"]; - public HttpClient? GetHttpClient() => _httpClient; + public HttpClient HttpClient { get; } - private HttpClient? _httpClient; - - public YouTubeClient() + private YouTubeClient(CookieCollection cookies, string userAgent) { - SetupClient(); + if (string.IsNullOrWhiteSpace(userAgent)) + { + throw new ArgumentNullException(nameof(userAgent)); + } + UserAgent = userAgent; + if (cookies.Count == 0) + { + Id = $"anon_{Guid.NewGuid()}"; + } + + CookieContainer.Add(cookies); + HttpClient = new HttpClient(GetHttpClientHandler()); } - private void SetupClient() + public static async Task> CreateAsync(CookieCollection cookies, string userAgent) { - _httpClient?.Dispose(); + var client = new YouTubeClient(cookies, userAgent); + var clientInitializeResult = await client.FetchClientDataAsync(); + if (!clientInitializeResult.IsSuccess) + { + return clientInitializeResult.Error ?? ResultError.Fail("Failed to initialize YouTube client!"); + } + return client; + } + + private HttpClientHandler GetHttpClientHandler() + { var clientHandler = new HttpClientHandler { AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip, UseCookies = true, CookieContainer = CookieContainer }; - _httpClient = new HttpClient(clientHandler); - _httpClient.DefaultRequestHeaders.Clear(); + return clientHandler; } - public async Task BuildClientAsync() + internal async Task FetchClientDataAsync() { - if (External.State is not { LoggedIn: true }) + if (State is not { LoggedIn: true }) { var state = await GetClientStateAsync(); if (!state.IsSuccess) { return state; } - External.State = state.Value; } - if (string.IsNullOrWhiteSpace(External.State.WebPlayerContextConfig?.WebPlayerContext?.DatasyncId)) + if (string.IsNullOrWhiteSpace(State?.WebPlayerContextConfig?.WebPlayerContext?.DatasyncId)) { var datasyncResult = await GetDatasyncIds(); if (!datasyncResult.IsSuccess) @@ -63,9 +80,9 @@ public sealed class YouTubeClient : IDisposable foreach (var id in datasyncResult.Value) { - if (External.DatasyncIds.Contains(id)) + if (DatasyncIds.Contains(id)) continue; - External.DatasyncIds.Add(id); + DatasyncIds.Add(id); } } @@ -79,18 +96,11 @@ public sealed class YouTubeClient : IDisposable Id = accountInfoResult.Value; } - - var channelResult = await GetChannelByIdAsync(Id); - if (!channelResult.IsSuccess) - { - return channelResult.Error ?? ResultError.Fail("Failed to get channel."); - } - External.Channel = channelResult.Value; return Result.Success(); } - public async Task> GetClientStateAsync() + private async Task GetClientStateAsync() { var httpRequest = new HttpRequestMessage { @@ -98,7 +108,7 @@ public sealed class YouTubeClient : IDisposable RequestUri = new Uri(NetworkService.Origin) }; - var result = await NetworkService.MakeRequestAsync(httpRequest, this); + var result = await NetworkService.MakeRequestAsync(httpRequest, this, true); if (!result.IsSuccess) { return result.Error ?? ResultError.Fail("Request failed!"); @@ -110,10 +120,9 @@ public sealed class YouTubeClient : IDisposable return clientStateResult.Error; } - ClientState? clientState; try { - clientState = JsonSerializer.Deserialize(clientStateResult.Value.Item1); + State = JsonSerializer.Deserialize(clientStateResult.Value.Item1); } catch (Exception e) { @@ -121,19 +130,19 @@ public sealed class YouTubeClient : IDisposable } - if (clientState == null) + if (State == null) { return ResultError.Fail("Unable to parse client state!"); } - clientState.IsPremiumUser = clientStateResult.Value.Item2; + State.IsPremiumUser = clientStateResult.Value.Item2; - return clientState; + return Result.Success(); } public async Task> GetChannelByIdAsync(string channelId) { - if (External.State == null) + if (State == null) { return ResultError.Fail("No client state!"); } @@ -143,12 +152,12 @@ public sealed class YouTubeClient : IDisposable return ResultError.Fail("Channel id is empty!"); } - var serializedContext = JsonSerializer.SerializeToNode(External.State.InnerTubeContext); + var serializedContext = JsonSerializer.SerializeToNode(State.InnerTubeContext); var payload = new JsonObject { { "context", serializedContext }, { "browseId", channelId } }; var requestMessage = new HttpRequestMessage { Method = HttpMethod.Post, - RequestUri = new Uri($"{NetworkService.Origin}/youtubei/v1/browse?key={External.State.InnertubeApiKey}"), + RequestUri = new Uri($"{NetworkService.Origin}/youtubei/v1/browse?key={State.InnertubeApiKey}"), Content = new StringContent(payload.ToJsonString(), Encoding.UTF8, MediaTypeNames.Application.Json) }; var responseResult = await NetworkService.MakeRequestAsync(requestMessage, this); @@ -160,14 +169,39 @@ public sealed class YouTubeClient : IDisposable return ChannelJsonParser.ParseJsonToChannelData(responseResult.Value); } + public string GetDatasyncId() + { + if (!string.IsNullOrWhiteSpace(State?.WebPlayerContextConfig?.WebPlayerContext?.DatasyncId)) + { + return State.WebPlayerContextConfig.WebPlayerContext.DatasyncId; + } + + var tempDatasyncId = ""; + foreach (var datasyncId in DatasyncIds) + { + var split = datasyncId.Split("||", StringSplitOptions.RemoveEmptyEntries); + switch (split.Length) + { + case 0: + case 2 when tempDatasyncId.Equals(split[1]): + continue; + case 2: + tempDatasyncId = split[1]; + break; + } + } + + return tempDatasyncId; + } + public void Dispose() { - _httpClient?.Dispose(); + HttpClient?.Dispose(); } private async Task> GetCurrentAccountIdAsync() { - if (External.State is not { LoggedIn: true }) + if (State is not { LoggedIn: true }) { return ResultError.Fail("Client not logged in!"); } @@ -177,7 +211,7 @@ public sealed class YouTubeClient : IDisposable Method = HttpMethod.Post, RequestUri = new Uri($"{NetworkService.Origin}/youtubei/v1/account/account_menu") }; - var serializedContext = JsonSerializer.SerializeToNode(External.State.InnerTubeContext); + var serializedContext = JsonSerializer.SerializeToNode(State.InnerTubeContext); var payload = new JsonObject { { "context", serializedContext } }; httpRequest.Content = new StringContent(payload.ToJsonString(), Encoding.UTF8, MediaTypeNames.Application.Json); var responseResult = await NetworkService.MakeRequestAsync(httpRequest, this); @@ -192,7 +226,7 @@ public sealed class YouTubeClient : IDisposable private async Task> GetDatasyncIds() { - if (External.State is not { LoggedIn: true } || CookieContainer.Count == 0) + if (State is not { LoggedIn: true } || CookieContainer.Count == 0) { return ResultError.Fail("Client is not logged in, requires logged in client for this endpoint (/getDatasyncIdsEndpoint)."); }