feat: add database connections, initial migration and early CLI tooling
All checks were successful
Update changelog / changelog (push) Successful in 25s

This commit is contained in:
2025-12-09 01:46:05 +01:00
parent ad3cd6710d
commit f648a73cb2
26 changed files with 2399 additions and 52 deletions

View File

@@ -14,10 +14,11 @@ jobs:
runs-on: ubuntu-latest
container: docker.io/thegeeklab/git-sv:2.0.9
steps:
- name: install tools
- name: Install tools
run: |
apk add -q --update --no-cache nodejs curl jq sed
- uses: actions/checkout@v6
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Generate upcoming changelog

304
Controllers/Cli.cs Normal file
View File

@@ -0,0 +1,304 @@
using Shadow.Data;
using Shadow.Entities;
using System.Security;
namespace Shadow.Controllers;
public class Cli
{
private readonly ApplicationDbContext db;
private readonly GeneralUseHelpers guhf;
private readonly string[] args;
// TODO: Add "changeUser"
public Cli(ApplicationDbContext _db, GeneralUseHelpers _guhf, string[] _args)
{
db = _db;
guhf = _guhf;
args = _args;
}
public async Task<bool> Parse()
{
// Returns true if the program can finish execution.
bool exit = true;
// Check if anything has been passed
if (args.Length == 0 || args.FirstOrDefault() is null) {
return true;
}
switch (args[0].ToLower())
{
case "adduser":
AddUser();
break;
case "resetpassword":
ResetPassword();
break;
case "removeuser":
RemoveUser();
break;
case "help":
case "--help":
case "h":
case "-h":
case "/h":
case "/?":
case "?":
ShowHelp();
break;
default:
Console.WriteLine($"Unknown option: \"{args[0]}\". See \"help\" for available arguments.");
break;
}
return exit;
}
public void ShowHelp()
{
// Shown when "Shadow --help"/"Shadow help"/... is ran.
Console.WriteLine(
$"--- Shadow commandline utility ---\n" +
$"Shadow version: #{ThisAssembly.Git.Commit} {ThisAssembly.Git.Branch} ({ThisAssembly.Git.CommitDate})\n\n" +
$"Available commands:\n" +
$"- addUser [username] - create a new user,\n" +
$"- resetPassword [username] - reset a user's password,\n" +
$"- removeUser [username] - remove the user COMPLETELY.\n\n" +
$"Username is optional. If not provided, user will be prompted to enter it.\n" +
$"Running without specifying a command launches a Kestrel web server.\n\n" +
$"License: AGPLv3+, Source code: https://gitea.7o7.cx/sherl/Shadow"
);
}
public void AddUser()
{
// Check if any cli arguments have been passed
// Both:
// Shadow addUser
// and
// Shadow addUser username
// are supported.
Console.WriteLine($"[Shadow] Add a new user");
if (args.Length == 2)
Console.WriteLine($" You will be promped to enter a password.");
else
Console.WriteLine($" You will be promped to enter a username and password.");
string? username = null;
if (args.Length != 2)
{
while (username is null || username == String.Empty)
username = ReadName(" Please enter a username: ");
}
// Check if user by this name exists in DB
User? foundUser = db.Users
.FirstOrDefault(u => u.NormalizedName == username!.ToLower());
if (foundUser != null) {
Console.WriteLine("Error! User with this name already exists in the database!");
return;
}
string password = ReadPassword();
string passwordConfirmation = ReadPassword(" Confirm password: ");
if (!password.Equals(passwordConfirmation))
{
Console.WriteLine("Error! Passwords do not match. Please try again.");
return;
}
bool isUserAdmin = YesNoPrompt($" Should \"{username}\" be an administrator? [y/N]: ", false);
User newUser = new User
{
Name = username!,
NormalizedName = username!.ToLower(),
Password = password
};
db.Users.Add(newUser);
db.SaveChanges();
}
public void ResetPassword()
{
// Check if any cli arguments have been passed
// Both:
// Shadow resetPassword
// and
// Shadow resetPassword username
// are supported.
Console.WriteLine($"[Shadow] Reset password");
if (args.Length == 2)
Console.WriteLine($" You will be promped to enter a new password.");
else
Console.WriteLine($" You will be promped to enter a username and a new password.");
string? username = null;
if (args.Length != 2)
{
while (username is null || username == String.Empty)
username = ReadName(" Please enter a username: ");
}
// Check if user by this name exists in DB
User? foundUser = db.Users
.FirstOrDefault(u => u.NormalizedName == username!.ToLower());
if (foundUser == null)
{
Console.WriteLine("Error! User with this name does not exist in the database!");
return;
}
string password = ReadPassword();
string passwordConfirmation = ReadPassword(" Confirm new password: ");
if (!password.Equals(passwordConfirmation))
{
Console.WriteLine("Error! Passwords do not match. Please try again.");
return;
}
foundUser.Password = password;
db.SaveChanges();
}
public void RemoveUser()
{
// Check if any cli arguments have been passed
// Both:
// Shadow removeUser
// and
// Shadow removeUser username
// are supported.
Console.WriteLine($"[Shadow] Remove user");
if (args.Length == 2)
Console.WriteLine($" You will be promped to enter the password.");
else
Console.WriteLine($" You will be promped to enter the username and password.");
string? username = null;
if (args.Length != 2)
{
while (username is null || username == String.Empty)
username = ReadName(" Please enter the username: ");
}
// Check if user by this name exists in DB
User? foundUser = db.Users
.FirstOrDefault(u => u.NormalizedName == username!.ToLower());
if (foundUser == null)
{
Console.WriteLine("Error! User with this name does not exist in the database!");
return;
}
string password = ReadPassword();
string passwordConfirmation = ReadPassword(" Confirm password: ");
if (!password.Equals(passwordConfirmation))
{
Console.WriteLine("Error! Passwords do not match. Please try again.");
return;
}
if (foundUser.Password != password)
{
Console.WriteLine($"Error! Entered password does not match that of \"{username}\"!");
return;
}
bool userDeletionConfirmation = YesNoPrompt($" Do you want to remove \"{username}\" completely?\n" +
$" This cannot be undone! [y/N]: ", false);
if (userDeletionConfirmation) {
// All playlists, interactions should be deleted as well.
List<AlbumInteraction> albumInteractions = db.AlbumInteractions
.Where(ai => ai.User == foundUser)
.ToList();
List<SongInteraction> songInteractions = db.SongInteractions
.Where(si => si.User == foundUser)
.ToList();
foreach(AlbumInteraction ai in albumInteractions)
db.AlbumInteractions.Remove(ai);
foreach (SongInteraction si in songInteractions)
db.SongInteractions.Remove(si);
db.Users.Remove(foundUser);
db.SaveChanges();
Console.WriteLine($"User \"{username}\" and all their data deleted successfully.");
}
else
Console.WriteLine("User not removed.");
}
public string ReadPassword(string prompt = " Enter password (will not be echoed back): ")
{
// https://www.silicloud.com/blog/how-to-hide-content-in-the-console-using-c/
string password = String.Empty;
ConsoleKeyInfo key;
bool exit = false;
Console.Write(prompt);
while (!exit) {
key = Console.ReadKey(true);
// Exit on enter
if (key.Key == ConsoleKey.Enter)
exit = true;
// Clear last character on backspace
else if (key.Key == ConsoleKey.Backspace && password.Length > 0)
password = password.Substring(0, (password.Length - 1));
// Append any other character
else
password += key.KeyChar;
}
Console.WriteLine();
return password;
}
public string? ReadName(string prompt = " Enter username: ")
{
Console.Write(prompt);
string? input = Console.ReadLine();
return input;
}
public bool YesNoPrompt(string prompt, bool? default_value = null)
{
// Checks if user input starts with "y", and if it does, returns true.
// Otherwise checks for "n". If both checks fail, and default_value is null,
// user will be asked repeatedly.
bool exit = false;
bool response = false;
Console.Write(prompt);
string? input = Console.ReadLine();
while (!exit)
{
if (input is not null && input.Length > 0)
if (input.ToLower().StartsWith("y"))
{
response = true;
exit = true;
}
else if (input.ToLower().StartsWith("n"))
{
response = false;
exit = true;
}
else if (default_value is not null)
{
response = (bool)default_value;
exit = true;
}
}
return response;
}
}

