Compare commits

...

10 Commits

Author SHA1 Message Date
cb1e1985c3 Signed-off-by: mbrucedogs <mbrucedogs@gmail.com> 2025-08-04 16:01:31 -05:00
8bec97afc4 refactor 4
Signed-off-by: mbrucedogs <mbrucedogs@gmail.com>
2025-07-24 13:37:04 -05:00
02159a1420 step 3 refacotr
Signed-off-by: mbrucedogs <mbrucedogs@gmail.com>
2025-07-24 13:34:12 -05:00
fbaba1427a refacotre 2
Signed-off-by: mbrucedogs <mbrucedogs@gmail.com>
2025-07-24 13:28:05 -05:00
86ffb58df4 refacator songcrawler step 1
Signed-off-by: mbrucedogs <mbrucedogs@gmail.com>
2025-07-24 13:25:29 -05:00
c1d2ecafc5 Signed-off-by: mbrucedogs <mbrucedogs@gmail.com> 2025-07-24 13:22:03 -05:00
2b6a4a5c75 updated view
Signed-off-by: mbrucedogs <mbrucedogs@gmail.com>
2025-07-24 13:13:31 -05:00
7173aec7ee Signed-off-by: mbrucedogs <mbrucedogs@gmail.com> 2025-07-22 07:56:38 -05:00
d087d2d393 fixed bug
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2025-07-22 07:56:22 -05:00
c2ae664d80 udpated code
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
2022-02-13 12:51:07 -06:00
6 changed files with 1097 additions and 568 deletions

View File

