using Microsoft.VisualBasic.FileIO; using Shadow.Data; using Shadow.Entities; using System.Security.Cryptography; namespace Shadow.Tools; public static class MediaParser { /// /// Generate a random hex string (with length 32 by default) /// /// Optional: hexstring length /// A hexstring of given length public static string HexStr(int length = 32) { return RandomNumberGenerator.GetHexString(length).ToLower(); } /// /// Get metadata content opportunistically /// /// Dictionary to search in /// Keywords to search for /// Retrieved value (string) on success, otherwise null public static string? GetAny(Dictionary metadata, List searchStrings) { foreach (string searchString in searchStrings) { if (metadata.TryGetValue(searchString, out string? value) && !string.IsNullOrEmpty(value)) return value; } return null; } /// /// Map exiftool metadata to Song entity. /// /// Database context /// ExifTool metadata /// New Song entity, or null if song already exists in db public static Song? CreateSong(ApplicationDbContext db, Dictionary exif) { // First of all, check if song already exists in db string uri = GetAny(exif, ["_shadow:fileHash"]) ?? throw new Exception("Fatal error: could not get file hash!"); if (db.Songs.FirstOrDefault(s => s.Uri == uri) != null) return null; // If not, extract exif data string title = GetAny(exif, [ "ItemList:Title", // iTunes m4a "ItemList:SortName", // iTunes m4a "Vorbis:Title", // Bandcamp ogg "ID3v2_3:Title", // Generic mp3/wav ID3 v2.3.0 ]) ?? Path.Combine(exif["System:Directory"], exif["System:FileName"]); string filepath = Path.GetFullPath( Path.Combine(exif["System:Directory"], exif["System:FileName"]) ); // TODO: bulletproof this string filetype = exif["File:FileType"].ToLower(); // Album/artist related string artistName = GetAny(exif, [ "ItemList:Artist", // iTunes m4a "ItemList:AlbumArtist", // iTunes m4a "Vorbis:Artist", // Bandcamp m4a "Vorbis:Albumartist", // Bandcamp m4a "ID3v2_3:Artist", // Generic mp3/wav ID3 v2.3.0 ]) ?? "[Unknown Artist]"; // this is a weak line of defense against deliberately crafted string albumName = GetAny(exif, [ "ItemList:Album", // iTunes m4a "Vorbis:Album", // Bandcamp m4a "ID3v2_3:Album", // Generic mp3/wav ID3 v2.3.0 ]) ?? "[Unknown Album]"; // again, weak line of defense // Try to find relevant artists and albums Artist artist = db.Artists .FirstOrDefault(a => a.NormalizedName == artistName.ToLower()) ?? new Artist { Name = artistName, NormalizedName = artistName.ToLower() }; Album album = db.Albums .FirstOrDefault(a => a.Name == albumName && a.Artist == artist) ?? new Album { Name = albumName, Uri = HexStr(), Artist = artist }; Song song = new() { Title = title, Uri = uri, Filepath = filepath, Filetype = filetype, Album = album, Artist = artist }; try { // Is Update() safe here? db.Artists.Update(artist); db.Albums.Update(album); db.Songs.Update(song); artist.Albums.Add(album); artist.Songs.Add(song); db.SaveChanges(); } catch (Exception e) { Console.WriteLine("[Error: MediaParser] Failed to extract metadata from {filepath}:\n" + $"{e}"); } return song; } }