View File

@@ -0,0 +1,30 @@
using Microsoft.EntityFrameworkCore;
using Shadow.Data;
using Shadow.Entities;
using System.Text;
namespace Shadow.Controllers;
public class GeneralUseHelpers(ApplicationDbContext db, IConfiguration appsettings)
{
private readonly ApplicationDbContext _db = db;
private readonly IConfiguration _appsettings = appsettings;
//async public Task<User?> GetUserFromEmail(string email)
//{
// return await _db.Users.FirstOrDefaultAsync(e => e.Email == email);
//}
//public string HashWithSHA512(string s)
//{
// using (var sha512 = SHA512.Create())
// {
// byte[] bytes = Encoding.ASCII.GetBytes(s);
// byte[] hash = sha512.ComputeHash(bytes);
// string hashstring = BitConverter.ToString(hash).Replace("-", "").ToLower();
// return hashstring;
// }
//}
}

29
Controllers/Seeder.cs Normal file
View File

@@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using Shadow.Controllers;
using Shadow.Data;
using Shadow.Entities;
namespace Shadow.Controllers;
public class Seeder
{
private readonly ApplicationDbContext db;
private readonly GeneralUseHelpers guhf;
public Seeder(ApplicationDbContext _db, GeneralUseHelpers _guhf)
{
db = _db;
guhf = _guhf;
}
public void Seed()
{
// TODO: Ensure [unknown album], [unknown artist] exist
// TODO: Force add a new user through CLI if no users exist
Console.WriteLine($"You're running Shadow, commit {ThisAssembly.Git.Commit} of branch {ThisAssembly.Git.Branch} ({ThisAssembly.Git.CommitDate})\n");
}
}

View File

@@ -1,33 +0,0 @@
using Microsoft.AspNetCore.Mvc;
using SharpExifTool;
namespace Shadow.Controllers
{
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries =
[
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
];
[HttpGet(Name = "GetWeatherForecast")]
//public async Task<IEnumerable<WeatherForecast>> Get()
public async Task<WeatherForecast> Get()
{
using (var exiftool = new ExifTool())
{
//var test = await exiftool.ExtractAllMetadataAsync(filename: "C:\\Path\\to\\file.flac/.mp3/.m4a/.ogg");
return new WeatherForecast();
}
//return Enumerable.Range(1, 5).Select(index => new WeatherForecast
//{
// Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
// TemperatureC = Random.Shared.Next(-20, 55),
// Summary = Summaries[Random.Shared.Next(Summaries.Length)]
//})
//.ToArray();
}
}
}

View File