@ -24,7 +24,7 @@ namespace BillboardPlaylistUpdater
static void Main(string[] args)
{
//args = new string[] { "mbrucedogstest" };
//args = new string[] { "mbrucedogs" };
if (args.Length != 1)
{
Console.WriteLine("usage: songcrawler partyid songspath");
@ -49,282 +49,386 @@ namespace BillboardPlaylistUpdater
else
songList = new List<SongList>();
RunTest();
//update Shared Charts and save
UpdateCurrentCharts();
client.Set(firepath, songList);
////RunTest();
//update Controller Charts for Local Search and save
//// TEST MODE: Only process first list and don't save back to Firebase
//Console.WriteLine("TEST MODE: Processing only first list...");
//if (songList != null && songList.Count > 0)
//{
// var firstList = songList[0];
// Console.WriteLine("********************************************************");
// Console.WriteLine(string.Format("Matching Controllers Songs for {0}", firstList.Title));
// Console.WriteLine("********************************************************");
// Search(firstList);
// // Show results summary
// Console.WriteLine("\n=== TEST RESULTS SUMMARY ===");
// int totalMatches = 0;
// foreach (var song in firstList.Songs)
// {
// Console.WriteLine($"{song.Title} - {song.Artist}: {song.FoundSongs.Count} matches");
// totalMatches += song.FoundSongs.Count;
// }
// Console.WriteLine($"Total matches across all songs: {totalMatches}");
//}
//else
//{
// Console.WriteLine("No song lists found to test.");
//}
// Commented out Firebase saves for testing
client.Set(firepath, songList);
client.Set(string.Format("controllers/{0}/songList", controller), songList);
UpdateSearchLists();
client.Set(string.Format("controllers/{0}/songList", controller), songList);
}
static void RunTest()
{
var testArtist = "Linkin Park Featuring Kiiara".RemoveCrap().ToLower();
var testTitle = "Heavy";
var psongs = songs.Where(s => s.Title.Contains(testTitle)).ToList();
foreach (var item in psongs)
{
var ia = item.Artist.RemoveCrap();
var it = item.Title.RemoveCrap();
var artist = DoesMatch(ia, testArtist);
var title = DoesMatch(it, testTitle);
}
}
static void UpdateCurrentCharts()
{
SongList hot100 = DownloadHot100("Hot 100", "https://www.billboard.com/charts/hot-100");
SongList pop = Download("Pop-Songs", "https://www.billboard.com/charts/pop-songs");
SongList rock = Download("Rock-Songs", "https://www.billboard.com/charts/rock-songs");
SongList country = Download("Country-Songs", "https://www.billboard.com/charts/country-songs");
SongList hiphop = Download("R-B-Hip-Hop-Songs", "https://www.billboard.com/charts/r-b-hip-hop-songs");
List<SongList> localSongList = new List<SongList>();
localSongList.Add(pop);
localSongList.Add(rock);
localSongList.Add(country);
localSongList.Add(hiphop);
localSongList.Add(hot100);
foreach (SongList sl in localSongList)
{
try
{
Console.WriteLine(string.Format("Checking for {0}", sl.Title));
var found = songList.Where(s => s.Title.ToLower() == sl.Title.ToLower());
if (found != null)
{
var items = found.ToList();
foreach (var item in items)
{
songList.Remove(item);
}
}
songList.Add(sl);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
songList = songList.OrderByDescending(l => l.Title).ToList();
}
static void UpdateSearchLists()
{
//update the controller SongLists
foreach (var list in songList)
//update the controller SongLists using parallel processing
Console.WriteLine($"Processing {songList.Count} lists in parallel...");
var options = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount };
Parallel.ForEach(songList, options, list =>
{
Console.WriteLine("********************************************************");
Console.WriteLine(string.Format("Matching Controllers Songs for {0}", list.Title));
Console.WriteLine("********************************************************");
Console.WriteLine($"********************************************************");
Console.WriteLine($"Matching Controllers Songs for {list.Title}");
Console.WriteLine($"********************************************************");
Search(list);
}
});
}
static void Search(SongList list)
{
foreach (var song in list.Songs)
{
song.FoundSongs.Clear();
var bA = song.Artist.RemoveCrap().ToLower();
var bT = song.Title.RemoveCrap().ToLower();
if (list.Songs == null || songs == null)
return;
foreach (var item in songs)
// Pre-filter disabled songs once
var availableSongs = songs.Where(s => !s.Disabled).ToList();
Console.WriteLine($"Searching through {availableSongs.Count} available songs for {list.Songs.Count} songs in list...");
// Build fast lookup indexes
var titleIndex = BuildTitleIndex(availableSongs);
var artistIndex = BuildArtistIndex(availableSongs);
// Process each song in the list sequentially
foreach (var songListItem in list.Songs)
{
Console.WriteLine($"Searching for: {songListItem.Title} - {songListItem.Artist}");
var songMatches = new List<(Song song, double score)>();
// Get candidate matches using indexes
var titleCandidates = GetTitleCandidates(songListItem.Title, titleIndex);
var artistCandidates = GetArtistCandidates(songListItem.Artist, artistIndex);
// Combine and deduplicate candidates
var allCandidates = titleCandidates.Union(artistCandidates).Distinct().ToList();
Console.WriteLine($" Found {allCandidates.Count} candidates to evaluate...");
// Evaluate only the candidates (much faster than checking all songs)
foreach (var song in allCandidates)
{
if (item.Artist != null && item.Title != null)
// Calculate similarity scores for title and artist
double titleSimilarity = CalculateSimilarity(songListItem.Title, song.Title);
double artistSimilarity = CalculateSimilarity(songListItem.Artist, song.Artist);
// Combined score (weighted average - title is more important)
double combinedScore = (titleSimilarity * 0.7) + (artistSimilarity * 0.3);
// If combined score is above threshold, consider it a match
if (combinedScore >= 0.85) // Adjustable threshold
{
var t = item.Title.RemoveCrap().ToLower();
var a = item.Artist.RemoveCrap().ToLower();
bool titleMatch = DoesMatch(bT, t);
if (titleMatch && DoesMatch(bA, a))
songMatches.Add((song, combinedScore));
}
}
// Sort matches by relevance and file type priority
songListItem.FoundSongs = songMatches
.OrderByDescending(x => x.score)
.ThenBy(x => GetFileTypePriority(x.song))
.ThenBy(x => GetChannelPriority(x.song))
.Take(10) // Limit to top 10 matches
.Select(x => x.song)
.ToList();
Console.WriteLine($" Total matches found: {songListItem.FoundSongs.Count}");
}
}
static double CalculateSimilarity(string str1, string str2)
{
if (string.IsNullOrEmpty(str1) || string.IsNullOrEmpty(str2))
return 0.0;
// Normalize strings for comparison
str1 = NormalizeString(str1);
str2 = NormalizeString(str2);
// Use multiple fuzzy string algorithms and combine results
double diceCoeff = str1.DiceCoefficient(str2);
double levenshtein = 1.0 - (str1.LevenshteinDistance(str2) / (double)Math.Max(str1.Length, str2.Length));
// Use Longest Common Subsequence as a third algorithm
double lcsScore = CalculateLCSSimilarity(str1, str2);
// Weighted average of different algorithms
return (diceCoeff * 0.4) + (levenshtein * 0.3) + (lcsScore * 0.3);
}
static double CalculateCombinedSimilarity(SongListSong songListItem, Song song)
{
double titleSimilarity = CalculateSimilarity(songListItem.Title, song.Title);
double artistSimilarity = CalculateSimilarity(songListItem.Artist, song.Artist);
// Weight title more heavily than artist
return (titleSimilarity * 0.7) + (artistSimilarity * 0.3);
}
static string NormalizeString(string input)
{
if (string.IsNullOrEmpty(input))
return string.Empty;
// Remove common karaoke prefixes/suffixes
var normalized = input.ToLowerInvariant();
// Remove common karaoke indicators
normalized = Regex.Replace(normalized, @"\b(karaoke|karaoke version|instrumental|backing track)\b", "", RegexOptions.IgnoreCase);
// Remove extra whitespace and punctuation
normalized = Regex.Replace(normalized, @"\s+", " ");
normalized = Regex.Replace(normalized, @"[^\w\s]", "");
return normalized.Trim();
}
static double CalculateLCSSimilarity(string str1, string str2)
{
if (string.IsNullOrEmpty(str1) || string.IsNullOrEmpty(str2))
return 0.0;
int lcsLength = CalculateLCSLength(str1, str2);
int maxLength = Math.Max(str1.Length, str2.Length);
if (maxLength == 0)
return 1.0; // Both strings are empty
return (double)lcsLength / maxLength;
}
static int CalculateLCSLength(string str1, string str2)
{
int[,] dp = new int[str1.Length + 1, str2.Length + 1];
for (int i = 1; i <= str1.Length; i++)
{
for (int j = 1; j <= str2.Length; j++)
{
if (str1[i - 1] == str2[j - 1])
{
dp[i, j] = dp[i - 1, j - 1] + 1;
}
else
{
dp[i, j] = Math.Max(dp[i - 1, j], dp[i, j - 1]);
}
}
}
return dp[str1.Length, str2.Length];
}
static int GetFileTypePriority(Song song)
{
// MP4 files get priority (lower number = higher priority)
if (song.FileType == FileType.MP4)
return 0;
else
return 1;
}
static int GetChannelPriority(Song song)
{
// Channel priorities in order (lower number = higher priority)
string[] channelPriorities = {
"Sing King Karaoke",
"KaraFun Karaoke",
"Stingray Karaoke"
};
// Extract folder name from path
string folderName = ExtractFolderName(song.Path);
// Find the priority index
for (int i = 0; i < channelPriorities.Length; i++)
{
if (folderName.IndexOf(channelPriorities[i], StringComparison.OrdinalIgnoreCase) >= 0)
{
return i; // Return the priority index (0 = highest priority)
}
}
// If not found in priority list, give it lowest priority
return channelPriorities.Length;
}
static string ExtractFolderName(string path)
{
if (string.IsNullOrEmpty(path))
return string.Empty;
try
{
// Get the directory name from the path
string directory = Path.GetDirectoryName(path);
if (string.IsNullOrEmpty(directory))
return string.Empty;
// Get the last folder name
return Path.GetFileName(directory);
}
catch
{
return string.Empty;
}
}
// Index building methods for fast lookup
static Dictionary<string, List<Song>> BuildTitleIndex(List<Song> songs)
{
var index = new Dictionary<string, List<Song>>();
foreach (var song in songs)
{
var normalizedTitle = NormalizeString(song.Title);
var words = normalizedTitle.Split(' ', (char)StringSplitOptions.RemoveEmptyEntries);
// Index by each word in the title
foreach (var word in words)
{
if (word.Length >= 3) // Only index words with 3+ characters
{
if (!index.ContainsKey(word))
index[word] = new List<Song>();
index[word].Add(song);
}
}
// Also index by first few characters for prefix matching
for (int i = 3; i <= Math.Min(8, normalizedTitle.Length); i++)
{
var prefix = normalizedTitle.Substring(0, i);
if (!index.ContainsKey(prefix))
index[prefix] = new List<Song>();
index[prefix].Add(song);
}
}
return index;
}
static Dictionary<string, List<Song>> BuildArtistIndex(List<Song> songs)
{
var index = new Dictionary<string, List<Song>>();
foreach (var song in songs)
{
var normalizedArtist = NormalizeString(song.Artist);
var words = normalizedArtist.Split(' ', (char)StringSplitOptions.RemoveEmptyEntries);
// Index by each word in the artist name
foreach (var word in words)
{
if (word.Length >= 3) // Only index words with 3+ characters
{
if (!index.ContainsKey(word))
index[word] = new List<Song>();
index[word].Add(song);
}
}
// Also index by first few characters for prefix matching
for (int i = 3; i <= Math.Min(8, normalizedArtist.Length); i++)
{
var prefix = normalizedArtist.Substring(0, i);
if (!index.ContainsKey(prefix))
index[prefix] = new List<Song>();
index[prefix].Add(song);
}
}
return index;
}
static List<Song> GetTitleCandidates(string searchTitle, Dictionary<string, List<Song>> titleIndex)
{
var candidates = new HashSet<Song>();
var normalizedSearch = NormalizeString(searchTitle);
var searchWords = normalizedSearch.Split(' ', (char)StringSplitOptions.RemoveEmptyEntries);
// Find songs that contain any of the search words
foreach (var word in searchWords)
{
if (word.Length >= 3 && titleIndex.ContainsKey(word))
{
foreach (var song in titleIndex[word])
{
candidates.Add(song);
}
}
// Also check prefixes
for (int i = 3; i <= Math.Min(8, word.Length); i++)
{
var prefix = word.Substring(0, i);
if (titleIndex.ContainsKey(prefix))
{
foreach (var song in titleIndex[prefix])
{
song.FoundSongs.Add(item);
candidates.Add(song);
}
}
}
Console.WriteLine("Found ({0}) Song:{1} - {2}", song.FoundSongs.Count(), song.Artist, song.Title);
}
}
static bool DoesMatch(string primary, string toMatch)
{
if (primary.Contains(toMatch) || toMatch.Contains(primary)) { return true; }
int diff = primary.LevenshteinDistance(toMatch);
int distance = 3;
if (toMatch.Length < 6) { distance = 2; }
return diff < distance;
}
static SongList Download(string listName, string url)
{
DateTime now = DateTime.Now;
string title = now.Year + " - " + listName;
Console.WriteLine("Downloading " + title);
string html = DownloadHtml(url);
SongList list = null;
List<SongListSong> songs = Parse(title, html);
if (songs != null)
{
list = new SongList();
list.Title = title;
list.Songs = songs;
}
return list;
}
static List<SongListSong> Parse(string name, string html)
{
List<SongListSong> songs = null;
var parser = new HtmlParser();
var document = parser.Parse(html);
//2-?
var articles = document.QuerySelectorAll("div.chart-list-item ");
if (articles.Count() > 0)
{
Console.WriteLine("Found " + articles.Count() + " Songs");
songs = new List<SongListSong>();
}
////1
//var number1artist = document.QuerySelectorAll("div.chart-number-one__artist ").First().InnerHtml.TrimStart().TrimEnd();
//var number1title = document.QuerySelectorAll("div.chart-number-one__title ").First().InnerHtml.TrimStart().TrimEnd();
//var number1 = new SongListSong();
//number1.Artist = number1artist;
//number1.Title = number1title;
//number1.Position = 1;
//if (number1artist.Contains("href"))
//{
// var start = number1artist.IndexOf(">") + 1;
// var end = number1artist.IndexOf("<",1) - 1;
// var art = number1artist.Substring(start, end - start);
// number1.Artist = art.TrimStart().TrimEnd();
//}
//songs.Add(number1);
var i = 1;
foreach (var article in articles)
{
var title = article.Attributes["data-title"].Value;
var artist = article.Attributes["data-artist"].Value;
var position = article.Attributes["data-rank"].Value;
var song = new SongListSong();
song.Artist = artist;
song.Title = title;
song.Position = Convert.ToInt32(position);
songs.Add(song);
i++;
}
Console.Write("Parsed " + songs.Count() + " Songs");
return songs;
}
static SongList DownloadHot100(string listName, string url)
{
DateTime now = DateTime.Now;
string title = now.Year + " - " + listName;
Console.WriteLine("Downloading " + title);
string html = DownloadHtml(url);
SongList list = null;
List<SongListSong> songs = ParseHot100(title, html);
if (songs != null)
{
list = new SongList();
list.Title = title;
list.Songs = songs;
}
return list;
}
static List<SongListSong> ParseHot100(string name, string html)
{
List<SongListSong> songs = null;
var parser = new HtmlParser();
var document = parser.Parse(html);
//2-?
var cs = "data-charts=\"";
var ce = "data-icons=\"https:";
var ics = html.IndexOf(cs) + cs.Length;
var ice = html.IndexOf(ce);
var json = html.Substring(ics, ice - ics);
json = json.Replace("\"", "").Replace("&quot;", "\"").Replace("&quotquot;", "\"").Replace("&quoquot;;", "\"").Replace("&ququot;", "\"");
JArray articles = JArray.Parse(json);
if (articles.Count() > 0)
{
Console.WriteLine("Found " + articles.Count() + " Songs");
songs = new List<SongListSong>();
}
var i = 1;
foreach (var article in articles)
{
var title = (string)article["title"];
var artist = (string)article["artist_name"];
var song = new SongListSong();
song.Artist = WebUtility.HtmlDecode(artist);
song.Title = WebUtility.HtmlDecode(title);
song.Position = Convert.ToInt32(i);
songs.Add(song);
i++;
}
Console.Write("Parsed " + songs.Count() + " Songs");
return songs;
return candidates.ToList();
}
static string DownloadHtml(string url)
static List<Song> GetArtistCandidates(string searchArtist, Dictionary<string, List<Song>> artistIndex)
{
string data = null;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, errors) => true;
ServicePointManager.ServerCertificateValidationCallback = delegate { return true; };
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(url);
request.Method = "GET";
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
if (response.StatusCode == HttpStatusCode.OK)
var candidates = new HashSet<Song>();
var normalizedSearch = NormalizeString(searchArtist);
var searchWords = normalizedSearch.Split(' ', (char)StringSplitOptions.RemoveEmptyEntries);
// Find songs that contain any of the search words
foreach (var word in searchWords)
{
Stream receiveStream = response.GetResponseStream();
StreamReader readStream = null;
if (response.CharacterSet == null)
if (word.Length >= 3 && artistIndex.ContainsKey(word))
{
readStream = new StreamReader(receiveStream);
foreach (var song in artistIndex[word])
{
candidates.Add(song);
}
}
else
// Also check prefixes
for (int i = 3; i <= Math.Min(8, word.Length); i++)
{
readStream = new StreamReader(receiveStream, Encoding.GetEncoding(response.CharacterSet));
var prefix = word.Substring(0, i);
if (artistIndex.ContainsKey(prefix))
{
foreach (var song in artistIndex[prefix])
{
candidates.Add(song);
}
}
}
data = readStream.ReadToEnd();
response.Close();
readStream.Close();
}
return data;
}
}
static class StingExtension
{
public static string RemoveCrap(this String str)
{
string regex = "(\\[.*\\])|(\".*\")|('.*')|(\\(.*\\))";
return Regex.Replace(str, regex, "").ToLower().Replace("ft.", "").Replace("feat.", "").Replace("featured", "").Replace("featuring", "").Replace("'", "").Replace(" "," ").Trim();
return candidates.ToList();
}
}
}

