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