@@ -0,0 +1,75 @@
using Microsoft.EntityFrameworkCore;
using Shadow.Entities;
using System.Diagnostics.Metrics;
using System.Reflection.Emit;
namespace Shadow.Data;
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
// EFC ORM Setup
public DbSet<Album> Albums => Set<Album>();
public DbSet<AlbumInteraction> AlbumInteractions => Set<AlbumInteraction>();
public DbSet<Artist> Artists => Set<Artist>();
public DbSet<Genre> Genres => Set<Genre>();
public DbSet<GenreSong> GenreSongs => Set<GenreSong>();
public DbSet<Image> Images => Set<Image>();
public DbSet<Playlist> Playlists => Set<Playlist>();
public DbSet<PlaylistSong> PlaylistSongs => Set<PlaylistSong>();
public DbSet<PlaylistUser> PlaylistUsers => Set<PlaylistUser>();
public DbSet<Radio> Radios => Set<Radio>();
public DbSet<Song> Songs => Set<Song>();
public DbSet<SongInteraction> SongInteractions => Set<SongInteraction>();
public DbSet<User> Users => Set<User>();
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// Composite keys setup
builder.Entity<AlbumInteraction>()
.HasKey(ai => new { ai.AlbumId, ai.UserId });
builder.Entity<GenreSong>()
.HasKey(gs => new { gs.GenreId, gs.SongId });
builder.Entity<PlaylistSong>()
.HasKey(ps => new { ps.PlaylistId, ps.SongId });
builder.Entity<PlaylistUser>()
.HasKey(pu => new { pu.PlaylistId, pu.UserId });
builder.Entity<SongInteraction>()
.HasKey(si => new { si.SongId, si.UserId });
// Constraints (UQ)
builder.Entity<Album>(a => {
a.HasIndex(a => a.Uri).IsUnique();
});
builder.Entity<Artist>(a => {
a.HasIndex(a => a.NormalizedName).IsUnique();
});
builder.Entity<Genre>(g => {
g.HasIndex(g => g.NormalizedName).IsUnique();
});
builder.Entity<Playlist>(p => {
p.HasIndex(p => p.Uri).IsUnique();
});
builder.Entity<Radio>(r => {
r.HasIndex(r => r.NormalizedName).IsUnique();
});
builder.Entity<Song>(s => {
s.HasIndex(s => s.Uri).IsUnique();
});
builder.Entity<User>(u => {
u.HasIndex(u => u.Name).IsUnique();
// u.HasIndex(u => u.Email).IsUnique();
});
// Force dependent side on Image
builder.Entity<Image>()
.HasOne(i => i.Song)
.WithOne(s => s.Image)
.HasForeignKey<Image>(i => i.SongId);
}
}

16
Entities/Album.cs Normal file
View File

@@ -0,0 +1,16 @@
namespace Shadow.Entities;
public class Album
{
required public int Id { get; set; }
public required string Name { get; set; }
public required string Uri { get; set; }
public int State { get; set; } = 0;
public int? ArtistId { get; set; } = null;
public Artist? Artist { get; set; } = null;
public List<Song> Songs { get; set; } = [];
public bool IsOk() => State == 0;
public bool IsOrphaned() => State == 1;
public bool IsArchived() => State == 2;
}

View File

@@ -0,0 +1,12 @@
namespace Shadow.Entities;
public class AlbumInteraction
{
required public int Id { get; set; }
required public int AlbumId { get; set; }
required public int UserId { get; set; }
public DateTime? PlayDate { get; set; } = null;
public bool Starred { get; set; } = false;
required public Album Album { get; set; }
required public User User { get; set; }
}

12
Entities/Artist.cs Normal file
View File

@@ -0,0 +1,12 @@
namespace Shadow.Entities;
public class Artist
{
required public int Id { get; set; }
required public string Name { get; set; }
required public string NormalizedName { get; set; }
// List of artists songs
public List<Song> Songs { get; set; } = [];
// List of artists albums
public List<Album> Albums { get; set; } = [];
}

10
Entities/Genre.cs Normal file
View File

@@ -0,0 +1,10 @@
namespace Shadow.Entities;
public class Genre
{
required public int Id { get; set; }
required public string Name { get; set; }
required public string NormalizedName { get; set; }
// List of genre-song pairs of a given genre
public List<GenreSong> GenreSongPair { get; set; } = [];
}

10
Entities/GenreSong.cs Normal file
View File

@@ -0,0 +1,10 @@
namespace Shadow.Entities;
public class GenreSong
{
// Composite keys
required public int GenreId { get; set; }
required public int SongId { get; set; }
required public Genre Genre { get; set; }
required public Song Song { get; set; }
}

14
Entities/Image.cs Normal file
View File

@@ -0,0 +1,14 @@
namespace Shadow.Entities;
public class Image
{
required public int Id { get; set; }
required public string Uri { get; set; }
public int State { get; set; }
public int? SongId { get; set; } = null;
public Song? Song { get; set; } = null;
public bool IsAvailable() => State == 0;
public bool IsOrphaned() => State == 1;
}

25
Entities/Playlist.cs Normal file
View File

@@ -0,0 +1,25 @@
namespace Shadow.Entities;
public class Playlist
{
required public int Id { get; set; }
required public string Name { get; set; }
required public string Uri { get; set; }
required public string Description { get; set; }
required public int CreatorId { get; set; } // UserId?
required public User Creator { get; set; }
public List<PlaylistUser> AuthorizedPlaylistUsers { get; set; } = [];
public bool CanAccess(User u) {
bool isUserPresent = false;
foreach (PlaylistUser pu in AuthorizedPlaylistUsers)
{
if (pu.User == u)
{
isUserPresent = true;
break;
}
}
return isUserPresent;
}
}

12
Entities/PlaylistSong.cs Normal file
View File

@@ -0,0 +1,12 @@
namespace Shadow.Entities;
public class PlaylistSong
{
// Composite keys
required public int PlaylistId { get; set; }
required public int SongId { get; set; }
required public int Index { get; set; }
required public Playlist Playlist { get; set; }
required public Song Song { get; set; }
}

9
Entities/PlaylistUser.cs Normal file
View File

@@ -0,0 +1,9 @@
namespace Shadow.Entities;
public class PlaylistUser
{
required public int PlaylistId { get; set; }
required public int UserId { get; set; }
required public Playlist Playlist { get; set; }
required public User User { get; set; }
}

12
Entities/Radio.cs Normal file
View File

@@ -0,0 +1,12 @@
namespace Shadow.Entities;
public class Radio
{
required public int Id { get; set; }
required public string Name { get; set; }
required public string NormalizedName { get; set; }
public string? Homepage { get; set; } = null;
required public string Url { get; set; }
required public int UserId { get; set; }
required public User User { get; set; }
}

59
Entities/Song.cs Normal file
View File

