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