From 98fe4b939106bb5d98ca76166b281c61249dc2b6 Mon Sep 17 00:00:00 2001 From: max Date: Sun, 31 Aug 2025 20:11:03 +0200 Subject: [PATCH] [CHANGE] Library service, db migrations, ui blazor --- .gitignore | 3 + .../Application/ForcedLoadingOverlay.razor | 23 ++ .../Components/Layout/ApplicationLayout.razor | 4 +- .../Components/Layout/BaseLayout.razor | 6 +- Manager.App/Components/Pages/Accounts.razor | 6 + .../Components/Pages/Accounts.razor.cs | 7 + Manager.App/Components/Pages/Library.razor | 40 ++ Manager.App/Components/Pages/Library.razor.cs | 36 ++ Manager.App/Components/_Imports.razor | 3 + Manager.App/Manager.App.csproj | 10 +- Manager.App/Services/ILibraryService.cs | 5 +- Manager.App/Services/LibraryService.cs | 49 ++- Manager.Data/Contexts/LibraryDbContext.cs | 8 + .../LibraryContext/ClientAccountEntity.cs | 3 +- Manager.Data/Manager.Data.csproj | 8 +- .../20250831150411_initialLib.Designer.cs | 386 ++++++++++++++++++ .../Migrations/20250831150411_initialLib.cs | 238 +++++++++++ .../LibraryDbContextModelSnapshot.cs | 383 +++++++++++++++++ README.md | 6 +- 19 files changed, 1206 insertions(+), 18 deletions(-) create mode 100644 Manager.App/Components/Application/ForcedLoadingOverlay.razor create mode 100644 Manager.App/Components/Pages/Accounts.razor create mode 100644 Manager.App/Components/Pages/Accounts.razor.cs create mode 100644 Manager.App/Components/Pages/Library.razor create mode 100644 Manager.App/Components/Pages/Library.razor.cs create mode 100644 Manager.Data/Migrations/20250831150411_initialLib.Designer.cs create mode 100644 Manager.Data/Migrations/20250831150411_initialLib.cs create mode 100644 Manager.Data/Migrations/LibraryDbContextModelSnapshot.cs diff --git a/.gitignore b/.gitignore index 104b544..d3ca710 100644 --- a/.gitignore +++ b/.gitignore @@ -482,3 +482,6 @@ $RECYCLE.BIN/ # Vim temporary swap files *.swp +/Manager.App/Library/Library.db +/Manager.App/Library/Library.db-shm +/Manager.App/Library/Library.db-wal diff --git a/Manager.App/Components/Application/ForcedLoadingOverlay.razor b/Manager.App/Components/Application/ForcedLoadingOverlay.razor new file mode 100644 index 0000000..e83ad1b --- /dev/null +++ b/Manager.App/Components/Application/ForcedLoadingOverlay.razor @@ -0,0 +1,23 @@ + + + + @Message + @if (CancellationTokenSource != null) + { + Cancel operation + } + + + +@code { + [Parameter] + public bool Visible { get; set; } + [Parameter] + public string Message { get; set; } = "Loading..."; + [Parameter] + public bool Absolute { get; set; } + [Parameter] + public int ZIndex { get; set; } = 9999; + [Parameter] + public CancellationTokenSource? CancellationTokenSource { get; set; } +} \ No newline at end of file diff --git a/Manager.App/Components/Layout/ApplicationLayout.razor b/Manager.App/Components/Layout/ApplicationLayout.razor index 9a10629..d4fb985 100644 --- a/Manager.App/Components/Layout/ApplicationLayout.razor +++ b/Manager.App/Components/Layout/ApplicationLayout.razor @@ -16,5 +16,7 @@ } - @Body +
+ @Body +
\ No newline at end of file diff --git a/Manager.App/Components/Layout/BaseLayout.razor b/Manager.App/Components/Layout/BaseLayout.razor index 892cce9..04778dd 100644 --- a/Manager.App/Components/Layout/BaseLayout.razor +++ b/Manager.App/Components/Layout/BaseLayout.razor @@ -1,9 +1,9 @@ @inherits LayoutComponentBase - - - + + + diff --git a/Manager.App/Components/Pages/Accounts.razor b/Manager.App/Components/Pages/Accounts.razor new file mode 100644 index 0000000..e93608c --- /dev/null +++ b/Manager.App/Components/Pages/Accounts.razor @@ -0,0 +1,6 @@ +@page "/Accounts" + + +@code { + +} \ No newline at end of file diff --git a/Manager.App/Components/Pages/Accounts.razor.cs b/Manager.App/Components/Pages/Accounts.razor.cs new file mode 100644 index 0000000..34f9b04 --- /dev/null +++ b/Manager.App/Components/Pages/Accounts.razor.cs @@ -0,0 +1,7 @@ +using Microsoft.AspNetCore.Components; + +namespace Manager.App.Components.Pages; + +public partial class Accounts : ComponentBase +{ +} \ No newline at end of file diff --git a/Manager.App/Components/Pages/Library.razor b/Manager.App/Components/Pages/Library.razor new file mode 100644 index 0000000..1f5e74f --- /dev/null +++ b/Manager.App/Components/Pages/Library.razor @@ -0,0 +1,40 @@ +@page "/Library" + +@inject ISnackbar Snackbar +@inject ILibraryService LibraryService + +Library information + + +@if (_libraryInformation != null) +{ + + + + Library path: + @_libraryInformation.LibraryPath + + + Created at (UTC): + @_libraryInformation.CreatedAtUtc + + + Last modified (UTC): + @_libraryInformation.LastModifiedUtc + + + Library size: + @Suffix.BytesToSizeSuffix(_libraryInformation.TotalSizeBytes) + + + + Total media: + @_libraryInformation.TotalMedia + + + Total channels: + @_libraryInformation.TotalChannels + + + +} \ No newline at end of file diff --git a/Manager.App/Components/Pages/Library.razor.cs b/Manager.App/Components/Pages/Library.razor.cs new file mode 100644 index 0000000..22750e7 --- /dev/null +++ b/Manager.App/Components/Pages/Library.razor.cs @@ -0,0 +1,36 @@ +using Manager.App.Models.Library; +using MudBlazor; + +namespace Manager.App.Components.Pages; + +public partial class Library +{ + private LibraryInformation? _libraryInformation; + private bool _loading; + private CancellationTokenSource _cancellationTokenSource = new(); + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + if (_cancellationTokenSource.IsCancellationRequested) + { + _cancellationTokenSource = new CancellationTokenSource(); + } + + _loading = true; + await InvokeAsync(StateHasChanged); + var result = await LibraryService.GetLibraryInfoAsync(_cancellationTokenSource.Token); + if (result is { IsSuccess: true, Value: not null }) + { + _libraryInformation = result.Value; + } + else + { + Snackbar.Add($"Failed to get library info. Error: {result.Error?.Description}", Severity.Error); + } + _loading = false; + await InvokeAsync(StateHasChanged); + } + } +} \ No newline at end of file diff --git a/Manager.App/Components/_Imports.razor b/Manager.App/Components/_Imports.razor index f854c12..04bd2e4 100644 --- a/Manager.App/Components/_Imports.razor +++ b/Manager.App/Components/_Imports.razor @@ -6,8 +6,11 @@ @using static Microsoft.AspNetCore.Components.Web.RenderMode @using Microsoft.AspNetCore.Components.Web.Virtualization @using Microsoft.JSInterop +@using DotBased.Utilities @using Manager.App @using Manager.App.Components +@using Manager.App.Components.Application +@using Manager.App.Services @* MudBlazor *@ @using MudBlazor \ No newline at end of file diff --git a/Manager.App/Manager.App.csproj b/Manager.App/Manager.App.csproj index e95d807..36dbce5 100644 --- a/Manager.App/Manager.App.csproj +++ b/Manager.App/Manager.App.csproj @@ -10,7 +10,11 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + @@ -26,4 +30,8 @@ + + + + diff --git a/Manager.App/Services/ILibraryService.cs b/Manager.App/Services/ILibraryService.cs index a797581..1ddd997 100644 --- a/Manager.App/Services/ILibraryService.cs +++ b/Manager.App/Services/ILibraryService.cs @@ -1,6 +1,9 @@ +using DotBased.Monads; +using Manager.App.Models.Library; + namespace Manager.App.Services; public interface ILibraryService { - + public Task> GetLibraryInfoAsync(CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/Manager.App/Services/LibraryService.cs b/Manager.App/Services/LibraryService.cs index a1b2faa..90e3aeb 100644 --- a/Manager.App/Services/LibraryService.cs +++ b/Manager.App/Services/LibraryService.cs @@ -21,14 +21,51 @@ public class LibraryService : ILibraryService _logger = logger; _librarySettings = librarySettings.Value; _dbContextFactory = contextFactory; - _libraryDirectory = Directory.CreateDirectory(Path.Combine(_librarySettings.Path, SubDirMedia)); - _libraryDirectory = Directory.CreateDirectory(Path.Combine(_librarySettings.Path, SubDirChannels)); + _libraryDirectory = Directory.CreateDirectory(_librarySettings.Path); + logger.LogDebug("Working dir for library: {LibraryWorkingDir}", _libraryDirectory.FullName); + Directory.CreateDirectory(Path.Combine(_librarySettings.Path, SubDirMedia)); + Directory.CreateDirectory(Path.Combine(_librarySettings.Path, SubDirChannels)); } - public async Task> GetLibraryInfoAsync() + public async Task> GetLibraryInfoAsync(CancellationToken cancellationToken = default) { - await using var context = await _dbContextFactory.CreateDbContextAsync(); - //TODO: Get library info - return ResultError.Fail("Not implemented!"); + try + { + await using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + var libInfo = new LibraryInformation + { + LibraryPath = _libraryDirectory.FullName, + CreatedAtUtc = _libraryDirectory.CreationTimeUtc, + LastModifiedUtc = _libraryDirectory.LastWriteTimeUtc, + TotalChannels = await context.Channels.CountAsync(cancellationToken: cancellationToken), + TotalMedia = await context.Media.CountAsync(cancellationToken: cancellationToken), + TotalSizeBytes = GetDirectorySize(_libraryDirectory) + }; + return libInfo; + } + catch (Exception e) + { + if (e is OperationCanceledException) + { + return ResultError.Fail("Library service operation cancelled"); + } + + _logger.LogError(e, "Failed to get library information"); + return ResultError.Fail("Failed to get library information"); + } + } + + private long GetDirectorySize(DirectoryInfo dir) + { + try + { + var size = dir.EnumerateFiles("", SearchOption.AllDirectories).Select(f => f.Length).Sum(); + return size; + } + catch (Exception e) + { + _logger.LogError(e, "Error while getting directory size."); + throw; + } } } \ No newline at end of file diff --git a/Manager.Data/Contexts/LibraryDbContext.cs b/Manager.Data/Contexts/LibraryDbContext.cs index 747e31b..26b65c5 100644 --- a/Manager.Data/Contexts/LibraryDbContext.cs +++ b/Manager.Data/Contexts/LibraryDbContext.cs @@ -32,17 +32,25 @@ public sealed class LibraryDbContext : DbContext { channel.ToTable("channels"); channel.HasKey(x => x.Id); + //TODO: Link media from channel }); modelBuilder.Entity(cae => { cae.ToTable("client_accounts"); cae.HasKey(x => x.Id); + cae.HasMany(x => x.Playlists) + .WithOne() + .HasForeignKey(x => x.ChannelId); + cae.HasMany(x => x.HttpCookies) + .WithOne() + .HasForeignKey(x => x.ClientId); }); modelBuilder.Entity(httpce => { httpce.ToTable("http_cookies"); + httpce.HasKey(x => x.Name); }); modelBuilder.Entity(me => diff --git a/Manager.Data/Entities/LibraryContext/ClientAccountEntity.cs b/Manager.Data/Entities/LibraryContext/ClientAccountEntity.cs index a05baf2..e29c442 100644 --- a/Manager.Data/Entities/LibraryContext/ClientAccountEntity.cs +++ b/Manager.Data/Entities/LibraryContext/ClientAccountEntity.cs @@ -4,7 +4,8 @@ namespace Manager.Data.Entities.LibraryContext; public class ClientAccountEntity : DateTimeBase { - public Guid Id { get; set; } + [MaxLength(DataConstants.DbContext.DefaultDbStringSize)] + public required string Id { get; set; } [MaxLength(DataConstants.DbContext.DefaultDbStringSize)] public string Name { get; set; } = ""; public List Playlists { get; set; } = []; diff --git a/Manager.Data/Manager.Data.csproj b/Manager.Data/Manager.Data.csproj index e749a16..f385f6a 100644 --- a/Manager.Data/Manager.Data.csproj +++ b/Manager.Data/Manager.Data.csproj @@ -11,8 +11,12 @@ - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/Manager.Data/Migrations/20250831150411_initialLib.Designer.cs b/Manager.Data/Migrations/20250831150411_initialLib.Designer.cs new file mode 100644 index 0000000..2ac6751 --- /dev/null +++ b/Manager.Data/Migrations/20250831150411_initialLib.Designer.cs @@ -0,0 +1,386 @@ +// +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("20250831150411_initialLib")] + partial class initialLib + { + /// + 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("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("Name") + .IsRequired() + .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("DateAddedUtc") + .HasColumnType("TEXT"); + + b.Property("DateModifiedUtc") + .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.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("Framerate") + .HasColumnType("REAL"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IsAdaptive") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedUnixEpoch") + .HasColumnType("INTEGER"); + + 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.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.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.ClientAccountEntity", null) + .WithMany("Playlists") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Manager.Data.Entities.LibraryContext.ClientAccountEntity", b => + { + b.Navigation("HttpCookies"); + + b.Navigation("Playlists"); + }); + + 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/20250831150411_initialLib.cs b/Manager.Data/Migrations/20250831150411_initialLib.cs new file mode 100644 index 0000000..631dd91 --- /dev/null +++ b/Manager.Data/Migrations/20250831150411_initialLib.cs @@ -0,0 +1,238 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Manager.Data.Migrations +{ + /// + public partial class initialLib : 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), + 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_client_accounts", x => x.Id); + }); + + 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); + }); + + 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: "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_client_accounts_ChannelId", + column: x => x.ChannelId, + 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) + }, + 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) + }, + 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), + DateAddedUtc = table.Column(type: "TEXT", nullable: false), + DateModifiedUtc = 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_playlists_ChannelId", + table: "playlists", + column: "ChannelId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "captions"); + + migrationBuilder.DropTable( + name: "channels"); + + migrationBuilder.DropTable( + name: "http_cookies"); + + migrationBuilder.DropTable( + name: "join_playlist_media"); + + migrationBuilder.DropTable( + name: "media_formats"); + + migrationBuilder.DropTable( + name: "playlists"); + + migrationBuilder.DropTable( + name: "media"); + + migrationBuilder.DropTable( + name: "client_accounts"); + } + } +} diff --git a/Manager.Data/Migrations/LibraryDbContextModelSnapshot.cs b/Manager.Data/Migrations/LibraryDbContextModelSnapshot.cs new file mode 100644 index 0000000..e284ea3 --- /dev/null +++ b/Manager.Data/Migrations/LibraryDbContextModelSnapshot.cs @@ -0,0 +1,383 @@ +// +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("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("Name") + .IsRequired() + .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("DateAddedUtc") + .HasColumnType("TEXT"); + + b.Property("DateModifiedUtc") + .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.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("Framerate") + .HasColumnType("REAL"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("IsAdaptive") + .HasColumnType("INTEGER"); + + b.Property("LastModifiedUnixEpoch") + .HasColumnType("INTEGER"); + + 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.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.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.ClientAccountEntity", null) + .WithMany("Playlists") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Manager.Data.Entities.LibraryContext.ClientAccountEntity", b => + { + b.Navigation("HttpCookies"); + + b.Navigation("Playlists"); + }); + + 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/README.md b/README.md index 34699a5..ff6e788 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,12 @@ Server application for managing YouTube content and managing a local library. ### Migrations Using migrations using the .NET CLI -Add migration +Add migration (library) ```shell -dotnet ef migrations add --project Manager.Data --context Manager.Data.Contexts.AppDbContext +dotnet ef migrations add --project Manager.Data --startup-project Manager.App --context Manager.Data.Contexts.LibraryDbContext ``` Update the database ```shell -dotnet ef database update --project Manager.Data --context Manager.Data.Contexts.AppDbContext +dotnet ef database update --project Manager.Data --startup-project Manager.App --context Manager.Data.Contexts.LibraryDbContext ```