@@ -0,0 +1,59 @@
using System.Numerics;
namespace Shadow.Entities;
public class Song
{
public int Id { get; set; }
required public string Title { get; set; }
required public string Uri { get; set; }
required public string Filepath { get; set; }
public int State { get; set; }
required public string Filetype { get; set; }
public DateTime Date { get; set; }
public int Duration { get; set; }
public int Bitrate { get; set; }
public int Size { get; set; }
public string? Comment { get; set; } = null;
public int Channels { get; set; } = 2;
public int SamplingRate { get; set; }
public int? BitDepth { get; set; } = null;
public int Index { get; set; }
public int? TrackNumber { get; set; } = null;
public int? DiscNumber { get; set; } = null;
required public int AlbumId { get; set; }
required public int ArtistId { get; set; }
public int? ImageId { get; set; } = null;
// Songs without an album entry shall default to "[Unnamed album]".
required public Album Album { get; set; }
// Same for artists, with "[Unknown artist]".
required public Artist Artist { get; set; }
public List<GenreSong> GenreSongPair { get; set; } = [];
public Image? Image { get; set; } = null;
public bool IsOk() => State == 0;
public bool IsOrphaned() => State == 1;
public bool IsArchived() => State == 2;
public string GetMimeType()
{
// Might be nice to rewrite like this later:
// https://stackoverflow.com/a/47601452
string mimeType = String.Empty;
switch (Filetype)
{
case "flac":
mimeType = "audio/flac"; break;
case "m4a":
mimeType = "audio/mp4"; break;
case "mp3":
mimeType = "audio/mpeg"; break;
case "ogg":
mimeType = "audio/ogg"; break;
case "wav":
mimeType = "audio/x-wav"; break;
default:
mimeType = "audio/unknown"; break;
}
return mimeType;
}
}

View File

@@ -0,0 +1,15 @@
namespace Shadow.Entities;
public class SongInteraction
{
public int Id { get; set; }
required public int SongId { get; set; }
required public int UserId { get; set; }
DateTime? PlayDate { get; set; } = null;
public int PlayCount { get; set; } = 0;
public bool? Starred { get; set; } = false;
public int Rating { get; set; } = 0;
public Song? Song { get; set; }
public User? User { get; set; }
}

18
Entities/User.cs Normal file
View File

@@ -0,0 +1,18 @@
namespace Shadow.Entities;
public class User
{
public int Id { get; set; }
required public string Name { get; set; }
required public string NormalizedName { get; set; }
// required public string Email { get; set; } // Currently not used
required public string Password { get; set; }
public int Role { get; set; } = 1;
public List<Playlist> Playlists { get; set; } = [];
public List<AlbumInteraction> AlbumInteractions { get; set; } = [];
public List<SongInteraction> SongInteractions { get; set; } = [];
public bool IsAdmin() => Role == 0;
public bool IsUnpriviledgedUser() => Role == 1;
}

View File