View File

@ -15,21 +15,18 @@ namespace Herse.Models
[JsonProperty("artist")]
public string Artist { get; set; }
[JsonProperty("genre")]
public string Genre { get; set; }
[JsonProperty("path")]
public string Path { get; set; }
// [JsonProperty("guid")]
// public string Guid { get; set; }
[JsonProperty("disabled")]
public bool Disabled { get; set; } = false;
[JsonProperty("favorite")]
public bool Favorite { get; set; } = false;
[JsonIgnore]
public FileType FileType
{
@ -51,4 +48,11 @@ namespace Herse.Models
}
}
}
public class History : Song
{
[JsonProperty("count")]
public int Count { get; set; }
}
}

View File

@ -74,7 +74,7 @@ namespace KaraokePlayer
songInfoForm.Show();
if(controller.Settings == null || controller.Settings.AutoAdvance)
{
await Task.Delay(TimeSpan.FromSeconds(10));
await Task.Delay(TimeSpan.FromSeconds(5));
player.play();
controller.SetState(PlayerState.Playing);
songInfoForm.Hide();

View File

@ -28,38 +28,113 @@
/// </summary>
private void InitializeComponent()
{
this.previewLabel = new System.Windows.Forms.Label();
this.mainPanel = new System.Windows.Forms.Panel();
this.songTitleLabel = new System.Windows.Forms.Label();
this.artistLabel = new System.Windows.Forms.Label();
this.singerLabel = new System.Windows.Forms.Label();
this.upNextLabel = new System.Windows.Forms.Label();
this.mainPanel.SuspendLayout();
this.SuspendLayout();
//
// previewLabel
// mainPanel
//
this.previewLabel.Dock = System.Windows.Forms.DockStyle.Fill;
this.previewLabel.Font = new System.Drawing.Font("Century Gothic", 60F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.previewLabel.ForeColor = System.Drawing.SystemColors.ControlLightLight;
this.previewLabel.Location = new System.Drawing.Point(0, 0);
this.previewLabel.Margin = new System.Windows.Forms.Padding(3, 100, 3, 0);
this.previewLabel.Name = "previewLabel";
this.previewLabel.Padding = new System.Windows.Forms.Padding(0, 100, 0, 0);
this.previewLabel.Size = new System.Drawing.Size(1008, 729);
this.previewLabel.TabIndex = 0;
this.previewLabel.Text = "This is the first line\r\n\r\nAnd this is the second line.";
this.previewLabel.TextAlign = System.Drawing.ContentAlignment.TopCenter;
this.mainPanel.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(15)))), ((int)(((byte)(15)))), ((int)(((byte)(15)))));
this.mainPanel.Controls.Add(this.songTitleLabel);
this.mainPanel.Controls.Add(this.artistLabel);
this.mainPanel.Controls.Add(this.singerLabel);
this.mainPanel.Controls.Add(this.upNextLabel);
this.mainPanel.Dock = System.Windows.Forms.DockStyle.Fill;
this.mainPanel.Location = new System.Drawing.Point(0, 0);
this.mainPanel.Name = "mainPanel";
this.mainPanel.Size = new System.Drawing.Size(1200, 800);
this.mainPanel.TabIndex = 0;
//
// songTitleLabel
//
this.songTitleLabel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.songTitleLabel.BackColor = System.Drawing.Color.Transparent;
this.songTitleLabel.Font = new System.Drawing.Font("Segoe UI", 48F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.songTitleLabel.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(255)))), ((int)(((byte)(215)))), ((int)(((byte)(0)))));
this.songTitleLabel.Location = new System.Drawing.Point(50, 450);
this.songTitleLabel.Name = "songTitleLabel";
this.songTitleLabel.Size = new System.Drawing.Size(1100, 200);
this.songTitleLabel.TabIndex = 3;
this.songTitleLabel.Text = "Song Title";
this.songTitleLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
this.songTitleLabel.UseCompatibleTextRendering = true;
//
// artistLabel
//
this.artistLabel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.artistLabel.BackColor = System.Drawing.Color.Transparent;
this.artistLabel.Font = new System.Drawing.Font("Segoe UI", 36F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.artistLabel.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(200)))), ((int)(((byte)(200)))), ((int)(((byte)(200)))));
this.artistLabel.Location = new System.Drawing.Point(50, 350);
this.artistLabel.Name = "artistLabel";
this.artistLabel.Size = new System.Drawing.Size(1100, 80);
this.artistLabel.TabIndex = 2;
this.artistLabel.Text = "Artist Name";
this.artistLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
this.artistLabel.UseCompatibleTextRendering = true;
//
// singerLabel
//
this.singerLabel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.singerLabel.BackColor = System.Drawing.Color.Transparent;
this.singerLabel.Font = new System.Drawing.Font("Segoe UI", 42F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.singerLabel.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(0)))), ((int)(((byte)(255)))), ((int)(((byte)(255)))));
this.singerLabel.Location = new System.Drawing.Point(50, 200);
this.singerLabel.Name = "singerLabel";
this.singerLabel.Size = new System.Drawing.Size(1100, 120);
this.singerLabel.TabIndex = 1;
this.singerLabel.Text = "Singer Name";
this.singerLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
this.singerLabel.UseCompatibleTextRendering = true;
//
// upNextLabel
//
this.upNextLabel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom)
| System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.upNextLabel.BackColor = System.Drawing.Color.Transparent;
this.upNextLabel.Font = new System.Drawing.Font("Segoe UI", 24F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
this.upNextLabel.ForeColor = System.Drawing.Color.FromArgb(((int)(((byte)(255)))), ((int)(((byte)(128)))), ((int)(((byte)(0)))));
this.upNextLabel.Location = new System.Drawing.Point(50, 100);
this.upNextLabel.Name = "upNextLabel";
this.upNextLabel.Size = new System.Drawing.Size(1100, 60);
this.upNextLabel.TabIndex = 0;
this.upNextLabel.Text = "UP NEXT";
this.upNextLabel.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
this.upNextLabel.UseCompatibleTextRendering = true;
//
// SongInfoForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.Black;
this.ClientSize = new System.Drawing.Size(1008, 729);
this.Controls.Add(this.previewLabel);
this.ClientSize = new System.Drawing.Size(1200, 800);
this.Controls.Add(this.mainPanel);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
this.Name = "SongInfoForm";
this.Text = "SongInfoForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "Karaoke - Up Next";
this.mainPanel.ResumeLayout(false);
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.Label previewLabel;
private System.Windows.Forms.Panel mainPanel;
private System.Windows.Forms.Label upNextLabel;
private System.Windows.Forms.Label singerLabel;
private System.Windows.Forms.Label artistLabel;
private System.Windows.Forms.Label songTitleLabel;
}
}

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@ -14,17 +15,159 @@ namespace KaraokePlayer
{
public partial class SongInfoForm : Form
{
private Timer fadeTimer;
private Timer pulseTimer;
private int fadeAlpha = 0;
private bool isFadingIn = true;
private int pulseAlpha = 255;
private bool pulseDirection = false;
public SongInfoForm()
{
InitializeComponent();
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
this.WindowState = FormWindowState.Maximized;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
this.WindowState = FormWindowState.Normal;
this.ShowInTaskbar = false;
this.TopMost = true;
// Enable double buffering to prevent flickering
SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true);
// Add paint event for gradient background
mainPanel.Paint += MainPanel_Paint;
// Setup fade timer
fadeTimer = new Timer();
fadeTimer.Interval = 20;
fadeTimer.Tick += FadeTimer_Tick;
// Setup pulse timer for "UP NEXT" label
pulseTimer = new Timer();
pulseTimer.Interval = 50;
pulseTimer.Tick += PulseTimer_Tick;
// Start fade in effect
StartFadeIn();
}
private void StartFadeIn()
{
fadeAlpha = 0;
isFadingIn = true;
fadeTimer.Start();
pulseTimer.Start();
}
private void PulseTimer_Tick(object sender, EventArgs e)
{
if (pulseDirection)
{
pulseAlpha += 5;
if (pulseAlpha >= 255)
{
pulseAlpha = 255;
pulseDirection = false;
}
}
else
{
pulseAlpha -= 5;
if (pulseAlpha <= 150)
{
pulseAlpha = 150;
pulseDirection = true;
}
}
upNextLabel.ForeColor = System.Drawing.Color.FromArgb(pulseAlpha, 255, 128, 0);
}
private void FadeTimer_Tick(object sender, EventArgs e)
{
if (isFadingIn)
{
fadeAlpha += 10;
if (fadeAlpha >= 255)
{
fadeAlpha = 255;
fadeTimer.Stop();
}
}
else
{
fadeAlpha -= 10;
if (fadeAlpha <= 0)
{
fadeAlpha = 0;
fadeTimer.Stop();
this.Hide();
}
}
// Update opacity
this.Opacity = fadeAlpha / 255.0;
mainPanel.Invalidate();
}
private void MainPanel_Paint(object sender, PaintEventArgs e)
{
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit;
// Create gradient background
using (LinearGradientBrush brush = new LinearGradientBrush(
mainPanel.ClientRectangle,
System.Drawing.Color.FromArgb(30, 30, 50),
System.Drawing.Color.FromArgb(20, 20, 30),
LinearGradientMode.Vertical))
{
e.Graphics.FillRectangle(brush, mainPanel.ClientRectangle);
}
// Add subtle radial gradient overlay for depth
using (GraphicsPath path = new GraphicsPath())
{
path.AddEllipse(-300, -300, mainPanel.Width + 600, mainPanel.Height + 600);
using (PathGradientBrush pgb = new PathGradientBrush(path))
{
pgb.CenterColor = System.Drawing.Color.FromArgb(40, 40, 80);
pgb.SurroundColors = new System.Drawing.Color[] { System.Drawing.Color.FromArgb(0, 0, 0) };
e.Graphics.FillPath(pgb, path);
}
}
// Add subtle border glow
using (Pen glowPen = new Pen(System.Drawing.Color.FromArgb(50, 100, 200), 2))
{
glowPen.LineJoin = LineJoin.Round;
e.Graphics.DrawRectangle(glowPen, 2, 2, mainPanel.Width - 4, mainPanel.Height - 4);
}
}
public void Update(QueueItem queueItem)
{
previewLabel.Text = "Up Next: " + queueItem.Singer.Name + "\r\n\r\n" + queueItem.Song.Artist + "\r\n\r\n" + queueItem.Song.Title;
if (queueItem?.Singer != null)
{
singerLabel.Text = queueItem.Singer.Name;
}
if (queueItem?.Song != null)
{
artistLabel.Text = queueItem.Song.Artist;
songTitleLabel.Text = queueItem.Song.Title;
}
// Force a repaint to update the gradient
mainPanel.Invalidate();
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
fadeTimer?.Stop();
fadeTimer?.Dispose();
pulseTimer?.Stop();
pulseTimer?.Dispose();
base.OnFormClosing(e);
}
}
}

