145 lines
4.5 KiB
C#
145 lines
4.5 KiB
C#
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();
|
|
|
|
/// <summary>
|
|
/// Returns a sorted list of paths to all files in a directory, recursively.
|
|
/// </summary>
|
|
/// <param name="directory">Path to directory</param>
|
|
/// <returns>Sorted list of filepaths</returns>
|
|
public async Task<List<string>> GetFilesRecursivelyAsync(string directory)
|
|
{
|
|
string[] allowedExtensions = [".flac", ".m4a", ".mp3", ".ogg", ".wav"];
|
|
try
|
|
{
|
|
List<string> 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();
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Return all multimedia content inside of library
|
|
/// </summary>
|
|
/// <returns>List of multimedia filepaths</returns>
|
|
public async Task<List<string>> GetAllMultimediaAsync()
|
|
{
|
|
|
|
// List files in cache
|
|
// Note: currently, the only excluded path from scanning is the thumbnail cache.
|
|
// This might change in the future.
|
|
List<string> cacheFiles = await GetFilesRecursivelyAsync(excludedPaths[0]);
|
|
|
|
// List files in library excluding cache
|
|
List<string> libraryContent = await GetFilesRecursivelyAsync(libraryPath);
|
|
List<string> libraryMultimedia = libraryContent.Except(cacheFiles).ToList();
|
|
|
|
return libraryMultimedia;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scan the library in its entirety
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public async Task<List<string>> PerformFullScanAsync()
|
|
{
|
|
Console.WriteLine("Performing full library scan...");
|
|
|
|
// Current library state as present in database
|
|
List<Song> currentLibraryMedia = await db.Songs
|
|
.Where(s => s.State == 0)
|
|
.ToListAsync();
|
|
|
|
// Updated library state
|
|
List<Song> updatedSongList = [];
|
|
List<string> newMultimediaPathNames = await GetAllMultimediaAsync();
|
|
foreach (string filepath in newMultimediaPathNames)
|
|
{
|
|
Console.WriteLine(filepath);
|
|
Dictionary<string, string> 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<Song> 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;
|
|
}
|
|
}
|