Compare commits

...

2 Commits

Author SHA1 Message Date
max
d501015b82 [GIT] Ignore library dir 2025-08-31 20:12:12 +02:00
max
98fe4b9391 [CHANGE] Library service, db migrations, ui blazor 2025-08-31 20:11:03 +02:00
19 changed files with 1204 additions and 18 deletions

1
.gitignore vendored
View File

@@ -482,3 +482,4 @@ $RECYCLE.BIN/
# Vim temporary swap files
*.swp
/Manager.App/Library/

View File

@@ -0,0 +1,23 @@
<MudOverlay Absolute="Absolute" DarkBackground LockScroll @bind-Visible="Visible" ZIndex="ZIndex">
<MudStack AlignItems="AlignItems.Center">
<MudProgressCircular Indeterminate/>
<MudText>@Message</MudText>
@if (CancellationTokenSource != null)
{
<MudButton OnClick="() => CancellationTokenSource.Cancel()" Disabled="CancellationTokenSource.IsCancellationRequested">Cancel operation</MudButton>
}
</MudStack>
</MudOverlay>
@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; }
}

View File

@@ -16,5 +16,7 @@
</MudTooltip>
}
</MudAppBar>
<div style="margin: 20px">
@Body
</div>
</CascadingValue>

View File

@@ -1,9 +1,9 @@
@inherits LayoutComponentBase
<MudThemeProvider @ref="@_themeProvider" IsDarkModeChanged="@OnDarkThemeChanged" Theme="@_mudTheme" IsDarkMode="@DarkTheme"/>
<MudPopoverProvider />
<MudDialogProvider/>
<MudSnackbarProvider/>
<MudPopoverProvider @rendermode="InteractiveServer"/>
<MudDialogProvider @rendermode="InteractiveServer"/>
<MudSnackbarProvider @rendermode="InteractiveServer"/>
<CascadingValue Value="this">
<MudLayout>

View File

@@ -0,0 +1,6 @@
@page "/Accounts"
@code {
}

View File

@@ -0,0 +1,7 @@
using Microsoft.AspNetCore.Components;
namespace Manager.App.Components.Pages;
public partial class Accounts : ComponentBase
{
}

View File

@@ -0,0 +1,40 @@
@page "/Library"
@inject ISnackbar Snackbar
@inject ILibraryService LibraryService
<PageTitle>Library information</PageTitle>
<ForcedLoadingOverlay Visible="_loading" CancellationTokenSource="@_cancellationTokenSource"/>
@if (_libraryInformation != null)
{
<MudSimpleTable Bordered Dense Elevation="0" Outlined Square Hover>
<tbody>
<tr>
<td>Library path:</td>
<td>@_libraryInformation.LibraryPath</td>
</tr>
<tr>
<td>Created at (UTC):</td>
<td>@_libraryInformation.CreatedAtUtc</td>
</tr>
<tr>
<td>Last modified (UTC):</td>
<td>@_libraryInformation.LastModifiedUtc</td>
</tr>
<tr>
<td>Library size:</td>
<td>@Suffix.BytesToSizeSuffix(_libraryInformation.TotalSizeBytes)</td>
</tr>
<tr>
<td>Total media:</td>
<td>@_libraryInformation.TotalMedia</td>
</tr>
<tr>
<td>Total channels:</td>
<td>@_libraryInformation.TotalChannels</td>
</tr>
</tbody>
</MudSimpleTable>
}

View File

@@ -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);
}
}
}

View File

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

View File

@@ -10,7 +10,11 @@
<PackageReference Include="DotBased" Version="1.0.0" />
<PackageReference Include="DotBased.Logging.MEL" Version="1.0.0" />
<PackageReference Include="DotBased.Logging.Serilog" Version="1.0.0" />
<PackageReference Include="MudBlazor" Version="8.10.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.19">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MudBlazor" Version="8.11.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
</ItemGroup>
@@ -26,4 +30,8 @@
<ProjectReference Include="..\Manager.YouTube\Manager.YouTube.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Logs\Debug\" />
</ItemGroup>
</Project>

View File

@@ -1,6 +1,9 @@
using DotBased.Monads;
using Manager.App.Models.Library;
namespace Manager.App.Services;
public interface ILibraryService
{
public Task<Result<LibraryInformation>> GetLibraryInfoAsync(CancellationToken cancellationToken = default);
}

View File

@@ -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<Result<LibraryInformation>> GetLibraryInfoAsync()
public async Task<Result<LibraryInformation>> 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;
}
}
}

View File