View File

@ -11,76 +11,214 @@ using System.Threading.Tasks;
using System.IO.Compression;
using System.Configuration;
using Newtonsoft.Json;
using System.Security.Cryptography;
namespace SongCrawler
{
/// <summary>
/// Main program class for the SongCrawler application.
/// Handles crawling, indexing, and managing karaoke songs from file systems and Firebase.
/// </summary>
class Program
{
#region Properties and Configuration
/// <summary>
/// Controller identifier from command line arguments
/// </summary>
private static string Controller { get; set; }
/// <summary>
/// Path to the songs folder from command line arguments
/// </summary>
private static string SongsFolderPath { get; set; }
/// <summary>
/// Original command line arguments array
/// </summary>
private static string[] Args { get; set; }
/// <summary>
/// Shared Firebase client instance
/// </summary>
private static FireSharp.FirebaseClient FirebaseClient { get; set; }
#endregion
#region Main Entry Point
/// <summary>
/// Main entry point for the application.
/// Initializes properties from command line arguments and routes to appropriate functionality.
/// </summary>
/// <param name="args">Command line arguments: [controller] [songspath] [optional: delete]</param>
static void Main(string[] args)
{
InitializeFromArgs(args);
if (args.Count() == 3)
{
DeleteSongs(args);
DeleteSongs();
}
else {
CrawlSongs(args);
else
{
CrawlSongs();
}
}
private static void CrawlSongs(string[] args)
#endregion
#region Initialization and Setup
/// <summary>
/// Initializes application properties from command line arguments.
/// Sets up Controller, SongsFolderPath, Args, and FirebaseClient.
/// </summary>
/// <param name="args">Command line arguments array</param>
private static void InitializeFromArgs(string[] args)
{
//string [] test = { "mbrucedogs", "z://" };
//args = test;
if (args.Length != 2)
Args = args;
if (args.Length > 0)
{
Console.WriteLine("usage: songcrawler partyid songspath");
return;
Controller = args[0];
}
string controller = args[0];
string songpath = args[1];
if (args.Length > 1)
{
SongsFolderPath = args[1];
}
FirebaseClient = CreateFirebaseClient();
}
/// <summary>
/// Creates and configures a Firebase client using settings from App.config.
/// </summary>
/// <returns>Configured Firebase client instance</returns>
private static FireSharp.FirebaseClient CreateFirebaseClient()
{
IFirebaseConfig config = new FirebaseConfig
{
AuthSecret = ConfigurationManager.AppSettings["Firebase.Secret"],
BasePath = ConfigurationManager.AppSettings["Firebase.Path"]
};
FireSharp.FirebaseClient client = new FireSharp.FirebaseClient(config);
string songsPath = string.Format("controllers/{0}/songs", controller);
string favoritesPath = string.Format("controllers/{0}/favorites", controller);
string disabledPath = string.Format("controllers/{0}/disabled", controller);
Console.WriteLine("Loading current library");
return new FireSharp.FirebaseClient(config);
}
List<Song> songs = null; //client.Get(songsPath).ResultAs<List<Song>>();
List<Song> disabled = null;
List<Song> favorited = null;
/// <summary>
/// Validates command line arguments against expected length.
/// </summary>
/// <param name="args">Arguments array to validate</param>
/// <param name="expectedLength">Expected number of arguments</param>
/// <param name="usage">Usage message to display if validation fails</param>
private static void ValidateArgs(string[] args, int expectedLength, string usage)
{
if (args.Length != expectedLength)
{
Console.WriteLine(usage);
return;
}
}
#endregion
#region Firebase Utilities
/// <summary>
/// Generates Firebase path for a specific controller and data type.
/// </summary>
/// <param name="controller">Controller identifier</param>
/// <param name="pathType">Type of data (songs, favorites, disabled, etc.)</param>
/// <returns>Formatted Firebase path</returns>
private static string GetControllerPath(string controller, string pathType)
{
return string.Format("controllers/{0}/{1}", controller, pathType);
}
/// <summary>
/// Loads data from Firebase with fallback handling for different response formats.
/// </summary>
/// <typeparam name="T">Type of data to load</typeparam>
/// <param name="client">Firebase client instance</param>
/// <param name="path">Firebase path to load from</param>
/// <returns>List of loaded objects</returns>
private static List<T> LoadFirebaseData<T>(FireSharp.FirebaseClient client, string path) where T : class
{
try
{
disabled = client.Get(disabledPath).ResultAs<List<Song>>();
return client.Get(path).ResultAs<List<T>>();
}
catch
{
disabled = convertToList(client.Get(disabledPath).Body);
return convertToList<T>(client.Get(path).Body);
}
}
try
/// <summary>
/// Converts dynamic JSON response to strongly-typed list.
/// Handles Firebase responses that don't deserialize directly to List<T>.
/// </summary>
/// <typeparam name="T">Type to convert to</typeparam>
/// <param name="json">Dynamic JSON response</param>
/// <returns>List of converted objects</returns>
private static List<T> convertToList<T>(dynamic json) where T : class
{
dynamic data = JsonConvert.DeserializeObject<dynamic>(json);
var list = new List<T>();
foreach (var itemDynamic in data)
{
favorited = client.Get(favoritesPath).ResultAs<List<Song>>();
}
catch
{
favorited = convertToList(client.Get(favoritesPath).Body);
var fjson = itemDynamic.Value.ToString();
var f = JsonConvert.DeserializeObject<T>(fjson);
list.Add(f);
}
return list;
}
songs = new List<Song>();
#endregion
client.Set(songsPath, songs);
#region File System Operations
/// <summary>
/// Scans directory for all supported music file types (mp3, mp4, zip).
/// </summary>
/// <param name="songpath">Directory path to scan</param>
/// <returns>List of all music file paths found</returns>
private static List<string> GetAllMusicFiles(string songpath)
{
List<string> files = new List<string>();
files.AddRange(FindFiles("mp4", songpath));
files.AddRange(FindFiles("mp3", songpath));
files.AddRange(FindFiles("zip", songpath));
return files;
}
/// <summary>
/// Finds all files with specified extension in directory and subdirectories.
/// </summary>
/// <param name="ext">File extension to search for</param>
/// <param name="path">Directory path to search</param>
/// <returns>Array of file paths found</returns>
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
/// <summary>
/// Processes a list of files and converts them to Song objects.
/// Handles duplicate checking, debug limits, and error handling.
/// </summary>
/// <param name="files">List of file paths to process</param>
/// <param name="songs">List to add processed songs to</param>
/// <param name="checkDuplicates">Whether to check for duplicate songs</param>
/// <param name="debug">Whether to run in debug mode (limited processing)</param>
/// <param name="debugLimit">Maximum number of files to process in debug mode</param>
private static void ProcessFilesToSongs(List<string> files, List<Song> songs, bool checkDuplicates = false, bool debug = false, int debugLimit = 1000)
{
Song song = null;
int i = 0;
foreach (string filepath in files)
@ -90,259 +228,38 @@ namespace SongCrawler
{
song = MakeSong(filepath);
Console.WriteLine(string.Format("{0:000000}/{1} - {2} - {3}", i, files.Count, song.Artist, song.Title));
songs.Add(song);
if (checkDuplicates)
{
if (!songs.Any(s => s.Title.ToLower() == song.Title.ToLower() && s.Artist.ToLower() == song.Artist.ToLower()))
{
songs.Add(song);
}
}
else
{
songs.Add(song);
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
//sync all favorite, history, disabled
if (favorited != null && favorited.Count > 0)
{
favorited.ForEach(s =>
if (debug && i > debugLimit)
{
if (s != null)
{
var found = songs.Find(ls => s.Path.ToLower() == ls.Path.ToLower());
if (found != null)
{
found.Favorite = true;
}
}
});
}
if (disabled != null && disabled.Count > 0)
{
disabled.ForEach(s =>
{
if (s != null)
{
var found = songs.Find(ls => s.Path.ToLower() == ls.Path.ToLower());
if (found != null)
{
found.Disabled = true;
}
}
});
}
client.Set(songsPath, songs);
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 = string.Format("controllers/{0}/newSongs", controller);
client.Set(newSongs, added);
}
private class PathOnly
{
[JsonProperty("path")]
public String Path { get; set; }
public PathOnly(string path)
{
this.Path = path;
}
}
private static List<Song> convertToList(dynamic json)
{
dynamic data = JsonConvert.DeserializeObject<dynamic>(json);
var list = new List<Song>();
foreach (var itemDynamic in data)
{
var fjson = itemDynamic.Value.ToString();
var f = JsonConvert.DeserializeObject<Song>(fjson);
list.Add(f);
}
return list;
}
private static void DeleteSongs(string[] args)
{
if (args.Length != 3)
{
Console.WriteLine("usage: songcrawler partyid songspath delete");
return;
}
string controller = args[0];
string songpath = args[1];
IFirebaseConfig config = new FirebaseConfig
{
AuthSecret = ConfigurationManager.AppSettings["Firebase.Secret"],
BasePath = ConfigurationManager.AppSettings["Firebase.Path"]
};
FireSharp.FirebaseClient client = new FireSharp.FirebaseClient(config);
string firepath = string.Format("controllers/{0}/songs", controller);
Console.WriteLine("Deleting Songs ...");
List<Song> songs = new List<Song>();
client.Set(firepath, songs);
}
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(string[] args)
{
if (args.Length != 2)
{
Console.WriteLine("usage: songcrawler partyid songspath");
return;
}
string controller = args[0];
string songpath = args[1];
IFirebaseConfig config = new FirebaseConfig
{
AuthSecret = ConfigurationManager.AppSettings["Firebase.Secret"],
BasePath = ConfigurationManager.AppSettings["Firebase.Path"]
};
FireSharp.FirebaseClient client = new FireSharp.FirebaseClient(config);
string firepath = string.Format("controllers/{0}/songs", controller);
Console.WriteLine("Loading current library");
List<Song> songs = new List<Song>();
List<string> files = new List<string>();
files.AddRange(FindFiles("mp4", songpath));
files.AddRange(FindFiles("mp3", songpath));
files.AddRange(FindFiles("zip", songpath));
Song song = null;
int i = 0;
foreach (string filepath in files)
{
i++;
try
{
song = MakeSong(filepath);
Console.WriteLine(string.Format("{0:000000}/{1} - {2}", i, files.Count(), song.Title));
if (!songs.Any(s => s.Title.ToLower() == song.Title.ToLower() && s.Artist.ToLower() == song.Artist.ToLower())) songs.Add(song);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
Console.WriteLine(string.Format("{0:000000}/{1} unique songs", songs.Count(), files.Count()));
client.Set(firepath, songs);
}
private static void FindDuplicates(string[] args)
{
string songpath = @"D:\KaraokeData\Karaoke"; // args[0];
List<Song> songs = songs = new List<Song>();
List<string> files = new List<string>();
files.AddRange(FindFiles("mp3", songpath));
files.AddRange(FindFiles("zip", songpath));
files.AddRange(FindFiles("mp4", songpath));
Song song = null;
int i = 0;
foreach (string filepath in files)
{
i++;
try
{
song = MakeSong(filepath);
Console.WriteLine(string.Format("{0:000000}/{1} - {2}", i, files.Count, song.Title));
songs.Add(song);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
break;
}
}
i = 0;
Dictionary<string, List<Song>> dupes = new Dictionary<string, List<Song>>();
foreach (var localsong in songs)
{
i++;
Console.WriteLine(string.Format("Checking for {0:000000}/{1}) {2} - {3}", i, files.Count, localsong.Artist, localsong.Title));
if (!string.IsNullOrEmpty(localsong.Artist) && !string.IsNullOrEmpty(localsong.Title))
{
string key = localsong.Artist + " - " + localsong.Title;
if (dupes.ContainsKey(key) == false)
{
var dsongs = songs.Where(s => s.Title.ToLower() == localsong.Title.ToLower() && s.Artist.ToLower() == localsong.Artist.ToLower());
if (dsongs.Count() > 1)
{
List<Song> d = new List<Song>();
d.AddRange(dsongs.ToList());
dupes.Add(key, d);
}
}
}
}
File.WriteAllText(@"D:\dupliates.json", JsonConvert.SerializeObject(dupes.OrderBy(o => o.Key)));
}
private static void DisableDuplicates(string[] args)
{
if (args.Length != 1)
{
Console.WriteLine("usage: songcrawler partyid songspath");
return;
}
string controller = args[0];
IFirebaseConfig config = new FirebaseConfig
{
AuthSecret = ConfigurationManager.AppSettings["Firebase.Secret"],
BasePath = ConfigurationManager.AppSettings["Firebase.Path"]
};
FireSharp.FirebaseClient client = new FireSharp.FirebaseClient(config);
string firepath = string.Format("controllers/{0}/songs", controller);
Console.WriteLine("Loading current library");
List<Song> songs = client.Get(firepath).ResultAs<List<Song>>();
Dictionary<string, List<Song>> dupes = new Dictionary<string, List<Song>>();
int i = 0;
foreach (var localsong in songs)
{
i++;
Console.WriteLine(string.Format("Checking for {0:000000}/{1}) {2} - {3}", i, songs.Count, localsong.Artist, localsong.Title));
if (!string.IsNullOrEmpty(localsong.Artist) && !string.IsNullOrEmpty(localsong.Title) && localsong.Disabled == false && localsong.Path.Contains(".mp4"))
{
string key = localsong.Artist + " - " + localsong.Title;
if (dupes.ContainsKey(key) == false)
{
var dsongs = songs.Where(s =>
s.Path != localsong.Path &&
s.Title.ToLower() == localsong.Title.ToLower() &&
s.Artist.ToLower() == localsong.Artist.ToLower() &&
localsong.Disabled == false);
if (dsongs.Count() > 1)
{
List<Song> d = new List<Song>();
d.AddRange(dsongs.ToList());
dupes.Add(key, d);
}
foreach (var item in dsongs)
{
item.Disabled = true;
}
}
}
}
client.Set(firepath, songs);
}
/// <summary>
/// Creates a Song object from a file path.
/// Handles different file types (mp3, mp4, zip) and extracts metadata.
/// </summary>
/// <param name="filepath">Path to the music file</param>
/// <returns>Song object with extracted metadata</returns>
private static Song MakeSong(string filepath)
{
Song song = null;
var ext = Path.GetExtension(filepath).ToLower();
switch (ext)
@ -357,17 +274,51 @@ namespace SongCrawler
}
CheckTitle(song);
song.Path = filepath;
return song;
return song;
}
private static string[] FindFiles(string ext, string path)
/// <summary>
/// Reads ID3 tags from audio files (mp3, mp4).
/// </summary>
/// <param name="path">Path to audio file</param>
/// <returns>Song object with extracted metadata</returns>
static Song ReadId3(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;
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;
}
/// <summary>
/// Reads ID3 tags from mp3 files inside zip archives.
/// </summary>
/// <param name="zippath">Path to zip file</param>
/// <returns>Song object with extracted metadata</returns>
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;
}
/// <summary>
/// Extracts title and artist from filename if ID3 tags are missing.
/// Assumes format: "Artist - Title" or just "Title".
/// </summary>
/// <param name="song">Song object to update</param>
private static void CheckTitle(Song song)
{
if(string.IsNullOrEmpty(song.Title))
@ -386,34 +337,286 @@ namespace SongCrawler
}
}
private static Song ReadId3FromZip(string zippath)
#endregion
#region Song Management
/// <summary>
/// Synchronizes song properties from source lists using path matching.
/// Used to restore favorites and disabled states after re-indexing.
/// </summary>
/// <param name="songs">Target song list to update</param>
/// <param name="sourceList">Source list containing property values</param>
/// <param name="propertySetter">Action to set the property on matching songs</param>
private static void SyncSongProperties(List<Song> songs, List<Song> sourceList, Action<Song> propertySetter)
{
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;
if (sourceList != null && sourceList.Count > 0)
{
sourceList.ForEach(s =>
{
if (s != null)
{
var found = songs.Find(ls => s.Path.ToLower() == ls.Path.ToLower());
if (found != null)
{
propertySetter(found);
}
}
});
}
}
static Song ReadId3(string path)
/// <summary>
/// Finds duplicate songs based on artist and title matching.
/// </summary>
/// <param name="songs">List of songs to check for duplicates</param>
/// <param name="filter">Optional filter to apply before duplicate checking</param>
/// <returns>Dictionary of duplicate groups keyed by "Artist - Title"</returns>
private static Dictionary<string, List<Song>> FindDuplicateSongs(List<Song> songs, Func<Song, bool> filter = null)
{
Song song = new Song();
TagLib.File tagFile;
Dictionary<string, List<Song>> dupes = new Dictionary<string, List<Song>>();
int i = 0;
foreach (var localsong in songs)
{
i++;
Console.WriteLine(string.Format("Checking for {0:000000}/{1}) {2} - {3}", i, songs.Count, localsong.Artist, localsong.Title));
if (!string.IsNullOrEmpty(localsong.Artist) && !string.IsNullOrEmpty(localsong.Title))
{
if (filter != null && !filter(localsong))
continue;
string key = localsong.Artist + " - " + localsong.Title;
if (!dupes.ContainsKey(key))
{
var dsongs = songs.Where(s => s.Title.ToLower() == localsong.Title.ToLower() && s.Artist.ToLower() == localsong.Artist.ToLower());
if (dsongs.Count() > 1)
{
List<Song> d = new List<Song>();
d.AddRange(dsongs.ToList());
dupes.Add(key, d);
}
}
}
}
return dupes;
}
#endregion
#region Core Operations
/// <summary>
/// Main crawling operation. Scans music files, creates song objects, and uploads to Firebase.
/// Maintains favorites and disabled states from previous runs.
/// </summary>
private static void CrawlSongs()
{
var debug = false;
ValidateArgs(Args, 2, "usage: songcrawler partyid songspath");
if (Args.Length != 2) return;
string songsPath = GetControllerPath(Controller, "songs");
string favoritesPath = GetControllerPath(Controller, "favorites");
string disabledPath = GetControllerPath(Controller, "disabled");
Console.WriteLine("Loading current library");
List<Song> songs = new List<Song>();
List<Song> disabled = LoadFirebaseData<Song>(FirebaseClient, disabledPath);
List<Song> favorited = LoadFirebaseData<Song>(FirebaseClient, favoritesPath);
FirebaseClient.Set(songsPath, songs);
List<string> files = GetAllMusicFiles(SongsFolderPath);
ProcessFilesToSongs(files, songs, debug: debug, debugLimit: 1000);
//sync all favorite, history, disabled
SyncSongProperties(songs, favorited, song => song.Favorite = true);
SyncSongProperties(songs, disabled, song => song.Disabled = true);
FirebaseClient.Set(songsPath, songs);
// 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);
}
/// <summary>
/// Crawls songs without allowing duplicates based on artist and title.
/// </summary>
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<Song> songs = new List<Song>();
List<string> 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);
}
/// <summary>
/// Deletes all songs for the specified controller.
/// </summary>
private static void DeleteSongs()
{
ValidateArgs(Args, 3, "usage: songcrawler partyid songspath delete");
if (Args.Length != 3) return;
string firepath = GetControllerPath(Controller, "songs");
Console.WriteLine("Deleting Songs ...");
List<Song> songs = new List<Song>();
FirebaseClient.Set(firepath, songs);
}
#endregion
#region Maintenance Operations
/// <summary>
/// Fixes newSongs list by replacing path-only entries with full song objects.
/// </summary>
private static void fixNewSongs()
{
string songsPath = GetControllerPath(Controller, "songs");
string newSongsPath = GetControllerPath(Controller, "newSongs");
List<Song> songs = FirebaseClient.Get(songsPath).ResultAs<List<Song>>();
List<Song> newSongs = FirebaseClient.Get(newSongsPath).ResultAs<List<Song>>();
List<Song> updated = new List<Song>();
foreach (Song n in newSongs){
var found = songs.First(s => s.Path == n.Path);
if(found != null)
{
updated.Add(found);
}
}
FirebaseClient.Set(newSongsPath, updated);
}
/// <summary>
/// Fixes history list by ensuring proper JSON structure.
/// </summary>
private static void fixHistory()
{
string historyPath = GetControllerPath(Controller, "history");
List<History> history = null;
try
{
tagFile = TagLib.File.Create(path);
song.Title = tagFile.Tag.Title.Trim();
song.Artist = tagFile.Tag.FirstPerformer.Trim();
if (tagFile.Tag.FirstGenre != null) {
song.Genre = tagFile.Tag.FirstGenre.Trim();
}
history = FirebaseClient.Get(historyPath).ResultAs<List<History>>();
}
catch
{
// do nothing;
dynamic data = JsonConvert.DeserializeObject<dynamic>(FirebaseClient.Get(historyPath).Body);
history = new List<History>();
foreach (var itemDynamic in data)
{
var fjson = itemDynamic.Value.ToString();
var f = JsonConvert.DeserializeObject<History>(fjson);
history.Add(f);
}
}
song.Path = path;
return song;
FirebaseClient.Set(historyPath, history);
}
/// <summary>
/// Finds and disables duplicate songs, keeping only the first occurrence.
/// Only processes mp4 files that are not already disabled.
/// </summary>
private static void DisableDuplicates()
{
ValidateArgs(Args, 1, "usage: songcrawler partyid songspath");
if (Args.Length != 1) return;
string firepath = GetControllerPath(Controller, "songs");
Console.WriteLine("Loading current library");
List<Song> songs = FirebaseClient.Get(firepath).ResultAs<List<Song>>();
var dupes = FindDuplicateSongs(songs, song => !song.Disabled && song.Path.Contains(".mp4"));
// Disable duplicate songs (keep the first one, disable the rest)
foreach (var duplicateGroup in dupes.Values)
{
for (int i = 1; i < duplicateGroup.Count; i++) // Skip first one, disable the rest
{
duplicateGroup[i].Disabled = true;
}
}
FirebaseClient.Set(firepath, songs);
}
/// <summary>
/// Finds duplicates in a local directory and exports to JSON file.
/// </summary>
/// <param name="args">Command line arguments (not used, hardcoded path)</param>
private static void FindDuplicates(string[] args)
{
string songpath = @"D:\KaraokeData\Karaoke"; // args[0];
List<Song> songs = new List<Song>();
List<string> files = GetAllMusicFiles(songpath);
ProcessFilesToSongs(files, songs);
var dupes = FindDuplicateSongs(songs);
File.WriteAllText(@"D:\dupliates.json", JsonConvert.SerializeObject(dupes.OrderBy(o => o.Key)));
}
#endregion
#region Utility Classes
/// <summary>
/// Represents a song with its creation timestamp for sorting.
/// </summary>
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;
}
}
/// <summary>
/// Lightweight class for storing only file paths in newSongs list.
/// </summary>
private class PathOnly
{
[JsonProperty("path")]
public String Path { get; set; }
public PathOnly(string path)
{
this.Path = path;
}
}
/// <summary>
/// Generates a consistent GUID from a string input using MD5 hashing.
/// </summary>
/// <param name="input">String to generate GUID from</param>
/// <returns>Consistent GUID for the input string</returns>
private static Guid GuidFromString(string input)
{
using (MD5 md5 = MD5.Create())
{
byte[] hash = md5.ComputeHash(Encoding.Default.GetBytes(input));
return new Guid(hash);
}
}
#endregion
}
}