using Microsoft.EntityFrameworkCore;
using Shadow.Data;
using Shadow.Entities;
using System;
using System.IO;
namespace Shadow.Tools;
public class LibraryWatcher(string watchPath, string[] excludedPaths, ApplicationDbContext dbContext)
{
private readonly string libraryPath = watchPath;
private readonly string[] excludedPaths = excludedPaths;
private readonly ApplicationDbContext db = dbContext;
private readonly GeneralUseHelpers guhf = new();
///
/// Returns a sorted list of paths to all files in a directory, recursively.
///
/// Path to directory
/// Sorted list of filepaths
public async Task> GetFilesRecursivelyAsync(string directory)
{
string[] allowedExtensions = [".flac", ".m4a", ".mp3", ".ogg", ".wav"];
try
{
List files =
Directory.GetFiles(directory, "*", SearchOption.AllDirectories)
.Where(file => allowedExtensions.Any(file.ToLower().EndsWith))
.ToList();
files.Sort();
return files;
}
catch (DirectoryNotFoundException)
{
Console.WriteLine($"[Error] Directory \"{directory}\" does not exist!\n" +
" Please create it manually, or use `Shadow setupWizard`.");
throw new DirectoryNotFoundException();
}
}
///
/// Return all multimedia content inside of library
///
/// List of multimedia filepaths
public async Task> GetAllMultimediaAsync()
{
// List files in cache
// Note: currently, the only excluded path from scanning is the thumbnail cache.
// This might change in the future.
List cacheFiles = await GetFilesRecursivelyAsync(excludedPaths[0]);
// List files in library excluding cache
List libraryContent = await GetFilesRecursivelyAsync(libraryPath);
List libraryMultimedia = libraryContent.Except(cacheFiles).ToList();
return libraryMultimedia;
}
///
/// Scan the library in its entirety
///
///
public async Task> PerformFullScanAsync()
{
Console.WriteLine("Performing full library scan...");
// Current library state as present in database
List currentLibraryMedia = await db.Songs
.Where(s => s.State == 0)
.ToListAsync();
// Updated library state
List updatedSongList = [];
List newMultimediaPathNames = await GetAllMultimediaAsync();
foreach (string filepath in newMultimediaPathNames)
{
Console.WriteLine(filepath);
Dictionary fileInfo = await MetadataExtractor.ExtractAsync(filepath);
// Pretend we are doing parsing here...
Console.WriteLine(GeneralUseHelpers.DictAsJson(fileInfo));
Song? songInDb = db.Songs
.Include(s => s.Album)
.FirstOrDefault(s => s.Uri == fileInfo["_shadow:fileHash"]);
if (songInDb != null)
{
// Don't parse the song
Console.WriteLine("Skipping song as it already exists in database...");
// But update it's location in case it has been moved
songInDb.Filepath = filepath;
// And state in case it has been reinstated
songInDb.State = 0; // Set non-orphaned state
songInDb.Album.State = 0; // -||-
db.Update(songInDb); // Is this necessary?
await db.SaveChangesAsync();
// Afterwards include it in the updated song list
updatedSongList.Add(songInDb);
}
else
{
// A new song? Parse it and add to DB
Song? newSong = MediaParser.CreateSong(db, fileInfo);
// Sanity check
if (newSong != null)
updatedSongList.Add(newSong);
}
}
Console.WriteLine($"Full scan complete! Processed {newMultimediaPathNames.Count} files.");
List orphanedSongs = currentLibraryMedia
.Except(updatedSongList)
.ToList();
Console.WriteLine($"Detected {orphanedSongs.Count} new orphaned songs");
foreach (Song s in orphanedSongs)
{
Song dbSong = db.Songs
.Include(d => d.Artist)
.First(d => d.Id == s.Id);
Console.WriteLine($"- {dbSong.Title} by {dbSong.Artist.Name} (previous path: {dbSong.Filepath})");
dbSong.State = 1;
await db.SaveChangesAsync();
}
Console.WriteLine();
// Update state inside of DB
string updatedLibraryState = MetadataExtractor.GetStringMD5(string.Join("\n", newMultimediaPathNames));
Global lastLibraryState = db.Globals.FirstOrDefault(g => g.Key == "libraryState")
?? new() { Key = "libraryState"};
lastLibraryState.Value = updatedLibraryState;
db.Update(lastLibraryState);
await db.SaveChangesAsync();
return newMultimediaPathNames;
}
}