@@ -0,0 +1,608 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Shadow.Data;
#nullable disable
namespace Shadow.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20251209000751_InitialMigration")]
partial class InitialMigration
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Shadow.Entities.Album", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int?>("ArtistId")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("State")
.HasColumnType("integer");
b.Property<string>("Uri")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("ArtistId");
b.HasIndex("Uri")
.IsUnique();
b.ToTable("Albums");
});
modelBuilder.Entity("Shadow.Entities.AlbumInteraction", b =>
{
b.Property<int>("AlbumId")
.HasColumnType("integer");
b.Property<int>("UserId")
.HasColumnType("integer");
b.Property<int>("Id")
.HasColumnType("integer");
b.Property<DateTime?>("PlayDate")
.HasColumnType("timestamp with time zone");
b.Property<bool>("Starred")
.HasColumnType("boolean");
b.HasKey("AlbumId", "UserId");
b.HasIndex("UserId");
b.ToTable("AlbumInteractions");
});
modelBuilder.Entity("Shadow.Entities.Artist", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("NormalizedName")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique();
b.ToTable("Artists");
});
modelBuilder.Entity("Shadow.Entities.Genre", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("NormalizedName")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique();
b.ToTable("Genres");
});
modelBuilder.Entity("Shadow.Entities.GenreSong", b =>
{
b.Property<int>("GenreId")
.HasColumnType("integer");
b.Property<int>("SongId")
.HasColumnType("integer");
b.HasKey("GenreId", "SongId");
b.HasIndex("SongId");
b.ToTable("GenreSongs");
});
modelBuilder.Entity("Shadow.Entities.Image", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int?>("SongId")
.HasColumnType("integer");
b.Property<int>("State")
.HasColumnType("integer");
b.Property<string>("Uri")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("SongId")
.IsUnique();
b.ToTable("Images");
});
modelBuilder.Entity("Shadow.Entities.Playlist", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("CreatorId")
.HasColumnType("integer");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Uri")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CreatorId");
b.HasIndex("Uri")
.IsUnique();
b.ToTable("Playlists");
});
modelBuilder.Entity("Shadow.Entities.PlaylistSong", b =>
{
b.Property<int>("PlaylistId")
.HasColumnType("integer");
b.Property<int>("SongId")
.HasColumnType("integer");
b.Property<int>("Index")
.HasColumnType("integer");
b.HasKey("PlaylistId", "SongId");
b.HasIndex("SongId");
b.ToTable("PlaylistSongs");
});
modelBuilder.Entity("Shadow.Entities.PlaylistUser", b =>
{
b.Property<int>("PlaylistId")
.HasColumnType("integer");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("PlaylistId", "UserId");
b.HasIndex("UserId");
b.ToTable("PlaylistUsers");
});
modelBuilder.Entity("Shadow.Entities.Radio", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Homepage")
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("NormalizedName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("text");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique();
b.HasIndex("UserId");
b.ToTable("Radios");
});
modelBuilder.Entity("Shadow.Entities.Song", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("AlbumId")
.HasColumnType("integer");
b.Property<int>("ArtistId")
.HasColumnType("integer");
b.Property<int?>("BitDepth")
.HasColumnType("integer");
b.Property<int>("Bitrate")
.HasColumnType("integer");
b.Property<int>("Channels")
.HasColumnType("integer");
b.Property<string>("Comment")
.HasColumnType("text");
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<int?>("DiscNumber")
.HasColumnType("integer");
b.Property<int>("Duration")
.HasColumnType("integer");
b.Property<string>("Filepath")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Filetype")
.IsRequired()
.HasColumnType("text");
b.Property<int?>("ImageId")
.HasColumnType("integer");
b.Property<int>("Index")
.HasColumnType("integer");
b.Property<int>("SamplingRate")
.HasColumnType("integer");
b.Property<int>("Size")
.HasColumnType("integer");
b.Property<int>("State")
.HasColumnType("integer");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.Property<int?>("TrackNumber")
.HasColumnType("integer");
b.Property<string>("Uri")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("AlbumId");
b.HasIndex("ArtistId");
b.HasIndex("Uri")
.IsUnique();
b.ToTable("Songs");
});
modelBuilder.Entity("Shadow.Entities.SongInteraction", b =>
{
b.Property<int>("SongId")
.HasColumnType("integer");
b.Property<int>("UserId")
.HasColumnType("integer");
b.Property<int>("Id")
.HasColumnType("integer");
b.Property<int>("PlayCount")
.HasColumnType("integer");
b.Property<int>("Rating")
.HasColumnType("integer");
b.Property<bool?>("Starred")
.HasColumnType("boolean");
b.HasKey("SongId", "UserId");
b.HasIndex("UserId");
b.ToTable("SongInteractions");
});
modelBuilder.Entity("Shadow.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("NormalizedName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Role")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("Shadow.Entities.Album", b =>
{
b.HasOne("Shadow.Entities.Artist", "Artist")
.WithMany("Albums")
.HasForeignKey("ArtistId");
b.Navigation("Artist");
});
modelBuilder.Entity("Shadow.Entities.AlbumInteraction", b =>
{
b.HasOne("Shadow.Entities.Album", "Album")
.WithMany()
.HasForeignKey("AlbumId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Shadow.Entities.User", "User")
.WithMany("AlbumInteractions")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Album");
b.Navigation("User");
});
modelBuilder.Entity("Shadow.Entities.GenreSong", b =>
{
b.HasOne("Shadow.Entities.Genre", "Genre")
.WithMany("GenreSongPair")
.HasForeignKey("GenreId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Shadow.Entities.Song", "Song")
.WithMany("GenreSongPair")
.HasForeignKey("SongId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Genre");
b.Navigation("Song");
});
modelBuilder.Entity("Shadow.Entities.Image", b =>
{
b.HasOne("Shadow.Entities.Song", "Song")
.WithOne("Image")
.HasForeignKey("Shadow.Entities.Image", "SongId");
b.Navigation("Song");
});
modelBuilder.Entity("Shadow.Entities.Playlist", b =>
{
b.HasOne("Shadow.Entities.User", "Creator")
.WithMany("Playlists")
.HasForeignKey("CreatorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Creator");
});
modelBuilder.Entity("Shadow.Entities.PlaylistSong", b =>
{
b.HasOne("Shadow.Entities.Playlist", "Playlist")
.WithMany()
.HasForeignKey("PlaylistId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Shadow.Entities.Song", "Song")
.WithMany()
.HasForeignKey("SongId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Playlist");
b.Navigation("Song");
});
modelBuilder.Entity("Shadow.Entities.PlaylistUser", b =>
{
b.HasOne("Shadow.Entities.Playlist", "Playlist")
.WithMany("AuthorizedPlaylistUsers")
.HasForeignKey("PlaylistId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Shadow.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Playlist");
b.Navigation("User");
});
modelBuilder.Entity("Shadow.Entities.Radio", b =>
{
b.HasOne("Shadow.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Shadow.Entities.Song", b =>
{
b.HasOne("Shadow.Entities.Album", "Album")
.WithMany("Songs")
.HasForeignKey("AlbumId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Shadow.Entities.Artist", "Artist")
.WithMany("Songs")
.HasForeignKey("ArtistId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Album");
b.Navigation("Artist");
});
modelBuilder.Entity("Shadow.Entities.SongInteraction", b =>
{
b.HasOne("Shadow.Entities.Song", "Song")
.WithMany()
.HasForeignKey("SongId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Shadow.Entities.User", "User")
.WithMany("SongInteractions")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Song");
b.Navigation("User");
});
modelBuilder.Entity("Shadow.Entities.Album", b =>
{
b.Navigation("Songs");
});
modelBuilder.Entity("Shadow.Entities.Artist", b =>
{
b.Navigation("Albums");
b.Navigation("Songs");
});
modelBuilder.Entity("Shadow.Entities.Genre", b =>
{
b.Navigation("GenreSongPair");
});
modelBuilder.Entity("Shadow.Entities.Playlist", b =>
{
b.Navigation("AuthorizedPlaylistUsers");
});
modelBuilder.Entity("Shadow.Entities.Song", b =>
{
b.Navigation("GenreSongPair");
b.Navigation("Image");
});
modelBuilder.Entity("Shadow.Entities.User", b =>
{
b.Navigation("AlbumInteractions");
b.Navigation("Playlists");
b.Navigation("SongInteractions");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,458 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Shadow.Migrations
{
/// <inheritdoc />
public partial class InitialMigration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Artists",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "text", nullable: false),
NormalizedName = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Artists", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Genres",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "text", nullable: false),
NormalizedName = table.Column<string>(type: "text", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Genres", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Users",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "text", nullable: false),
NormalizedName = table.Column<string>(type: "text", nullable: false),
Password = table.Column<string>(type: "text", nullable: false),
Role = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Users", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Albums",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "text", nullable: false),
Uri = table.Column<string>(type: "text", nullable: false),
State = table.Column<int>(type: "integer", nullable: false),
ArtistId = table.Column<int>(type: "integer", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Albums", x => x.Id);
table.ForeignKey(
name: "FK_Albums_Artists_ArtistId",
column: x => x.ArtistId,
principalTable: "Artists",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "Playlists",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "text", nullable: false),
Uri = table.Column<string>(type: "text", nullable: false),
Description = table.Column<string>(type: "text", nullable: false),
CreatorId = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Playlists", x => x.Id);
table.ForeignKey(
name: "FK_Playlists_Users_CreatorId",
column: x => x.CreatorId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Radios",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Name = table.Column<string>(type: "text", nullable: false),
NormalizedName = table.Column<string>(type: "text", nullable: false),
Homepage = table.Column<string>(type: "text", nullable: true),
Url = table.Column<string>(type: "text", nullable: false),
UserId = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Radios", x => x.Id);
table.ForeignKey(
name: "FK_Radios_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "AlbumInteractions",
columns: table => new
{
AlbumId = table.Column<int>(type: "integer", nullable: false),
UserId = table.Column<int>(type: "integer", nullable: false),
Id = table.Column<int>(type: "integer", nullable: false),
PlayDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
Starred = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AlbumInteractions", x => new { x.AlbumId, x.UserId });
table.ForeignKey(
name: "FK_AlbumInteractions_Albums_AlbumId",
column: x => x.AlbumId,
principalTable: "Albums",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_AlbumInteractions_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Songs",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Title = table.Column<string>(type: "text", nullable: false),
Uri = table.Column<string>(type: "text", nullable: false),
Filepath = table.Column<string>(type: "text", nullable: false),
State = table.Column<int>(type: "integer", nullable: false),
Filetype = table.Column<string>(type: "text", nullable: false),
Date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
Duration = table.Column<int>(type: "integer", nullable: false),
Bitrate = table.Column<int>(type: "integer", nullable: false),
Size = table.Column<int>(type: "integer", nullable: false),
Comment = table.Column<string>(type: "text", nullable: true),
Channels = table.Column<int>(type: "integer", nullable: false),
SamplingRate = table.Column<int>(type: "integer", nullable: false),
BitDepth = table.Column<int>(type: "integer", nullable: true),
Index = table.Column<int>(type: "integer", nullable: false),
TrackNumber = table.Column<int>(type: "integer", nullable: true),
DiscNumber = table.Column<int>(type: "integer", nullable: true),
AlbumId = table.Column<int>(type: "integer", nullable: false),
ArtistId = table.Column<int>(type: "integer", nullable: false),
ImageId = table.Column<int>(type: "integer", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Songs", x => x.Id);
table.ForeignKey(
name: "FK_Songs_Albums_AlbumId",
column: x => x.AlbumId,
principalTable: "Albums",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_Songs_Artists_ArtistId",
column: x => x.ArtistId,
principalTable: "Artists",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "PlaylistUsers",
columns: table => new
{
PlaylistId = table.Column<int>(type: "integer", nullable: false),
UserId = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PlaylistUsers", x => new { x.PlaylistId, x.UserId });
table.ForeignKey(
name: "FK_PlaylistUsers_Playlists_PlaylistId",
column: x => x.PlaylistId,
principalTable: "Playlists",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_PlaylistUsers_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "GenreSongs",
columns: table => new
{
GenreId = table.Column<int>(type: "integer", nullable: false),
SongId = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_GenreSongs", x => new { x.GenreId, x.SongId });
table.ForeignKey(
name: "FK_GenreSongs_Genres_GenreId",
column: x => x.GenreId,
principalTable: "Genres",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_GenreSongs_Songs_SongId",
column: x => x.SongId,
principalTable: "Songs",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Images",
columns: table => new
{
Id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
Uri = table.Column<string>(type: "text", nullable: false),
State = table.Column<int>(type: "integer", nullable: false),
SongId = table.Column<int>(type: "integer", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Images", x => x.Id);
table.ForeignKey(
name: "FK_Images_Songs_SongId",
column: x => x.SongId,
principalTable: "Songs",
principalColumn: "Id");
});
migrationBuilder.CreateTable(
name: "PlaylistSongs",
columns: table => new
{
PlaylistId = table.Column<int>(type: "integer", nullable: false),
SongId = table.Column<int>(type: "integer", nullable: false),
Index = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_PlaylistSongs", x => new { x.PlaylistId, x.SongId });
table.ForeignKey(
name: "FK_PlaylistSongs_Playlists_PlaylistId",
column: x => x.PlaylistId,
principalTable: "Playlists",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_PlaylistSongs_Songs_SongId",
column: x => x.SongId,
principalTable: "Songs",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "SongInteractions",
columns: table => new
{
SongId = table.Column<int>(type: "integer", nullable: false),
UserId = table.Column<int>(type: "integer", nullable: false),
Id = table.Column<int>(type: "integer", nullable: false),
PlayCount = table.Column<int>(type: "integer", nullable: false),
Starred = table.Column<bool>(type: "boolean", nullable: true),
Rating = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SongInteractions", x => new { x.SongId, x.UserId });
table.ForeignKey(
name: "FK_SongInteractions_Songs_SongId",
column: x => x.SongId,
principalTable: "Songs",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_SongInteractions_Users_UserId",
column: x => x.UserId,
principalTable: "Users",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_AlbumInteractions_UserId",
table: "AlbumInteractions",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_Albums_ArtistId",
table: "Albums",
column: "ArtistId");
migrationBuilder.CreateIndex(
name: "IX_Albums_Uri",
table: "Albums",
column: "Uri",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Artists_NormalizedName",
table: "Artists",
column: "NormalizedName",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Genres_NormalizedName",
table: "Genres",
column: "NormalizedName",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_GenreSongs_SongId",
table: "GenreSongs",
column: "SongId");
migrationBuilder.CreateIndex(
name: "IX_Images_SongId",
table: "Images",
column: "SongId",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Playlists_CreatorId",
table: "Playlists",
column: "CreatorId");
migrationBuilder.CreateIndex(
name: "IX_Playlists_Uri",
table: "Playlists",
column: "Uri",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_PlaylistSongs_SongId",
table: "PlaylistSongs",
column: "SongId");
migrationBuilder.CreateIndex(
name: "IX_PlaylistUsers_UserId",
table: "PlaylistUsers",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_Radios_NormalizedName",
table: "Radios",
column: "NormalizedName",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Radios_UserId",
table: "Radios",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_SongInteractions_UserId",
table: "SongInteractions",
column: "UserId");
migrationBuilder.CreateIndex(
name: "IX_Songs_AlbumId",
table: "Songs",
column: "AlbumId");
migrationBuilder.CreateIndex(
name: "IX_Songs_ArtistId",
table: "Songs",
column: "ArtistId");
migrationBuilder.CreateIndex(
name: "IX_Songs_Uri",
table: "Songs",
column: "Uri",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Users_Name",
table: "Users",
column: "Name",
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AlbumInteractions");
migrationBuilder.DropTable(
name: "GenreSongs");
migrationBuilder.DropTable(
name: "Images");
migrationBuilder.DropTable(
name: "PlaylistSongs");
migrationBuilder.DropTable(
name: "PlaylistUsers");
migrationBuilder.DropTable(
name: "Radios");
migrationBuilder.DropTable(
name: "SongInteractions");
migrationBuilder.DropTable(
name: "Genres");
migrationBuilder.DropTable(
name: "Playlists");
migrationBuilder.DropTable(
name: "Songs");
migrationBuilder.DropTable(
name: "Users");
migrationBuilder.DropTable(
name: "Albums");
migrationBuilder.DropTable(
name: "Artists");
}
}
}

View File

@@ -0,0 +1,605 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using Shadow.Data;
#nullable disable
namespace Shadow.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
partial class ApplicationDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.0")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Shadow.Entities.Album", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int?>("ArtistId")
.HasColumnType("integer");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<int>("State")
.HasColumnType("integer");
b.Property<string>("Uri")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("ArtistId");
b.HasIndex("Uri")
.IsUnique();
b.ToTable("Albums");
});
modelBuilder.Entity("Shadow.Entities.AlbumInteraction", b =>
{
b.Property<int>("AlbumId")
.HasColumnType("integer");
b.Property<int>("UserId")
.HasColumnType("integer");
b.Property<int>("Id")
.HasColumnType("integer");
b.Property<DateTime?>("PlayDate")
.HasColumnType("timestamp with time zone");
b.Property<bool>("Starred")
.HasColumnType("boolean");
b.HasKey("AlbumId", "UserId");
b.HasIndex("UserId");
b.ToTable("AlbumInteractions");
});
modelBuilder.Entity("Shadow.Entities.Artist", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("NormalizedName")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique();
b.ToTable("Artists");
});
modelBuilder.Entity("Shadow.Entities.Genre", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("NormalizedName")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique();
b.ToTable("Genres");
});
modelBuilder.Entity("Shadow.Entities.GenreSong", b =>
{
b.Property<int>("GenreId")
.HasColumnType("integer");
b.Property<int>("SongId")
.HasColumnType("integer");
b.HasKey("GenreId", "SongId");
b.HasIndex("SongId");
b.ToTable("GenreSongs");
});
modelBuilder.Entity("Shadow.Entities.Image", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int?>("SongId")
.HasColumnType("integer");
b.Property<int>("State")
.HasColumnType("integer");
b.Property<string>("Uri")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("SongId")
.IsUnique();
b.ToTable("Images");
});
modelBuilder.Entity("Shadow.Entities.Playlist", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("CreatorId")
.HasColumnType("integer");
b.Property<string>("Description")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Uri")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("CreatorId");
b.HasIndex("Uri")
.IsUnique();
b.ToTable("Playlists");
});
modelBuilder.Entity("Shadow.Entities.PlaylistSong", b =>
{
b.Property<int>("PlaylistId")
.HasColumnType("integer");
b.Property<int>("SongId")
.HasColumnType("integer");
b.Property<int>("Index")
.HasColumnType("integer");
b.HasKey("PlaylistId", "SongId");
b.HasIndex("SongId");
b.ToTable("PlaylistSongs");
});
modelBuilder.Entity("Shadow.Entities.PlaylistUser", b =>
{
b.Property<int>("PlaylistId")
.HasColumnType("integer");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("PlaylistId", "UserId");
b.HasIndex("UserId");
b.ToTable("PlaylistUsers");
});
modelBuilder.Entity("Shadow.Entities.Radio", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Homepage")
.HasColumnType("text");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("NormalizedName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Url")
.IsRequired()
.HasColumnType("text");
b.Property<int>("UserId")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique();
b.HasIndex("UserId");
b.ToTable("Radios");
});
modelBuilder.Entity("Shadow.Entities.Song", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<int>("AlbumId")
.HasColumnType("integer");
b.Property<int>("ArtistId")
.HasColumnType("integer");
b.Property<int?>("BitDepth")
.HasColumnType("integer");
b.Property<int>("Bitrate")
.HasColumnType("integer");
b.Property<int>("Channels")
.HasColumnType("integer");
b.Property<string>("Comment")
.HasColumnType("text");
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<int?>("DiscNumber")
.HasColumnType("integer");
b.Property<int>("Duration")
.HasColumnType("integer");
b.Property<string>("Filepath")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Filetype")
.IsRequired()
.HasColumnType("text");
b.Property<int?>("ImageId")
.HasColumnType("integer");
b.Property<int>("Index")
.HasColumnType("integer");
b.Property<int>("SamplingRate")
.HasColumnType("integer");
b.Property<int>("Size")
.HasColumnType("integer");
b.Property<int>("State")
.HasColumnType("integer");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text");
b.Property<int?>("TrackNumber")
.HasColumnType("integer");
b.Property<string>("Uri")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("AlbumId");
b.HasIndex("ArtistId");
b.HasIndex("Uri")
.IsUnique();
b.ToTable("Songs");
});
modelBuilder.Entity("Shadow.Entities.SongInteraction", b =>
{
b.Property<int>("SongId")
.HasColumnType("integer");
b.Property<int>("UserId")
.HasColumnType("integer");
b.Property<int>("Id")
.HasColumnType("integer");
b.Property<int>("PlayCount")
.HasColumnType("integer");
b.Property<int>("Rating")
.HasColumnType("integer");
b.Property<bool?>("Starred")
.HasColumnType("boolean");
b.HasKey("SongId", "UserId");
b.HasIndex("UserId");
b.ToTable("SongInteractions");
});
modelBuilder.Entity("Shadow.Entities.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasColumnType("text");
b.Property<string>("NormalizedName")
.IsRequired()
.HasColumnType("text");
b.Property<string>("Password")
.IsRequired()
.HasColumnType("text");
b.Property<int>("Role")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("Name")
.IsUnique();
b.ToTable("Users");
});
modelBuilder.Entity("Shadow.Entities.Album", b =>
{
b.HasOne("Shadow.Entities.Artist", "Artist")
.WithMany("Albums")
.HasForeignKey("ArtistId");
b.Navigation("Artist");
});
modelBuilder.Entity("Shadow.Entities.AlbumInteraction", b =>
{
b.HasOne("Shadow.Entities.Album", "Album")
.WithMany()
.HasForeignKey("AlbumId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Shadow.Entities.User", "User")
.WithMany("AlbumInteractions")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Album");
b.Navigation("User");
});
modelBuilder.Entity("Shadow.Entities.GenreSong", b =>
{
b.HasOne("Shadow.Entities.Genre", "Genre")
.WithMany("GenreSongPair")
.HasForeignKey("GenreId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Shadow.Entities.Song", "Song")
.WithMany("GenreSongPair")
.HasForeignKey("SongId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Genre");
b.Navigation("Song");
});
modelBuilder.Entity("Shadow.Entities.Image", b =>
{
b.HasOne("Shadow.Entities.Song", "Song")
.WithOne("Image")
.HasForeignKey("Shadow.Entities.Image", "SongId");
b.Navigation("Song");
});
modelBuilder.Entity("Shadow.Entities.Playlist", b =>
{
b.HasOne("Shadow.Entities.User", "Creator")
.WithMany("Playlists")
.HasForeignKey("CreatorId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Creator");
});
modelBuilder.Entity("Shadow.Entities.PlaylistSong", b =>
{
b.HasOne("Shadow.Entities.Playlist", "Playlist")
.WithMany()
.HasForeignKey("PlaylistId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Shadow.Entities.Song", "Song")
.WithMany()
.HasForeignKey("SongId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Playlist");
b.Navigation("Song");
});
modelBuilder.Entity("Shadow.Entities.PlaylistUser", b =>
{
b.HasOne("Shadow.Entities.Playlist", "Playlist")
.WithMany("AuthorizedPlaylistUsers")
.HasForeignKey("PlaylistId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Shadow.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Playlist");
b.Navigation("User");
});
modelBuilder.Entity("Shadow.Entities.Radio", b =>
{
b.HasOne("Shadow.Entities.User", "User")
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Shadow.Entities.Song", b =>
{
b.HasOne("Shadow.Entities.Album", "Album")
.WithMany("Songs")
.HasForeignKey("AlbumId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Shadow.Entities.Artist", "Artist")
.WithMany("Songs")
.HasForeignKey("ArtistId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Album");
b.Navigation("Artist");
});
modelBuilder.Entity("Shadow.Entities.SongInteraction", b =>
{
b.HasOne("Shadow.Entities.Song", "Song")
.WithMany()
.HasForeignKey("SongId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("Shadow.Entities.User", "User")
.WithMany("SongInteractions")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Song");
b.Navigation("User");
});
modelBuilder.Entity("Shadow.Entities.Album", b =>
{
b.Navigation("Songs");
});
modelBuilder.Entity("Shadow.Entities.Artist", b =>
{
b.Navigation("Albums");
b.Navigation("Songs");
});
modelBuilder.Entity("Shadow.Entities.Genre", b =>
{
b.Navigation("GenreSongPair");
});
modelBuilder.Entity("Shadow.Entities.Playlist", b =>
{
b.Navigation("AuthorizedPlaylistUsers");
});
modelBuilder.Entity("Shadow.Entities.Song", b =>
{
b.Navigation("GenreSongPair");
b.Navigation("Image");
});
modelBuilder.Entity("Shadow.Entities.User", b =>
{
b.Navigation("AlbumInteractions");
b.Navigation("Playlists");
b.Navigation("SongInteractions");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -1,12 +1,34 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi;
using Shadow.Controllers;
using Shadow.Data;
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
string connectionString = builder.Configuration.GetConnectionString("DefaultConnection")
?? throw new ArgumentException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
{
options.UseNpgsql(connectionString);
});
builder.Services.AddControllers(options =>
{
// Add XML serializer
options.OutputFormatters.Add(new XmlSerializerOutputFormatter());
}).AddJsonOptions(options =>
{
// Pretty-print JSON
options.JsonSerializerOptions.WriteIndented = true;
// Preserve keys' case
options.JsonSerializerOptions.PropertyNamingPolicy = null;
});
builder.Services.AddOpenApi();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddScoped<GeneralUseHelpers>();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
@@ -41,12 +63,33 @@ builder.Services.AddSwaggerGen(options =>
// [new OpenApiSecuritySchemeReference("bearer", document)] = []
//});
// using System.Reflection;
var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename));
});
var app = builder.Build();
if (args.FirstOrDefault() is not null)
{
// Handle CLI if arguments have been passed.
bool shutdown = false;
using (IServiceScope scope = app.Services.CreateScope())
{
ApplicationDbContext db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
GeneralUseHelpers guhf = scope.ServiceProvider.GetRequiredService<GeneralUseHelpers>();
Cli cli = new(db, guhf, args);
shutdown = await cli.Parse();
}
if (shutdown) return;
}
// Seed database
using (IServiceScope scope = app.Services.CreateScope())
{
ApplicationDbContext db = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
GeneralUseHelpers guhf = scope.ServiceProvider.GetRequiredService<GeneralUseHelpers>();
Seeder seeder = new(db, guhf);
seeder.Seed();
}
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())

View File

@@ -10,6 +10,7 @@
},
"https": {
"commandName": "Project",
"commandLineArgs": "",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},

View File

@@ -19,10 +19,19 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="GitInfo" Version="3.6.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
<PackageReference Include="Npgsql" Version="10.0.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
<PackageReference Include="SharpExifTool" Version="13.32.0.1" />
<PackageReference Include="SkiaSharp" Version="3.119.1" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="3.119.1" />
@@ -32,4 +41,10 @@
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.0.1" />
</ItemGroup>
<ItemGroup>
<Folder Include="Migrations\" />
<Folder Include="Mapping\" />
<Folder Include="DTOs\" />
</ItemGroup>
</Project>

View File

@@ -1,13 +0,0 @@
namespace Shadow
{
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}
}