feat: add db cleanup command line argument
This commit is contained in:
112
Tools/Cli.cs
112
Tools/Cli.cs
@@ -1,3 +1,4 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Shadow.Data;
|
||||
using Shadow.Entities;
|
||||
|
||||
@@ -8,7 +9,8 @@ public class Cli
|
||||
private readonly GeneralUseHelpers guhf;
|
||||
private readonly string[] args;
|
||||
|
||||
// TODO: Add "changeUser", "fullRescan", "destructiveRescan"
|
||||
// TODO: Add "changeUser", "fullRescan", "destructiveRescan", "healthcheck"
|
||||
// TODO: Make this async
|
||||
|
||||
public Cli(ApplicationDbContext _db, GeneralUseHelpers _guhf, string[] _args)
|
||||
{
|
||||
@@ -65,6 +67,15 @@ public class Cli
|
||||
if (!Seeder.EnsureMigrations(db)) return true;
|
||||
SetupWizard();
|
||||
break;
|
||||
case "clear":
|
||||
case "cleanup":
|
||||
case "cleardb":
|
||||
case "cleanupdb":
|
||||
case "--clear":
|
||||
case "--cleanup":
|
||||
if (!Seeder.EnsureMigrations(db)) return true;
|
||||
CleanUpDb();
|
||||
break;
|
||||
default:
|
||||
Console.WriteLine($"Unknown option: \"{args[0]}\". See \"help\" for available arguments.");
|
||||
break;
|
||||
@@ -88,9 +99,10 @@ public class Cli
|
||||
$"- removeUser [username] - remove the user COMPLETELY.\n" +
|
||||
$"\n" +
|
||||
$"=== Server maintenance ===\n" +
|
||||
$"- setupWizard - configure library and thumbnail location on disk.\n" +
|
||||
$"- setupWizard - configure library and thumbnail location on disk,\n" +
|
||||
$"- cleanup - cleanup database from orphaned songs/albums/artists.\n" +
|
||||
$"\n" +
|
||||
$"Username is optional. If not provided, user will be prompted to enter it.\n" +
|
||||
$"[field] means the field 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"
|
||||
@@ -281,6 +293,8 @@ public class Cli
|
||||
" - Shadow's image thumbnails path, used to provide cover art extracted from media.\n" +
|
||||
" Finally, you will be asked if you want your changes saved.\n");
|
||||
|
||||
// TODO: do a healthcheck here
|
||||
|
||||
Global? musicLibraryPath = db.Globals.FirstOrDefault(g => g.Key == "musicLibraryPath");
|
||||
Global? musicThumbnailPath = db.Globals.FirstOrDefault(g => g.Key == "musicThumbnailPath");
|
||||
|
||||
@@ -299,7 +313,7 @@ public class Cli
|
||||
Console.WriteLine(" No existing configuration found.\n");
|
||||
}
|
||||
|
||||
string newMusicLibraryPath = DefaultPrompt("Please enter your new music library path", musicLibraryPath?.Value).TrimEnd(['\\', '/']);
|
||||
string newMusicLibraryPath = DefaultPrompt("Please enter your new music library path", musicLibraryPath?.Value).TrimEnd(['\\', '/']);
|
||||
string newMusicThumbnailPath = DefaultPrompt("Please enter your new image thumbnail path", musicThumbnailPath?.Value ?? Path.Combine(newMusicLibraryPath, ".shadow")).TrimEnd(['\\', '/']);
|
||||
|
||||
bool confirmed = YesNoPrompt("Are you sure you want:\n" +
|
||||
@@ -322,6 +336,9 @@ public class Cli
|
||||
musicLibraryPath.Value = newMusicLibraryPath;
|
||||
musicThumbnailPath.Value = newMusicThumbnailPath;
|
||||
|
||||
System.IO.Directory.CreateDirectory(newMusicLibraryPath);
|
||||
System.IO.Directory.CreateDirectory(newMusicThumbnailPath);
|
||||
|
||||
// UpdateRange can both Add and Update rows.
|
||||
db.Globals.UpdateRange(musicLibraryPath, musicThumbnailPath);
|
||||
db.SaveChanges();
|
||||
@@ -338,6 +355,93 @@ public class Cli
|
||||
return success;
|
||||
}
|
||||
|
||||
public void CleanUpDb()
|
||||
{
|
||||
// Ask user here whether he/she wants to
|
||||
// clear the DB from dangling records (orphaned songs/albums/artists).
|
||||
int rowsAffected = 0;
|
||||
|
||||
// Retrieve orphaned songs
|
||||
List<Song> orphanedSongs = db.Songs.Where(s => s.State == 1).ToList();
|
||||
|
||||
// Ask if it's alright to remove them
|
||||
// and related listening data permanently
|
||||
if (orphanedSongs.Count != 0)
|
||||
{
|
||||
bool songConsent = false;
|
||||
songConsent = YesNoPrompt($"Found {orphanedSongs.Count} orphaned songs. Remove them? [y/N]: ", false);
|
||||
if (songConsent)
|
||||
{
|
||||
foreach (Song s in orphanedSongs)
|
||||
{
|
||||
// Remove song interactions
|
||||
List<SongInteraction> interactions = db.SongInteractions.Where(si => si.Song == s).ToList();
|
||||
db.SongInteractions.RemoveRange(interactions);
|
||||
|
||||
// Remove song from playlists
|
||||
List<PlaylistSong> playlists = db.PlaylistSongs.Where(ps => ps.Song == s).ToList();
|
||||
db.PlaylistSongs.RemoveRange(playlists);
|
||||
|
||||
// Remove song from albums
|
||||
Album album = s.Album;
|
||||
album.Songs.Remove(s);
|
||||
|
||||
if (album.Songs.Count == 0) album.State = 1; // Set album state to orphaned.
|
||||
|
||||
// TODO: Remove song images if not used by any other resource
|
||||
// ...
|
||||
}
|
||||
|
||||
// Perform cleanup with stored procedure
|
||||
rowsAffected += db.Database.ExecuteSqlRaw("CALL song_cleanup()");
|
||||
// rowsAffected += songs.Count;
|
||||
}
|
||||
}
|
||||
else Console.WriteLine("No orphaned songs found.");
|
||||
|
||||
// Rinse and repeat
|
||||
// Retrieve orphaned albums
|
||||
List<Album> orphanedAlbums = db.Albums.Where(a => a.State == 1).ToList();
|
||||
|
||||
if (orphanedAlbums.Count != 0)
|
||||
{
|
||||
bool albumConsent = false;
|
||||
albumConsent = YesNoPrompt($"Found {orphanedAlbums.Count} orphaned albums. Remove them? [y/N]: ", false);
|
||||
if (albumConsent)
|
||||
{
|
||||
db.Albums.RemoveRange(orphanedAlbums);
|
||||
rowsAffected += orphanedAlbums.Count;
|
||||
}
|
||||
}
|
||||
else Console.WriteLine("No orphaned albums found.");
|
||||
|
||||
// Retrieve orphaned artists (artists with no songs AND albums)
|
||||
List<Artist> orphanedArtists = db.Artists.Where(a => a.Songs.Count == 0 && a.Albums.Count == 0).ToList();
|
||||
Artist? unknownArtist = db.Artists.FirstOrDefault(a => a.NormalizedName == "[unknown artist]");
|
||||
|
||||
// Account for the [Unknown Artist],
|
||||
// which is a meta-artist and shall
|
||||
// not be slated for removal.
|
||||
// Can be null only if this command
|
||||
// is ran before seeding.
|
||||
if (unknownArtist != null) orphanedArtists.Remove(unknownArtist);
|
||||
|
||||
if (orphanedArtists.Count != 0)
|
||||
{
|
||||
bool artistConsent = false;
|
||||
artistConsent = YesNoPrompt($"Found {orphanedArtists.Count} orphaned artists. Remove them? [y/N]: ", false);
|
||||
if (artistConsent)
|
||||
{
|
||||
db.Artists.RemoveRange(orphanedArtists);
|
||||
rowsAffected += orphanedArtists.Count;
|
||||
}
|
||||
}
|
||||
else Console.WriteLine("No orphaned artists found.");
|
||||
|
||||
Console.WriteLine($"{rowsAffected} entries affected.");
|
||||
db.SaveChanges();
|
||||
}
|
||||
|
||||
public static 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/
|
||||
|
||||
Reference in New Issue
Block a user