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
```