@@ -32,17 +32,25 @@ public sealed class LibraryDbContext : DbContext
{
channel.ToTable("channels");
channel.HasKey(x => x.Id);
//TODO: Link media from channel
});
modelBuilder.Entity<ClientAccountEntity>(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<HttpCookieEntity>(httpce =>
{
httpce.ToTable("http_cookies");
httpce.HasKey(x => x.Name);
});
modelBuilder.Entity<MediaEntity>(me =>

View File

@@ -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<PlaylistEntity> Playlists { get; set; } = [];

View File

@@ -11,8 +11,12 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.19" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.19">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.19" />
</ItemGroup>
<ItemGroup>

View File

@@ -0,0 +1,386 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<string>("MediaId")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("LanguageCode")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("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<string>("Id")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAtUtc")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<DateTime>("JoinedDate")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModifiedUtc")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<long>("Subscribers")
.HasColumnType("INTEGER");
b.Property<long>("TotalVideos")
.HasColumnType("INTEGER");
b.Property<long>("TotalViews")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("channels", (string)null);
});
modelBuilder.Entity("Manager.Data.Entities.LibraryContext.ClientAccountEntity", b =>
{
b.Property<string>("Id")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAtUtc")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModifiedUtc")
.HasColumnType("TEXT");
b.Property<string>("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<string>("Name")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("ClientId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAtUtc")
.HasColumnType("TEXT");
b.Property<string>("Domain")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("ExpiresUtc")
.HasColumnType("TEXT");
b.Property<bool>("HttpOnly")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastModifiedUtc")
.HasColumnType("TEXT");
b.Property<string>("Path")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("SameSite")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<bool>("Secure")
.HasColumnType("INTEGER");
b.Property<string>("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<string>("PlaylistId")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("MediaId")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime>("DateAddedUtc")
.HasColumnType("TEXT");
b.Property<DateTime>("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<string>("Id")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("ChannelId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAtUtc")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<int>("ExternalState")
.HasColumnType("INTEGER");
b.Property<bool>("IsDownloaded")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastModifiedUtc")
.HasColumnType("TEXT");
b.Property<int>("State")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime>("UploadDateUtc")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("media", (string)null);
});
modelBuilder.Entity("Manager.Data.Entities.LibraryContext.MediaFormatEntity", b =>
{
b.Property<string>("MediaId")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<int>("Itag")
.HasColumnType("INTEGER");
b.Property<long>("ApproxDurationMs")
.HasColumnType("INTEGER");
b.Property<int?>("AudioChannels")
.HasColumnType("INTEGER");
b.Property<string>("AudioSampleRate")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<long>("AverageBitrate")
.HasColumnType("INTEGER");
b.Property<long>("Bitrate")
.HasColumnType("INTEGER");
b.Property<long>("ContentLengthBytes")
.HasColumnType("INTEGER");
b.Property<double?>("Framerate")
.HasColumnType("REAL");
b.Property<int?>("Height")
.HasColumnType("INTEGER");
b.Property<bool>("IsAdaptive")
.HasColumnType("INTEGER");
b.Property<long>("LastModifiedUnixEpoch")
.HasColumnType("INTEGER");
b.Property<double?>("LoudnessDb")
.HasColumnType("REAL");
b.Property<string>("MimeType")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("Quality")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("QualityLabel")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<int?>("Width")
.HasColumnType("INTEGER");
b.HasKey("MediaId", "Itag");
b.ToTable("media_formats", (string)null);
});
modelBuilder.Entity("Manager.Data.Entities.LibraryContext.PlaylistEntity", b =>
{
b.Property<string>("Id")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("ChannelId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAtUtc")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<DateTime>("LastModifiedUtc")
.HasColumnType("TEXT");
b.Property<string>("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
}
}
}

View File

@@ -0,0 +1,238 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Manager.Data.Migrations
{
/// <inheritdoc />
public partial class initialLib : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "channels",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
Description = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
JoinedDate = table.Column<DateTime>(type: "TEXT", nullable: false),
Subscribers = table.Column<long>(type: "INTEGER", nullable: false),
TotalVideos = table.Column<long>(type: "INTEGER", nullable: false),
TotalViews = table.Column<long>(type: "INTEGER", nullable: false),
CreatedAtUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
LastModifiedUtc = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_channels", x => x.Id);
});
migrationBuilder.CreateTable(
name: "client_accounts",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
CreatedAtUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
LastModifiedUtc = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_client_accounts", x => x.Id);
});
migrationBuilder.CreateTable(
name: "media",
columns: table => new
{
Id = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
Title = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
Description = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
UploadDateUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
ChannelId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
ExternalState = table.Column<int>(type: "INTEGER", nullable: false),
IsDownloaded = table.Column<bool>(type: "INTEGER", nullable: false),
State = table.Column<int>(type: "INTEGER", nullable: false),
CreatedAtUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
LastModifiedUtc = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_media", x => x.Id);
});
migrationBuilder.CreateTable(
name: "http_cookies",
columns: table => new
{
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
Value = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
Domain = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
Path = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
ExpiresUtc = table.Column<DateTimeOffset>(type: "TEXT", nullable: true),
Secure = table.Column<bool>(type: "INTEGER", nullable: false),
HttpOnly = table.Column<bool>(type: "INTEGER", nullable: false),
SameSite = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
ClientId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
CreatedAtUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
LastModifiedUtc = table.Column<DateTime>(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<string>(type: "TEXT", maxLength: 100, nullable: false),
Name = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
Description = table.Column<string>(type: "TEXT", maxLength: 500, nullable: true),
ChannelId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
CreatedAtUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
LastModifiedUtc = table.Column<DateTime>(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<string>(type: "TEXT", maxLength: 100, nullable: false),
LanguageCode = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
Name = table.Column<string>(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<string>(type: "TEXT", maxLength: 100, nullable: false),
Itag = table.Column<int>(type: "INTEGER", nullable: false),
Quality = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
IsAdaptive = table.Column<bool>(type: "INTEGER", nullable: false),
MimeType = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
Bitrate = table.Column<long>(type: "INTEGER", nullable: false),
AverageBitrate = table.Column<long>(type: "INTEGER", nullable: false),
LastModifiedUnixEpoch = table.Column<long>(type: "INTEGER", nullable: false),
ContentLengthBytes = table.Column<long>(type: "INTEGER", nullable: false),
ApproxDurationMs = table.Column<long>(type: "INTEGER", nullable: false),
Width = table.Column<int>(type: "INTEGER", nullable: true),
Height = table.Column<int>(type: "INTEGER", nullable: true),
Framerate = table.Column<double>(type: "REAL", nullable: true),
QualityLabel = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
AudioChannels = table.Column<int>(type: "INTEGER", nullable: true),
AudioSampleRate = table.Column<string>(type: "TEXT", maxLength: 100, nullable: true),
LoudnessDb = table.Column<double>(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<string>(type: "TEXT", maxLength: 100, nullable: false),
MediaId = table.Column<string>(type: "TEXT", maxLength: 100, nullable: false),
DateAddedUtc = table.Column<DateTime>(type: "TEXT", nullable: false),
DateModifiedUtc = table.Column<DateTime>(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");
}
/// <inheritdoc />
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");
}
}
}

View File

@@ -0,0 +1,383 @@
// <auto-generated />
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<string>("MediaId")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("LanguageCode")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("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<string>("Id")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAtUtc")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<DateTime>("JoinedDate")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModifiedUtc")
.HasColumnType("TEXT");
b.Property<string>("Name")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<long>("Subscribers")
.HasColumnType("INTEGER");
b.Property<long>("TotalVideos")
.HasColumnType("INTEGER");
b.Property<long>("TotalViews")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.ToTable("channels", (string)null);
});
modelBuilder.Entity("Manager.Data.Entities.LibraryContext.ClientAccountEntity", b =>
{
b.Property<string>("Id")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAtUtc")
.HasColumnType("TEXT");
b.Property<DateTime>("LastModifiedUtc")
.HasColumnType("TEXT");
b.Property<string>("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<string>("Name")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("ClientId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAtUtc")
.HasColumnType("TEXT");
b.Property<string>("Domain")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTimeOffset?>("ExpiresUtc")
.HasColumnType("TEXT");
b.Property<bool>("HttpOnly")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastModifiedUtc")
.HasColumnType("TEXT");
b.Property<string>("Path")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("SameSite")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<bool>("Secure")
.HasColumnType("INTEGER");
b.Property<string>("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<string>("PlaylistId")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("MediaId")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime>("DateAddedUtc")
.HasColumnType("TEXT");
b.Property<DateTime>("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<string>("Id")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("ChannelId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAtUtc")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<int>("ExternalState")
.HasColumnType("INTEGER");
b.Property<bool>("IsDownloaded")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastModifiedUtc")
.HasColumnType("TEXT");
b.Property<int>("State")
.HasColumnType("INTEGER");
b.Property<string>("Title")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime>("UploadDateUtc")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("media", (string)null);
});
modelBuilder.Entity("Manager.Data.Entities.LibraryContext.MediaFormatEntity", b =>
{
b.Property<string>("MediaId")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<int>("Itag")
.HasColumnType("INTEGER");
b.Property<long>("ApproxDurationMs")
.HasColumnType("INTEGER");
b.Property<int?>("AudioChannels")
.HasColumnType("INTEGER");
b.Property<string>("AudioSampleRate")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<long>("AverageBitrate")
.HasColumnType("INTEGER");
b.Property<long>("Bitrate")
.HasColumnType("INTEGER");
b.Property<long>("ContentLengthBytes")
.HasColumnType("INTEGER");
b.Property<double?>("Framerate")
.HasColumnType("REAL");
b.Property<int?>("Height")
.HasColumnType("INTEGER");
b.Property<bool>("IsAdaptive")
.HasColumnType("INTEGER");
b.Property<long>("LastModifiedUnixEpoch")
.HasColumnType("INTEGER");
b.Property<double?>("LoudnessDb")
.HasColumnType("REAL");
b.Property<string>("MimeType")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("Quality")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("QualityLabel")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<int?>("Width")
.HasColumnType("INTEGER");
b.HasKey("MediaId", "Itag");
b.ToTable("media_formats", (string)null);
});
modelBuilder.Entity("Manager.Data.Entities.LibraryContext.PlaylistEntity", b =>
{
b.Property<string>("Id")
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<string>("ChannelId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedAtUtc")
.HasColumnType("TEXT");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("TEXT");
b.Property<DateTime>("LastModifiedUtc")
.HasColumnType("TEXT");
b.Property<string>("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
}
}
}

View File

@@ -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 <MigrationName> --project Manager.Data --context Manager.Data.Contexts.AppDbContext
dotnet ef migrations add <migration-name> --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
```