using AngleSharp.Parser.Html;
using DuoVia.FuzzyStrings;
using FireSharp.Config;
using FireSharp.Interfaces;
using Herse.Models;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace SongListUpdater
{
class Program
{
static List songs = null;
static List songList = null;
static void Main(string[] args)
{
//args = new string[] { "mbrucedogs" };
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);
songs = client.Get(string.Format("controllers/{0}/songs", controller)).ResultAs>();
string firepath = "songList";
Console.WriteLine("Loading current library");
songList = client.Get(firepath).ResultAs>();
if (songList != null)
Console.WriteLine(string.Format("{0} songList loaded", songList.Count));
else
songList = new List();
////RunTest();
//// 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 UpdateSearchLists()
{
//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($"Matching Controllers Songs for {list.Title}");
Console.WriteLine($"********************************************************");
Search(list);
});
}
static void Search(SongList list)
{
if (list.Songs == null || songs == null)
return;
// 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)
{
// 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
{
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> BuildTitleIndex(List songs)
{
var index = new Dictionary>();
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();
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();
index[prefix].Add(song);
}
}
return index;
}
static Dictionary> BuildArtistIndex(List songs)
{
var index = new Dictionary>();
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();
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();
index[prefix].Add(song);
}
}
return index;
}
static List GetTitleCandidates(string searchTitle, Dictionary> titleIndex)
{
var candidates = new HashSet();
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])
{
candidates.Add(song);
}
}
}
}
return candidates.ToList();
}
static List GetArtistCandidates(string searchArtist, Dictionary> artistIndex)
{
var candidates = new HashSet();
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)
{
if (word.Length >= 3 && artistIndex.ContainsKey(word))
{
foreach (var song in artistIndex[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 (artistIndex.ContainsKey(prefix))
{
foreach (var song in artistIndex[prefix])
{
candidates.Add(song);
}
}
}
}
return candidates.ToList();
}
}
}