diff --git a/SongCrawler/Program.cs b/SongCrawler/Program.cs index 2fc48fa..2eb10cf 100644 --- a/SongCrawler/Program.cs +++ b/SongCrawler/Program.cs @@ -15,58 +15,66 @@ using System.Security.Cryptography; namespace SongCrawler { - + /// + /// Main program class for the SongCrawler application. + /// Handles crawling, indexing, and managing karaoke songs from file systems and Firebase. + /// class Program { - // Properties to store commonly used values from args + #region Properties and Configuration + + /// + /// Controller identifier from command line arguments + /// private static string Controller { get; set; } + + /// + /// Path to the songs folder from command line arguments + /// private static string SongsFolderPath { get; set; } + + /// + /// Original command line arguments array + /// private static string[] Args { get; set; } + + /// + /// Shared Firebase client instance + /// private static FireSharp.FirebaseClient FirebaseClient { get; set; } - private static Guid GuidFromString(string input) + #endregion + + #region Main Entry Point + + /// + /// Main entry point for the application. + /// Initializes properties from command line arguments and routes to appropriate functionality. + /// + /// Command line arguments: [controller] [songspath] [optional: delete] + static void Main(string[] args) { - using (MD5 md5 = MD5.Create()) + InitializeFromArgs(args); + + if (args.Count() == 3) { - byte[] hash = md5.ComputeHash(Encoding.Default.GetBytes(input)); - return new Guid(hash); + DeleteSongs(); + } + else + { + CrawlSongs(); } } - // Helper methods to eliminate duplication - private static FireSharp.FirebaseClient CreateFirebaseClient() - { - IFirebaseConfig config = new FirebaseConfig - { - AuthSecret = ConfigurationManager.AppSettings["Firebase.Secret"], - BasePath = ConfigurationManager.AppSettings["Firebase.Path"] - }; - return new FireSharp.FirebaseClient(config); - } + #endregion - private static string GetControllerPath(string controller, string pathType) - { - return string.Format("controllers/{0}/{1}", controller, pathType); - } - - private static List GetAllMusicFiles(string songpath) - { - List files = new List(); - files.AddRange(FindFiles("mp4", songpath)); - files.AddRange(FindFiles("mp3", songpath)); - files.AddRange(FindFiles("zip", songpath)); - return files; - } - - private static void ValidateArgs(string[] args, int expectedLength, string usage) - { - if (args.Length != expectedLength) - { - Console.WriteLine(usage); - return; - } - } + #region Initialization and Setup + /// + /// Initializes application properties from command line arguments. + /// Sets up Controller, SongsFolderPath, Args, and FirebaseClient. + /// + /// Command line arguments array private static void InitializeFromArgs(string[] args) { Args = args; @@ -81,7 +89,57 @@ namespace SongCrawler FirebaseClient = CreateFirebaseClient(); } - // Additional helper methods for more refactoring + /// + /// Creates and configures a Firebase client using settings from App.config. + /// + /// Configured Firebase client instance + private static FireSharp.FirebaseClient CreateFirebaseClient() + { + IFirebaseConfig config = new FirebaseConfig + { + AuthSecret = ConfigurationManager.AppSettings["Firebase.Secret"], + BasePath = ConfigurationManager.AppSettings["Firebase.Path"] + }; + return new FireSharp.FirebaseClient(config); + } + + /// + /// Validates command line arguments against expected length. + /// + /// Arguments array to validate + /// Expected number of arguments + /// Usage message to display if validation fails + private static void ValidateArgs(string[] args, int expectedLength, string usage) + { + if (args.Length != expectedLength) + { + Console.WriteLine(usage); + return; + } + } + + #endregion + + #region Firebase Utilities + + /// + /// Generates Firebase path for a specific controller and data type. + /// + /// Controller identifier + /// Type of data (songs, favorites, disabled, etc.) + /// Formatted Firebase path + private static string GetControllerPath(string controller, string pathType) + { + return string.Format("controllers/{0}/{1}", controller, pathType); + } + + /// + /// Loads data from Firebase with fallback handling for different response formats. + /// + /// Type of data to load + /// Firebase client instance + /// Firebase path to load from + /// List of loaded objects private static List LoadFirebaseData(FireSharp.FirebaseClient client, string path) where T : class { try @@ -94,6 +152,71 @@ namespace SongCrawler } } + /// + /// Converts dynamic JSON response to strongly-typed list. + /// Handles Firebase responses that don't deserialize directly to List. + /// + /// Type to convert to + /// Dynamic JSON response + /// List of converted objects + private static List convertToList(dynamic json) where T : class + { + dynamic data = JsonConvert.DeserializeObject(json); + var list = new List(); + foreach (var itemDynamic in data) + { + var fjson = itemDynamic.Value.ToString(); + var f = JsonConvert.DeserializeObject(fjson); + list.Add(f); + } + return list; + } + + #endregion + + #region File System Operations + + /// + /// Scans directory for all supported music file types (mp3, mp4, zip). + /// + /// Directory path to scan + /// List of all music file paths found + private static List GetAllMusicFiles(string songpath) + { + List files = new List(); + files.AddRange(FindFiles("mp4", songpath)); + files.AddRange(FindFiles("mp3", songpath)); + files.AddRange(FindFiles("zip", songpath)); + return files; + } + + /// + /// Finds all files with specified extension in directory and subdirectories. + /// + /// File extension to search for + /// Directory path to search + /// Array of file paths found + private static string[] FindFiles(string ext, string path) + { + Console.Write(string.Format("\rscanning {0} for {1} - ", path, ext)); + string[] files = Directory.GetFiles(path, "*." + ext, SearchOption.AllDirectories); + Console.WriteLine(string.Format("{0} found", files.Length)); + return files; + } + + #endregion + + #region Song Processing + + /// + /// Processes a list of files and converts them to Song objects. + /// Handles duplicate checking, debug limits, and error handling. + /// + /// List of file paths to process + /// List to add processed songs to + /// Whether to check for duplicate songs + /// Whether to run in debug mode (limited processing) + /// Maximum number of files to process in debug mode private static void ProcessFilesToSongs(List files, List songs, bool checkDuplicates = false, bool debug = false, int debugLimit = 1000) { Song song = null; @@ -129,6 +252,102 @@ namespace SongCrawler } } + /// + /// Creates a Song object from a file path. + /// Handles different file types (mp3, mp4, zip) and extracts metadata. + /// + /// Path to the music file + /// Song object with extracted metadata + private static Song MakeSong(string filepath) + { + Song song = null; + var ext = Path.GetExtension(filepath).ToLower(); + switch (ext) + { + case ".mp3": + case ".mp4": + song = ReadId3(filepath); + break; + case ".zip": + song = ReadId3FromZip(filepath); + break; + } + CheckTitle(song); + song.Path = filepath; + return song; + } + + /// + /// Reads ID3 tags from audio files (mp3, mp4). + /// + /// Path to audio file + /// Song object with extracted metadata + static Song ReadId3(string path) + { + Song song = new Song(); + TagLib.File tagFile; + try + { + tagFile = TagLib.File.Create(path); + song.Title = tagFile.Tag.Title.Trim(); + song.Artist = tagFile.Tag.FirstPerformer.Trim(); + } + catch + { + // do nothing; + } + song.Path = path; + return song; + } + + /// + /// Reads ID3 tags from mp3 files inside zip archives. + /// + /// Path to zip file + /// Song object with extracted metadata + private static Song ReadId3FromZip(string zippath) + { + ZipFile.ExtractToDirectory(zippath, "c:\\temp"); + string filepath = Directory.GetFiles("c:\\temp", "*.mp3")[0]; + Song song = ReadId3(filepath); + foreach (string file in Directory.GetFiles("c:\\temp")) File.Delete(file); + return song; + } + + /// + /// Extracts title and artist from filename if ID3 tags are missing. + /// Assumes format: "Artist - Title" or just "Title". + /// + /// Song object to update + private static void CheckTitle(Song song) + { + if(string.IsNullOrEmpty(song.Title)) + { + string file = Path.GetFileNameWithoutExtension(song.Path); + string[] parts = file.Split('-'); + if (parts.Length == 1) + { + song.Title = parts[0].Trim(); + } + else if (parts.Length > 1) + { + song.Artist = parts[parts.Length - 2].Trim(); + song.Title = parts[parts.Length - 1].Trim(); + } + } + } + + #endregion + + #region Song Management + + /// + /// Synchronizes song properties from source lists using path matching. + /// Used to restore favorites and disabled states after re-indexing. + /// + /// Target song list to update + /// Source list containing property values + /// Action to set the property on matching songs private static void SyncSongProperties(List songs, List sourceList, Action propertySetter) { if (sourceList != null && sourceList.Count > 0) @@ -147,6 +366,12 @@ namespace SongCrawler } } + /// + /// Finds duplicate songs based on artist and title matching. + /// + /// List of songs to check for duplicates + /// Optional filter to apply before duplicate checking + /// Dictionary of duplicate groups keyed by "Artist - Title" private static Dictionary> FindDuplicateSongs(List songs, Func filter = null) { Dictionary> dupes = new Dictionary>(); @@ -178,24 +403,16 @@ namespace SongCrawler return dupes; } - static void Main(string[] args) - { - InitializeFromArgs(args); - - if (args.Count() == 3) - { - DeleteSongs(); - } - else - { - CrawlSongs(); - } - } + #endregion + #region Core Operations + + /// + /// Main crawling operation. Scans music files, creates song objects, and uploads to Firebase. + /// Maintains favorites and disabled states from previous runs. + /// private static void CrawlSongs() { - //string [] test = { "mbrucedogs", "z://" }; - //args = test; var debug = false; ValidateArgs(Args, 2, "usage: songcrawler partyid songspath"); if (Args.Length != 2) return; @@ -220,44 +437,36 @@ namespace SongCrawler SyncSongProperties(songs, disabled, song => song.Disabled = true); FirebaseClient.Set(songsPath, songs); - //string test = string.Format("controllers/{0}/testsongs", Controller); - //Dictionary testSongs = new Dictionary(); - //foreach (Song s in songs) - //{ - // testSongs[s.Guid] = s; - //} - //FirebaseClient.Set(test, testSongs); - + // Create newSongs list from most recently created files var created = songs.Select(s => new CreatedSong(File.GetCreationTime(s.Path), s)).ToList(); var first200 = created.Where(s => s.created != null).OrderByDescending(s => s.created).Take(200); var added = first200.Select(s => new PathOnly(path: s.song.Path)).ToList(); string newSongs = GetControllerPath(Controller, "newSongs"); FirebaseClient.Set(newSongs, added); - } - private class PathOnly + + /// + /// Crawls songs without allowing duplicates based on artist and title. + /// + private static void CrawlNoDupeSongs() { - [JsonProperty("path")] - public String Path { get; set; } - public PathOnly(string path) - { - this.Path = path; - } - } - - private static List convertToList(dynamic json) where T : class - { - dynamic data = JsonConvert.DeserializeObject(json); - var list = new List(); - foreach (var itemDynamic in data) - { - var fjson = itemDynamic.Value.ToString(); - var f = JsonConvert.DeserializeObject(fjson); - list.Add(f); - } - return list; + ValidateArgs(Args, 2, "usage: songcrawler partyid songspath"); + if (Args.Length != 2) return; + + string firepath = GetControllerPath(Controller, "songs"); + Console.WriteLine("Loading current library"); + List songs = new List(); + + List files = GetAllMusicFiles(SongsFolderPath); + + ProcessFilesToSongs(files, songs, checkDuplicates: true); + Console.WriteLine(string.Format("{0:000000}/{1} unique songs", songs.Count(), files.Count())); + FirebaseClient.Set(firepath, songs); } + /// + /// Deletes all songs for the specified controller. + /// private static void DeleteSongs() { ValidateArgs(Args, 3, "usage: songcrawler partyid songspath delete"); @@ -267,9 +476,15 @@ namespace SongCrawler Console.WriteLine("Deleting Songs ..."); List songs = new List(); FirebaseClient.Set(firepath, songs); - } + #endregion + + #region Maintenance Operations + + /// + /// Fixes newSongs list by replacing path-only entries with full song objects. + /// private static void fixNewSongs() { string songsPath = GetControllerPath(Controller, "songs"); @@ -289,6 +504,9 @@ namespace SongCrawler FirebaseClient.Set(newSongsPath, updated); } + /// + /// Fixes history list by ensuring proper JSON structure. + /// private static void fixHistory() { string historyPath = GetControllerPath(Controller, "history"); @@ -310,46 +528,11 @@ namespace SongCrawler } FirebaseClient.Set(historyPath, history); } - public class CreatedSong - { - public DateTime created { get; set; } - public Song song { get; set; } - public CreatedSong(DateTime created, Song song) - { - this.song = song; - this.created = created; - } - } - - private static void CrawlNoDupeSongs() - { - ValidateArgs(Args, 2, "usage: songcrawler partyid songspath"); - if (Args.Length != 2) return; - - string firepath = GetControllerPath(Controller, "songs"); - Console.WriteLine("Loading current library"); - List songs = new List(); - - List files = GetAllMusicFiles(SongsFolderPath); - - ProcessFilesToSongs(files, songs, checkDuplicates: true); - Console.WriteLine(string.Format("{0:000000}/{1} unique songs", songs.Count(), files.Count())); - FirebaseClient.Set(firepath, songs); - } - - private static void FindDuplicates(string[] args) - { - string songpath = @"D:\KaraokeData\Karaoke"; // args[0]; - List songs = songs = new List(); - - List files = GetAllMusicFiles(songpath); - - ProcessFilesToSongs(files, songs); - - var dupes = FindDuplicateSongs(songs); - File.WriteAllText(@"D:\dupliates.json", JsonConvert.SerializeObject(dupes.OrderBy(o => o.Key))); - } + /// + /// Finds and disables duplicate songs, keeping only the first occurrence. + /// Only processes mp4 files that are not already disabled. + /// private static void DisableDuplicates() { ValidateArgs(Args, 1, "usage: songcrawler partyid songspath"); @@ -372,77 +555,68 @@ namespace SongCrawler FirebaseClient.Set(firepath, songs); } - private static Song MakeSong(string filepath) + /// + /// Finds duplicates in a local directory and exports to JSON file. + /// + /// Command line arguments (not used, hardcoded path) + private static void FindDuplicates(string[] args) { - - Song song = null; - var ext = Path.GetExtension(filepath).ToLower(); - switch (ext) - { - case ".mp3": - case ".mp4": - song = ReadId3(filepath); - break; - case ".zip": - song = ReadId3FromZip(filepath); - break; - } - CheckTitle(song); - song.Path = filepath; - return song; + string songpath = @"D:\KaraokeData\Karaoke"; // args[0]; + List songs = new List(); + + List files = GetAllMusicFiles(songpath); + + ProcessFilesToSongs(files, songs); + + var dupes = FindDuplicateSongs(songs); + File.WriteAllText(@"D:\dupliates.json", JsonConvert.SerializeObject(dupes.OrderBy(o => o.Key))); } - private static string[] FindFiles(string ext, string path) - { - Console.Write(string.Format("\rscanning {0} for {1} - ", path, ext)); - string[] files = Directory.GetFiles(path, "*." + ext, SearchOption.AllDirectories); - Console.WriteLine(string.Format("{0} found", files.Length)); - return files; - } + #endregion - private static void CheckTitle(Song song) + #region Utility Classes + + /// + /// Represents a song with its creation timestamp for sorting. + /// + public class CreatedSong { - if(string.IsNullOrEmpty(song.Title)) + public DateTime created { get; set; } + public Song song { get; set; } + public CreatedSong(DateTime created, Song song) { - string file = Path.GetFileNameWithoutExtension(song.Path); - string[] parts = file.Split('-'); - if (parts.Length == 1) - { - song.Title = parts[0].Trim(); - } - else if (parts.Length > 1) - { - song.Artist = parts[parts.Length - 2].Trim(); - song.Title = parts[parts.Length - 1].Trim(); - } + this.song = song; + this.created = created; } } - private static Song ReadId3FromZip(string zippath) + /// + /// Lightweight class for storing only file paths in newSongs list. + /// + private class PathOnly { - ZipFile.ExtractToDirectory(zippath, "c:\\temp"); - string filepath = Directory.GetFiles("c:\\temp", "*.mp3")[0]; - Song song = ReadId3(filepath); - foreach (string file in Directory.GetFiles("c:\\temp")) File.Delete(file); - return song; + [JsonProperty("path")] + public String Path { get; set; } + public PathOnly(string path) + { + this.Path = path; + } } - static Song ReadId3(string path) + /// + /// Generates a consistent GUID from a string input using MD5 hashing. + /// + /// String to generate GUID from + /// Consistent GUID for the input string + private static Guid GuidFromString(string input) { - Song song = new Song(); - TagLib.File tagFile; - try + using (MD5 md5 = MD5.Create()) { - tagFile = TagLib.File.Create(path); - song.Title = tagFile.Tag.Title.Trim(); - song.Artist = tagFile.Tag.FirstPerformer.Trim(); + byte[] hash = md5.ComputeHash(Encoding.Default.GetBytes(input)); + return new Guid(hash); } - catch - { - // do nothing; - } - song.Path = path; - return song; } + + #endregion } }