218 lines
8.3 KiB
C#
218 lines
8.3 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using Shadow.Data;
|
|
using Shadow.Entities;
|
|
|
|
namespace Shadow.Tools;
|
|
public class Seeder
|
|
{
|
|
private readonly ApplicationDbContext db;
|
|
private readonly GeneralUseHelpers guhf;
|
|
|
|
public Seeder(ApplicationDbContext _db, GeneralUseHelpers _guhf)
|
|
{
|
|
db = _db;
|
|
guhf = _guhf;
|
|
}
|
|
public bool Seed()
|
|
{
|
|
// Will return true if application should be shut down.
|
|
// Otherwise returns false.
|
|
bool shutdown = false;
|
|
|
|
Console.WriteLine($"You're running Shadow, commit {ThisAssembly.Git.Commit} of branch {ThisAssembly.Git.Branch} ({ThisAssembly.Git.CommitDate})\n");
|
|
|
|
// Check migration status. If we can't proceed, exit now.
|
|
bool migrationSucceeded = EnsureMigrations(db);
|
|
if (!migrationSucceeded) return true;
|
|
|
|
// Check if this is a clean, first run. If so, run the setup wizard.
|
|
Global? lastVersion = db.Globals.FirstOrDefault(c => c.Key == "lastVersion");
|
|
Global? runs = db.Globals.FirstOrDefault(g => g.Key == "runs");
|
|
if (lastVersion is null)
|
|
{
|
|
shutdown = true;
|
|
bool setupSuccessfull = false;
|
|
Console.WriteLine("This seems to be a fresh install. Running setup wizard for you.\n");
|
|
Cli cli = new(db, guhf, []);
|
|
setupSuccessfull = cli.SetupWizard();
|
|
|
|
if (setupSuccessfull)
|
|
{
|
|
// Cli.SetupWizard() takes care of musicLibraryPath and musicThumbnailPath
|
|
lastVersion = new Global() { Key = "lastVersion", Value = ThisAssembly.Git.Commit };
|
|
runs = new Global() { Key = "runs", Value = "0" };
|
|
Global lastVersionDate = new Global() { Key = "lastVersionDate", Value = ThisAssembly.Git.CommitDate };
|
|
Global libraryState = new Global() { Key = "libraryState", Value = "00000000000000000000000000000000" };
|
|
db.Globals.AddRange(lastVersion, lastVersionDate, runs, libraryState);
|
|
}
|
|
else
|
|
Console.WriteLine("Setup wizard failed. Please try running it again using \"Shadow setupWizard\".");
|
|
|
|
db.SaveChanges();
|
|
return shutdown;
|
|
}
|
|
|
|
// Check if running a newer (different) version
|
|
if (lastVersion.Value != ThisAssembly.Git.Commit)
|
|
{
|
|
Global lastVersionDate = db.Globals.FirstOrDefault(c => c.Key == "lastVersionDate")
|
|
?? new Global() { Key = "lastVersionDate", Value = ThisAssembly.Git.CommitDate };
|
|
|
|
if (String.Compare(lastVersionDate.Value, ThisAssembly.Git.CommitDate) > 0)
|
|
{
|
|
// User is running an earlier version of the application.
|
|
Console.WriteLine("Downgrade detected! Waiting 30 seconds.\n" +
|
|
"Please consider stopping Shadow in order to avoid accidental data loss!\n");
|
|
Thread.Sleep(30_000);
|
|
}
|
|
else
|
|
Console.WriteLine("Upgrade detected. Make sure you're using the most recent migrations.\n" +
|
|
"If not, apply them automatically or with `dotnet ef database update`.\n");
|
|
|
|
lastVersion.Value = ThisAssembly.Git.Commit;
|
|
lastVersionDate.Value = ThisAssembly.Git.CommitDate;
|
|
|
|
db.UpdateRange(lastVersion, lastVersionDate);
|
|
}
|
|
|
|
// Check if any user/admin exist. Display appropriate warnings if not.
|
|
int userCount = db.Users.Count();
|
|
if (userCount == 0)
|
|
Console.WriteLine("[Warn]: No user accounts found. Running a server no one can access! Consider creating an account with `Shadow addUser`.\n");
|
|
|
|
int adminCount = db.Users.Count(u => u.Role == 0); // equivalent to u.IsAdmin()
|
|
if (adminCount == 0 && userCount > 0)
|
|
Console.WriteLine("[Warn]: No admin accounts exist. Consider creating one with `Shadow addUser`.\n");
|
|
|
|
// Ensure [Unknown Album], [Unknown Artist] exist
|
|
Album unknownAlbum = db.Albums.FirstOrDefault(a => a.Name == "[Unknown Album]") ?? new Album()
|
|
{
|
|
Name = "[Unknown Album]",
|
|
Uri = "00000000000000000000000000000000"
|
|
};
|
|
|
|
Artist unknownArtist = db.Artists.FirstOrDefault(a => a.Name == "[Unknown Artist]") ?? new Artist()
|
|
{
|
|
Name = "[Unknown Artist]",
|
|
NormalizedName = "[unknown artist]"
|
|
};
|
|
|
|
runs ??= new Global() { Key = "runs", Value = "0" };
|
|
if (int.TryParse(runs.Value, out int runsInt))
|
|
runs.Value = $"{runsInt + 1}";
|
|
|
|
// UpdateRange works both as an Add and Update.
|
|
db.UpdateRange(unknownAlbum, unknownArtist, runs);
|
|
db.SaveChanges();
|
|
|
|
return shutdown;
|
|
|
|
}
|
|
|
|
public static bool EnsureMigrations(ApplicationDbContext dbContext)
|
|
{
|
|
// Returns true if migrations have been successfully applied.
|
|
// Otherwise returns false.
|
|
bool migrationSuccess = false;
|
|
List<string> availableMigrations = dbContext.Database.GetMigrations().ToList();
|
|
List<string> appliedMigrations = dbContext.Database.GetAppliedMigrations().ToList();
|
|
|
|
bool hasMissingMigrations = availableMigrations.Count > appliedMigrations.Count;
|
|
bool doMigrationsMatch = availableMigrations.SequenceEqual(appliedMigrations);
|
|
if (hasMissingMigrations)
|
|
{
|
|
bool userMigrationConsent = false;
|
|
Console.WriteLine("\n" +
|
|
"========================================\n" +
|
|
"[Warn] Database migrations missing!\n" +
|
|
"========================================");
|
|
if (appliedMigrations.Count == 0)
|
|
{
|
|
Console.WriteLine( "Empty database detected. Applying migrations is recommended, and required if this is the first run of Shadow.");
|
|
userMigrationConsent = Cli.YesNoPrompt("Do you want to apply migrations automatically? [Y/n]: ", true);
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"Detected existing {appliedMigrations.Count} migrations. Backing up the database before applying migrations is recommended.");
|
|
userMigrationConsent = Cli.YesNoPrompt("Do you want to apply migrations automatically? [y/N]: ", false);
|
|
}
|
|
|
|
// Do we have user permission to perform migration?
|
|
if (userMigrationConsent)
|
|
{
|
|
dbContext.Database.Migrate();
|
|
dbContext.SaveChanges();
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine("Exiting...");
|
|
return migrationSuccess;
|
|
}
|
|
|
|
// Check if migrations match afterwards
|
|
List<string> newlyAppliedMigrations = dbContext.Database.GetAppliedMigrations().ToList();
|
|
bool doNewlyAppliedMigrationsMatch = availableMigrations.SequenceEqual(newlyAppliedMigrations);
|
|
if (doNewlyAppliedMigrationsMatch)
|
|
{
|
|
Console.WriteLine("\n" +
|
|
"==============================\n" +
|
|
"Migration success!\n" +
|
|
"==============================\n");
|
|
migrationSuccess = true;
|
|
}
|
|
else
|
|
{
|
|
// An error ocurred when applying migrations automatically
|
|
Console.WriteLine("\n" +
|
|
"==================================================\n" +
|
|
"[Error] Failed applying migrations automatically!\n" +
|
|
"==================================================\n");
|
|
Console.WriteLine("The list of newly applied migrations does not match the list of planned migrations.\n" +
|
|
"Shadow cannot operate with missing migrations. Please manually revise the database structure and create backups before altering it.\n");
|
|
migrationSuccess = false;
|
|
}
|
|
}
|
|
else migrationSuccess = true;
|
|
|
|
return migrationSuccess;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if the library needs a full rescan
|
|
/// </summary>
|
|
/// <returns>True if full rescan is needed</returns>
|
|
/// <exception cref="MissingFieldException">Thrown when either last library state, path to music library or cache is unknown</exception>
|
|
public async Task<bool> ScanPrefetchAsync()
|
|
{
|
|
bool scanNecessary = false;
|
|
Global? lastLibraryState = await db.Globals.FirstOrDefaultAsync(g => g.Key == "libraryState");
|
|
Global? libraryPath = await db.Globals.FirstOrDefaultAsync(g => g.Key == "musicLibraryPath");
|
|
Global? cachePath = await db.Globals.FirstOrDefaultAsync(g => g.Key == "musicThumbnailPath");
|
|
|
|
if (libraryPath is null || cachePath is null || lastLibraryState is null)
|
|
{
|
|
throw new MissingFieldException("[Error] Missing libraryState, musicLibraryPath, musicThumbnailPath. Please rerun the setup wizard with `Shadow setupWizard`.");
|
|
}
|
|
LibraryWatcher lw = new(libraryPath.Value!, [cachePath.Value!], db);
|
|
|
|
// Get library contents
|
|
List<string> currentLibraryStateList = await lw.GetAllMultimediaAsync();
|
|
// Compute their hash
|
|
string currentLibraryStateString = string.Join("\n", currentLibraryStateList);
|
|
string currentLibraryState = MetadataExtractor.GetStringMD5(currentLibraryStateString);
|
|
|
|
// Compare against last known library state
|
|
if (currentLibraryState != lastLibraryState.Value)
|
|
scanNecessary = true;
|
|
|
|
// The contents changed? Initiate a full rescan, then call LibraryWatcher.
|
|
if (scanNecessary)
|
|
await lw.PerformFullScanAsync();
|
|
// State seems identical? Launch just the LibraryWatcher.
|
|
// TODO: lw.Watch()...
|
|
|
|
return scanNecessary;
|
|
|
|
}
|
|
}
|