Compare commits
10 Commits
d40ab93df2
...
cb1e1985c3
| Author | SHA1 | Date | |
|---|---|---|---|
| cb1e1985c3 | |||
| 8bec97afc4 | |||
| 02159a1420 | |||
| fbaba1427a | |||
| 86ffb58df4 | |||
| c1d2ecafc5 | |||
| 2b6a4a5c75 | |||
| 7173aec7ee | |||
| d087d2d393 | |||
| c2ae664d80 |
@ -24,7 +24,7 @@ namespace BillboardPlaylistUpdater
|
|||||||
|
|
||||||
static void Main(string[] args)
|
static void Main(string[] args)
|
||||||
{
|
{
|
||||||
//args = new string[] { "mbrucedogstest" };
|
//args = new string[] { "mbrucedogs" };
|
||||||
if (args.Length != 1)
|
if (args.Length != 1)
|
||||||
{
|
{
|
||||||
Console.WriteLine("usage: songcrawler partyid songspath");
|
Console.WriteLine("usage: songcrawler partyid songspath");
|
||||||
@ -49,282 +49,386 @@ namespace BillboardPlaylistUpdater
|
|||||||
else
|
else
|
||||||
songList = new List<SongList>();
|
songList = new List<SongList>();
|
||||||
|
|
||||||
RunTest();
|
////RunTest();
|
||||||
//update Shared Charts and save
|
|
||||||
UpdateCurrentCharts();
|
|
||||||
client.Set(firepath, songList);
|
|
||||||
|
|
||||||
//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);
|
client.Set(string.Format("controllers/{0}/songList", controller), songList);
|
||||||
UpdateSearchLists();
|
UpdateSearchLists();
|
||||||
client.Set(string.Format("controllers/{0}/songList", controller), songList);
|
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()
|
static void UpdateSearchLists()
|
||||||
{
|
{
|
||||||
//update the controller SongLists
|
//update the controller SongLists using parallel processing
|
||||||
foreach (var list in songList)
|
Console.WriteLine($"Processing {songList.Count} lists in parallel...");
|
||||||
|
|
||||||
|
var options = new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount };
|
||||||
|
|
||||||
|
Parallel.ForEach(songList, options, list =>
|
||||||
{
|
{
|
||||||
Console.WriteLine("********************************************************");
|
Console.WriteLine($"********************************************************");
|
||||||
Console.WriteLine(string.Format("Matching Controllers Songs for {0}", list.Title));
|
Console.WriteLine($"Matching Controllers Songs for {list.Title}");
|
||||||
Console.WriteLine("********************************************************");
|
Console.WriteLine($"********************************************************");
|
||||||
Search(list);
|
Search(list);
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static void Search(SongList list)
|
static void Search(SongList list)
|
||||||
{
|
{
|
||||||
foreach (var song in list.Songs)
|
if (list.Songs == null || songs == null)
|
||||||
{
|
return;
|
||||||
song.FoundSongs.Clear();
|
|
||||||
var bA = song.Artist.RemoveCrap().ToLower();
|
|
||||||
var bT = song.Title.RemoveCrap().ToLower();
|
|
||||||
|
|
||||||
foreach (var item in songs)
|
// Pre-filter disabled songs once
|
||||||
{
|
var availableSongs = songs.Where(s => !s.Disabled).ToList();
|
||||||
if (item.Artist != null && item.Title != null)
|
Console.WriteLine($"Searching through {availableSongs.Count} available songs for {list.Songs.Count} songs in list...");
|
||||||
{
|
|
||||||
var t = item.Title.RemoveCrap().ToLower();
|
|
||||||
var a = item.Artist.RemoveCrap().ToLower();
|
|
||||||
bool titleMatch = DoesMatch(bT, t);
|
|
||||||
if (titleMatch && DoesMatch(bA, a))
|
|
||||||
{
|
|
||||||
song.FoundSongs.Add(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Console.WriteLine("Found ({0}) Song:{1} - {2}", song.FoundSongs.Count(), song.Artist, song.Title);
|
|
||||||
|
|
||||||
|
// 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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool DoesMatch(string primary, string toMatch)
|
// Sort matches by relevance and file type priority
|
||||||
{
|
songListItem.FoundSongs = songMatches
|
||||||
if (primary.Contains(toMatch) || toMatch.Contains(primary)) { return true; }
|
.OrderByDescending(x => x.score)
|
||||||
int diff = primary.LevenshteinDistance(toMatch);
|
.ThenBy(x => GetFileTypePriority(x.song))
|
||||||
int distance = 3;
|
.ThenBy(x => GetChannelPriority(x.song))
|
||||||
if (toMatch.Length < 6) { distance = 2; }
|
.Take(10) // Limit to top 10 matches
|
||||||
return diff < distance;
|
.Select(x => x.song)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Console.WriteLine($" Total matches found: {songListItem.FoundSongs.Count}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static SongList Download(string listName, string url)
|
static double CalculateSimilarity(string str1, string str2)
|
||||||
{
|
{
|
||||||
DateTime now = DateTime.Now;
|
if (string.IsNullOrEmpty(str1) || string.IsNullOrEmpty(str2))
|
||||||
string title = now.Year + " - " + listName;
|
return 0.0;
|
||||||
|
|
||||||
Console.WriteLine("Downloading " + title);
|
// Normalize strings for comparison
|
||||||
|
str1 = NormalizeString(str1);
|
||||||
|
str2 = NormalizeString(str2);
|
||||||
|
|
||||||
string html = DownloadHtml(url);
|
// 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));
|
||||||
|
|
||||||
SongList list = null;
|
// Use Longest Common Subsequence as a third algorithm
|
||||||
List<SongListSong> songs = Parse(title, html);
|
double lcsScore = CalculateLCSSimilarity(str1, str2);
|
||||||
if (songs != null)
|
|
||||||
{
|
// Weighted average of different algorithms
|
||||||
list = new SongList();
|
return (diceCoeff * 0.4) + (levenshtein * 0.3) + (lcsScore * 0.3);
|
||||||
list.Title = title;
|
|
||||||
list.Songs = songs;
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<SongListSong> Parse(string name, string html)
|
static double CalculateCombinedSimilarity(SongListSong songListItem, Song song)
|
||||||
{
|
{
|
||||||
List<SongListSong> songs = null;
|
double titleSimilarity = CalculateSimilarity(songListItem.Title, song.Title);
|
||||||
var parser = new HtmlParser();
|
double artistSimilarity = CalculateSimilarity(songListItem.Artist, song.Artist);
|
||||||
var document = parser.Parse(html);
|
|
||||||
//2-?
|
// Weight title more heavily than artist
|
||||||
var articles = document.QuerySelectorAll("div.chart-list-item ");
|
return (titleSimilarity * 0.7) + (artistSimilarity * 0.3);
|
||||||
if (articles.Count() > 0)
|
|
||||||
{
|
|
||||||
Console.WriteLine("Found " + articles.Count() + " Songs");
|
|
||||||
songs = new List<SongListSong>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
////1
|
static string NormalizeString(string input)
|
||||||
//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;
|
if (string.IsNullOrEmpty(input))
|
||||||
var artist = article.Attributes["data-artist"].Value;
|
return string.Empty;
|
||||||
var position = article.Attributes["data-rank"].Value;
|
|
||||||
var song = new SongListSong();
|
// Remove common karaoke prefixes/suffixes
|
||||||
song.Artist = artist;
|
var normalized = input.ToLowerInvariant();
|
||||||
song.Title = title;
|
|
||||||
song.Position = Convert.ToInt32(position);
|
// Remove common karaoke indicators
|
||||||
songs.Add(song);
|
normalized = Regex.Replace(normalized, @"\b(karaoke|karaoke version|instrumental|backing track)\b", "", RegexOptions.IgnoreCase);
|
||||||
i++;
|
|
||||||
}
|
// Remove extra whitespace and punctuation
|
||||||
Console.Write("Parsed " + songs.Count() + " Songs");
|
normalized = Regex.Replace(normalized, @"\s+", " ");
|
||||||
return songs;
|
normalized = Regex.Replace(normalized, @"[^\w\s]", "");
|
||||||
|
|
||||||
|
return normalized.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
static SongList DownloadHot100(string listName, string url)
|
static double CalculateLCSSimilarity(string str1, string str2)
|
||||||
{
|
{
|
||||||
DateTime now = DateTime.Now;
|
if (string.IsNullOrEmpty(str1) || string.IsNullOrEmpty(str2))
|
||||||
string title = now.Year + " - " + listName;
|
return 0.0;
|
||||||
|
|
||||||
Console.WriteLine("Downloading " + title);
|
int lcsLength = CalculateLCSLength(str1, str2);
|
||||||
|
int maxLength = Math.Max(str1.Length, str2.Length);
|
||||||
|
|
||||||
string html = DownloadHtml(url);
|
if (maxLength == 0)
|
||||||
|
return 1.0; // Both strings are empty
|
||||||
|
|
||||||
SongList list = null;
|
return (double)lcsLength / maxLength;
|
||||||
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)
|
static int CalculateLCSLength(string str1, string str2)
|
||||||
{
|
{
|
||||||
List<SongListSong> songs = null;
|
int[,] dp = new int[str1.Length + 1, str2.Length + 1];
|
||||||
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(""", "\"").Replace(""quot;", "\"").Replace("&quoquot;;", "\"").Replace("&ququot;", "\"");
|
|
||||||
JArray articles = JArray.Parse(json);
|
|
||||||
|
|
||||||
if (articles.Count() > 0)
|
for (int i = 1; i <= str1.Length; i++)
|
||||||
{
|
{
|
||||||
Console.WriteLine("Found " + articles.Count() + " Songs");
|
for (int j = 1; j <= str2.Length; j++)
|
||||||
songs = new List<SongListSong>();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
var i = 1;
|
|
||||||
foreach (var article in articles)
|
|
||||||
{
|
{
|
||||||
var title = (string)article["title"];
|
if (str1[i - 1] == str2[j - 1])
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
static string DownloadHtml(string url)
|
|
||||||
{
|
{
|
||||||
string data = null;
|
dp[i, j] = dp[i - 1, j - 1] + 1;
|
||||||
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)
|
|
||||||
{
|
|
||||||
Stream receiveStream = response.GetResponseStream();
|
|
||||||
StreamReader readStream = null;
|
|
||||||
if (response.CharacterSet == null)
|
|
||||||
{
|
|
||||||
readStream = new StreamReader(receiveStream);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
readStream = new StreamReader(receiveStream, Encoding.GetEncoding(response.CharacterSet));
|
dp[i, j] = Math.Max(dp[i - 1, j], dp[i, j - 1]);
|
||||||
}
|
}
|
||||||
data = readStream.ReadToEnd();
|
|
||||||
|
|
||||||
response.Close();
|
|
||||||
readStream.Close();
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static class StingExtension
|
return dp[str1.Length, str2.Length];
|
||||||
|
}
|
||||||
|
|
||||||
|
static int GetFileTypePriority(Song song)
|
||||||
{
|
{
|
||||||
public static string RemoveCrap(this String str)
|
// MP4 files get priority (lower number = higher priority)
|
||||||
|
if (song.FileType == FileType.MP4)
|
||||||
|
return 0;
|
||||||
|
else
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int GetChannelPriority(Song song)
|
||||||
{
|
{
|
||||||
string regex = "(\\[.*\\])|(\".*\")|('.*')|(\\(.*\\))";
|
// Channel priorities in order (lower number = higher priority)
|
||||||
return Regex.Replace(str, regex, "").ToLower().Replace("ft.", "").Replace("feat.", "").Replace("featured", "").Replace("featuring", "").Replace("'", "").Replace(" "," ").Trim();
|
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])
|
||||||
|
{
|
||||||
|
candidates.Add(song);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidates.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<Song> GetArtistCandidates(string searchArtist, Dictionary<string, List<Song>> artistIndex)
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,21 +15,18 @@ namespace Herse.Models
|
|||||||
[JsonProperty("artist")]
|
[JsonProperty("artist")]
|
||||||
public string Artist { get; set; }
|
public string Artist { get; set; }
|
||||||
|
|
||||||
[JsonProperty("genre")]
|
|
||||||
public string Genre { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("path")]
|
[JsonProperty("path")]
|
||||||
public string Path { get; set; }
|
public string Path { get; set; }
|
||||||
|
|
||||||
|
// [JsonProperty("guid")]
|
||||||
|
// public string Guid { get; set; }
|
||||||
|
|
||||||
[JsonProperty("disabled")]
|
[JsonProperty("disabled")]
|
||||||
public bool Disabled { get; set; } = false;
|
public bool Disabled { get; set; } = false;
|
||||||
|
|
||||||
|
|
||||||
[JsonProperty("favorite")]
|
[JsonProperty("favorite")]
|
||||||
public bool Favorite { get; set; } = false;
|
public bool Favorite { get; set; } = false;
|
||||||
|
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public FileType FileType
|
public FileType FileType
|
||||||
{
|
{
|
||||||
@ -51,4 +48,11 @@ namespace Herse.Models
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class History : Song
|
||||||
|
{
|
||||||
|
[JsonProperty("count")]
|
||||||
|
public int Count { get; set; }
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -74,7 +74,7 @@ namespace KaraokePlayer
|
|||||||
songInfoForm.Show();
|
songInfoForm.Show();
|
||||||
if(controller.Settings == null || controller.Settings.AutoAdvance)
|
if(controller.Settings == null || controller.Settings.AutoAdvance)
|
||||||
{
|
{
|
||||||
await Task.Delay(TimeSpan.FromSeconds(10));
|
await Task.Delay(TimeSpan.FromSeconds(5));
|
||||||
player.play();
|
player.play();
|
||||||
controller.SetState(PlayerState.Playing);
|
controller.SetState(PlayerState.Playing);
|
||||||
songInfoForm.Hide();
|
songInfoForm.Hide();
|
||||||
|
|||||||
109
KaraokePlayer/SongInfoForm.Designer.cs
generated
109
KaraokePlayer/SongInfoForm.Designer.cs
generated
@ -28,38 +28,113 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void InitializeComponent()
|
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();
|
this.SuspendLayout();
|
||||||
//
|
//
|
||||||
// previewLabel
|
// mainPanel
|
||||||
//
|
//
|
||||||
this.previewLabel.Dock = System.Windows.Forms.DockStyle.Fill;
|
this.mainPanel.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(15)))), ((int)(((byte)(15)))), ((int)(((byte)(15)))));
|
||||||
this.previewLabel.Font = new System.Drawing.Font("Century Gothic", 60F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
|
this.mainPanel.Controls.Add(this.songTitleLabel);
|
||||||
this.previewLabel.ForeColor = System.Drawing.SystemColors.ControlLightLight;
|
this.mainPanel.Controls.Add(this.artistLabel);
|
||||||
this.previewLabel.Location = new System.Drawing.Point(0, 0);
|
this.mainPanel.Controls.Add(this.singerLabel);
|
||||||
this.previewLabel.Margin = new System.Windows.Forms.Padding(3, 100, 3, 0);
|
this.mainPanel.Controls.Add(this.upNextLabel);
|
||||||
this.previewLabel.Name = "previewLabel";
|
this.mainPanel.Dock = System.Windows.Forms.DockStyle.Fill;
|
||||||
this.previewLabel.Padding = new System.Windows.Forms.Padding(0, 100, 0, 0);
|
this.mainPanel.Location = new System.Drawing.Point(0, 0);
|
||||||
this.previewLabel.Size = new System.Drawing.Size(1008, 729);
|
this.mainPanel.Name = "mainPanel";
|
||||||
this.previewLabel.TabIndex = 0;
|
this.mainPanel.Size = new System.Drawing.Size(1200, 800);
|
||||||
this.previewLabel.Text = "This is the first line\r\n\r\nAnd this is the second line.";
|
this.mainPanel.TabIndex = 0;
|
||||||
this.previewLabel.TextAlign = System.Drawing.ContentAlignment.TopCenter;
|
//
|
||||||
|
// 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
|
// SongInfoForm
|
||||||
//
|
//
|
||||||
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
|
||||||
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
|
||||||
this.BackColor = System.Drawing.Color.Black;
|
this.BackColor = System.Drawing.Color.Black;
|
||||||
this.ClientSize = new System.Drawing.Size(1008, 729);
|
this.ClientSize = new System.Drawing.Size(1200, 800);
|
||||||
this.Controls.Add(this.previewLabel);
|
this.Controls.Add(this.mainPanel);
|
||||||
|
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
|
||||||
this.Name = "SongInfoForm";
|
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);
|
this.ResumeLayout(false);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5,6 +5,7 @@ using System.Collections.Generic;
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Drawing;
|
using System.Drawing;
|
||||||
|
using System.Drawing.Drawing2D;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@ -14,17 +15,159 @@ namespace KaraokePlayer
|
|||||||
{
|
{
|
||||||
public partial class SongInfoForm : Form
|
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()
|
public SongInfoForm()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
|
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
|
||||||
this.WindowState = FormWindowState.Maximized;
|
this.WindowState = FormWindowState.Normal;
|
||||||
this.ShowInTaskbar = false;
|
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)
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,76 +11,214 @@ using System.Threading.Tasks;
|
|||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Configuration;
|
using System.Configuration;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
namespace SongCrawler
|
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
|
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)
|
static void Main(string[] args)
|
||||||
{
|
{
|
||||||
|
InitializeFromArgs(args);
|
||||||
|
|
||||||
if (args.Count() == 3)
|
if (args.Count() == 3)
|
||||||
{
|
{
|
||||||
DeleteSongs(args);
|
DeleteSongs();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CrawlSongs();
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
CrawlSongs(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)
|
||||||
|
{
|
||||||
|
Args = args;
|
||||||
|
if (args.Length > 0)
|
||||||
|
{
|
||||||
|
Controller = args[0];
|
||||||
|
}
|
||||||
|
if (args.Length > 1)
|
||||||
|
{
|
||||||
|
SongsFolderPath = args[1];
|
||||||
|
}
|
||||||
|
FirebaseClient = CreateFirebaseClient();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void CrawlSongs(string[] args)
|
/// <summary>
|
||||||
|
/// Creates and configures a Firebase client using settings from App.config.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Configured Firebase client instance</returns>
|
||||||
|
private static FireSharp.FirebaseClient CreateFirebaseClient()
|
||||||
{
|
{
|
||||||
//string [] test = { "mbrucedogs", "z://" };
|
|
||||||
//args = test;
|
|
||||||
if (args.Length != 2)
|
|
||||||
{
|
|
||||||
Console.WriteLine("usage: songcrawler partyid songspath");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
string controller = args[0];
|
|
||||||
string songpath = args[1];
|
|
||||||
IFirebaseConfig config = new FirebaseConfig
|
IFirebaseConfig config = new FirebaseConfig
|
||||||
{
|
{
|
||||||
AuthSecret = ConfigurationManager.AppSettings["Firebase.Secret"],
|
AuthSecret = ConfigurationManager.AppSettings["Firebase.Secret"],
|
||||||
BasePath = ConfigurationManager.AppSettings["Firebase.Path"]
|
BasePath = ConfigurationManager.AppSettings["Firebase.Path"]
|
||||||
};
|
};
|
||||||
FireSharp.FirebaseClient client = new FireSharp.FirebaseClient(config);
|
return 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");
|
|
||||||
|
|
||||||
List<Song> songs = null; //client.Get(songsPath).ResultAs<List<Song>>();
|
/// <summary>
|
||||||
List<Song> disabled = null;
|
/// Validates command line arguments against expected length.
|
||||||
List<Song> favorited = null;
|
/// </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
|
try
|
||||||
{
|
{
|
||||||
disabled = client.Get(disabledPath).ResultAs<List<Song>>();
|
return client.Get(path).ResultAs<List<T>>();
|
||||||
}
|
}
|
||||||
catch
|
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
|
||||||
{
|
{
|
||||||
favorited = client.Get(favoritesPath).ResultAs<List<Song>>();
|
dynamic data = JsonConvert.DeserializeObject<dynamic>(json);
|
||||||
}
|
var list = new List<T>();
|
||||||
catch
|
foreach (var itemDynamic in data)
|
||||||
{
|
{
|
||||||
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>();
|
List<string> files = new List<string>();
|
||||||
files.AddRange(FindFiles("mp4", songpath));
|
files.AddRange(FindFiles("mp4", songpath));
|
||||||
files.AddRange(FindFiles("mp3", songpath));
|
files.AddRange(FindFiles("mp3", songpath));
|
||||||
files.AddRange(FindFiles("zip", 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;
|
Song song = null;
|
||||||
int i = 0;
|
int i = 0;
|
||||||
foreach (string filepath in files)
|
foreach (string filepath in files)
|
||||||
@ -90,259 +228,38 @@ namespace SongCrawler
|
|||||||
{
|
{
|
||||||
song = MakeSong(filepath);
|
song = MakeSong(filepath);
|
||||||
Console.WriteLine(string.Format("{0:000000}/{1} - {2} - {3}", i, files.Count, song.Artist, song.Title));
|
Console.WriteLine(string.Format("{0:000000}/{1} - {2} - {3}", i, files.Count, song.Artist, song.Title));
|
||||||
|
|
||||||
|
if (checkDuplicates)
|
||||||
|
{
|
||||||
|
if (!songs.Any(s => s.Title.ToLower() == song.Title.ToLower() && s.Artist.ToLower() == song.Artist.ToLower()))
|
||||||
|
{
|
||||||
songs.Add(song);
|
songs.Add(song);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine(ex.Message);
|
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
|
|
||||||
//sync all favorite, history, disabled
|
|
||||||
if (favorited != null && favorited.Count > 0)
|
|
||||||
{
|
{
|
||||||
favorited.ForEach(s =>
|
|
||||||
{
|
|
||||||
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);
|
songs.Add(song);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine(ex.Message);
|
Console.WriteLine(ex.Message);
|
||||||
}
|
}
|
||||||
|
if (debug && i > debugLimit)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i = 0;
|
/// <summary>
|
||||||
Dictionary<string, List<Song>> dupes = new Dictionary<string, List<Song>>();
|
/// Creates a Song object from a file path.
|
||||||
foreach (var localsong in songs)
|
/// Handles different file types (mp3, mp4, zip) and extracts metadata.
|
||||||
{
|
/// </summary>
|
||||||
i++;
|
/// <param name="filepath">Path to the music file</param>
|
||||||
Console.WriteLine(string.Format("Checking for {0:000000}/{1}) {2} - {3}", i, files.Count, localsong.Artist, localsong.Title));
|
/// <returns>Song object with extracted metadata</returns>
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Song MakeSong(string filepath)
|
private static Song MakeSong(string filepath)
|
||||||
{
|
{
|
||||||
|
|
||||||
Song song = null;
|
Song song = null;
|
||||||
var ext = Path.GetExtension(filepath).ToLower();
|
var ext = Path.GetExtension(filepath).ToLower();
|
||||||
switch (ext)
|
switch (ext)
|
||||||
@ -360,14 +277,48 @@ namespace SongCrawler
|
|||||||
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));
|
Song song = new Song();
|
||||||
string[] files = Directory.GetFiles(path, "*." + ext, SearchOption.AllDirectories);
|
TagLib.File tagFile;
|
||||||
Console.WriteLine(string.Format("{0} found", files.Length));
|
try
|
||||||
return files;
|
{
|
||||||
|
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)
|
private static void CheckTitle(Song song)
|
||||||
{
|
{
|
||||||
if(string.IsNullOrEmpty(song.Title))
|
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");
|
if (sourceList != null && sourceList.Count > 0)
|
||||||
string filepath = Directory.GetFiles("c:\\temp", "*.mp3")[0];
|
{
|
||||||
Song song = ReadId3(filepath);
|
sourceList.ForEach(s =>
|
||||||
foreach (string file in Directory.GetFiles("c:\\temp")) File.Delete(file);
|
{
|
||||||
return song;
|
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();
|
Dictionary<string, List<Song>> dupes = new Dictionary<string, List<Song>>();
|
||||||
TagLib.File tagFile;
|
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
|
try
|
||||||
{
|
{
|
||||||
tagFile = TagLib.File.Create(path);
|
history = FirebaseClient.Get(historyPath).ResultAs<List<History>>();
|
||||||
song.Title = tagFile.Tag.Title.Trim();
|
|
||||||
song.Artist = tagFile.Tag.FirstPerformer.Trim();
|
|
||||||
if (tagFile.Tag.FirstGenre != null) {
|
|
||||||
song.Genre = tagFile.Tag.FirstGenre.Trim();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
// do nothing;
|
dynamic data = JsonConvert.DeserializeObject<dynamic>(FirebaseClient.Get(historyPath).Body);
|
||||||
}
|
history = new List<History>();
|
||||||
song.Path = path;
|
foreach (var itemDynamic in data)
|
||||||
return song;
|
{
|
||||||
|
var fjson = itemDynamic.Value.ToString();
|
||||||
|
var f = JsonConvert.DeserializeObject<History>(fjson);
|
||||||
|
history.Add(f);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user