130 lines
3.7 KiB
C#
130 lines
3.7 KiB
C#
using Microsoft.VisualBasic.FileIO;
|
|
using Shadow.Data;
|
|
using Shadow.Entities;
|
|
using System.Security.Cryptography;
|
|
|
|
namespace Shadow.Tools;
|
|
|
|
public static class MediaParser
|
|
{
|
|
/// <summary>
|
|
/// Generate a random hex string (with length 32 by default)
|
|
/// </summary>
|
|
/// <param name="length">Optional: hexstring length</param>
|
|
/// <returns>A hexstring of given length</returns>
|
|
public static string HexStr(int length = 32)
|
|
{
|
|
return RandomNumberGenerator.GetHexString(length).ToLower();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get metadata content opportunistically
|
|
/// </summary>
|
|
/// <param name="metadata">Dictionary to search in</param>
|
|
/// <param name="searchStrings">Keywords to search for</param>
|
|
/// <returns>Retrieved value (string) on success, otherwise null</returns>
|
|
public static string? GetAny(Dictionary<string, string> metadata, List<string> searchStrings)
|
|
{
|
|
foreach (string searchString in searchStrings)
|
|
{
|
|
if (metadata.TryGetValue(searchString, out string? value) && !string.IsNullOrEmpty(value))
|
|
return value;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Map exiftool metadata to Song entity.
|
|
/// </summary>
|
|
/// <param name="db">Database context</param>
|
|
/// <param name="exif">ExifTool metadata</param>
|
|
/// <returns>New Song entity, or null if song already exists in db</returns>
|
|
public static Song? CreateSong(ApplicationDbContext db, Dictionary<string, string> 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
|
|
|
|
// TODO: Try and find genres
|
|
|
|
// 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.SaveChanges();
|
|
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;
|
|
}
|
|
|
|
|
|
}
|