public IEnumerator MatchKey()
        {
            if (!string.IsNullOrEmpty(key) || level == null || !(level is CustomPreviewBeatmapLevel))
            {
                yield break;
            }

            string songHash = null;

            if (!string.IsNullOrEmpty(hash))
            {
                songHash = hash;
            }
            else if (!string.IsNullOrEmpty(levelId))
            {
                songHash = CustomHelpers.GetSongHash(level.levelID);
            }

            if (songHash != null && SongDataCore.Plugin.BeatSaver.Data.Songs.ContainsKey(hash))
            {
                var song = SongDataCore.Plugin.BeatSaver.Data.Songs[hash];
                key = song.key;
            }
            else
            {
                // no more hitting api just to match a key.  We know the song hash.
                //yield return SongDownloader.Instance.RequestSongByLevelIDCoroutine(level.levelID.Split('_')[2], (Song bsSong) => { if (bsSong != null) key = bsSong.key; });
            }
        }
        /// <summary>
        /// Add Song to Editing Playlist
        /// </summary>
        /// <param name="songInfo"></param>
        public void AddSongToEditingPlaylist(IBeatmapLevel songInfo)
        {
            if (this.CurrentEditingPlaylist == null)
            {
                return;
            }

            this.CurrentEditingPlaylist.songs.Add(new PlaylistSong()
            {
                songName = songInfo.songName,
                levelId  = songInfo.levelID,
                hash     = CustomHelpers.GetSongHash(songInfo.levelID),
            });

            this.CurrentEditingPlaylistLevelIds.Add(songInfo.levelID);
            this.CurrentEditingPlaylist.SavePlaylist();
        }
        /// <summary>
        /// Filter songs based on ranked or unranked status.
        /// </summary>
        /// <param name="levels"></param>
        /// <param name="includeRanked"></param>
        /// <param name="includeUnranked"></param>
        /// <returns></returns>
        private List <IPreviewBeatmapLevel> FilterRanked(List <IPreviewBeatmapLevel> levels, bool includeRanked, bool includeUnranked)
        {
            return(levels.Where(x =>
            {
                var hash = CustomHelpers.GetSongHash(x.levelID);
                double maxPP = 0.0;
                if (SongDataCore.Plugin.ScoreSaber.Data.Songs.ContainsKey(hash))
                {
                    maxPP = SongDataCore.Plugin.ScoreSaber.Data.Songs[hash].diffs.Max(y => y.pp);
                }

                if (maxPP > 0f)
                {
                    return includeRanked;
                }
                else
                {
                    return includeUnranked;
                }
            }).ToList());
        }
        /// <summary>
        /// Sorting by star rating.
        /// </summary>
        /// <param name="levels"></param>
        /// <returns></returns>
        private List <IPreviewBeatmapLevel> SortStars(List <IPreviewBeatmapLevel> levels)
        {
            Logger.Info("Sorting song list by star points...");

            if (!SongDataCore.Plugin.ScoreSaber.IsDataAvailable())
            {
                return(levels);
            }

            return(levels
                   .OrderByDescending(x =>
            {
                var hash = CustomHelpers.GetSongHash(x.levelID);
                var stars = 0.0;
                if (SongDataCore.Plugin.ScoreSaber.Data.Songs.ContainsKey(hash))
                {
                    var diffs = SongDataCore.Plugin.ScoreSaber.Data.Songs[hash].diffs;
                    stars = diffs.Max(y => y.star);
                }

                //Logger.Debug("Stars={0}", stars);
                if (stars != 0)
                {
                    return stars;
                }

                if (_settings.invertSortResults)
                {
                    return double.MaxValue;
                }
                else
                {
                    return double.MinValue;
                }
            })
                   .ToList());
        }
        /// <summary>
        /// Sorting by BeatSaver heat stat.
        /// </summary>
        /// <param name="levelIds"></param>
        /// <returns></returns>
        private List <IPreviewBeatmapLevel> SortBeatSaverHeat(List <IPreviewBeatmapLevel> levelIds)
        {
            Logger.Info("Sorting song list by BeatSaver Heat!");

            // Do not always have data when trying to sort by heat
            if (!SongDataCore.Plugin.BeatSaver.IsDataAvailable())
            {
                return(levelIds);
            }

            return(levelIds
                   .OrderByDescending(x => {
                var hash = CustomHelpers.GetSongHash(x.levelID);
                if (SongDataCore.Plugin.BeatSaver.Data.Songs.ContainsKey(hash))
                {
                    return SongDataCore.Plugin.BeatSaver.Data.Songs[hash].stats.heat;
                }
                else
                {
                    return int.MinValue;
                }
            })
                   .ToList());
        }
        /// <summary>
        /// Sorting by PP.
        /// </summary>
        /// <param name="levels"></param>
        /// <returns></returns>
        private List <IPreviewBeatmapLevel> SortPerformancePoints(List <IPreviewBeatmapLevel> levels)
        {
            Logger.Info("Sorting song list by performance points...");

            if (!SongDataCore.Plugin.ScoreSaber.IsDataAvailable())
            {
                return(levels);
            }

            return(levels
                   .OrderByDescending(x =>
            {
                var hash = CustomHelpers.GetSongHash(x.levelID);
                if (SongDataCore.Plugin.ScoreSaber.Data.Songs.ContainsKey(hash))
                {
                    return SongDataCore.Plugin.ScoreSaber.Data.Songs[hash].diffs.Max(y => y.pp);
                }
                else
                {
                    return 0;
                }
            })
                   .ToList());
        }
        /// <summary>
        /// Favorites used to exist as part of the song_browser_settings.xml
        /// This makes little sense now.  This is the upgrade path.
        /// Convert all existing favorites to the best of our effort into a playlist.
        /// </summary>
        /// <param name="levelIdToCustomLevel"></param>
        /// <param name="levelIdToSongVersion"></param>
        public void ConvertFavoritesToPlaylist(Dictionary <String, CustomPreviewBeatmapLevel> customSongsMap)
        {
            // map songs in case we are converting a huge list
            Dictionary <String, CustomPreviewBeatmapLevel> levelIdToCustomLevel = new Dictionary <string, CustomPreviewBeatmapLevel>(StringComparer.OrdinalIgnoreCase);

            foreach (var kp in customSongsMap)
            {
                if (levelIdToCustomLevel.ContainsKey(kp.Value.levelID))
                {
                    continue;
                }
                levelIdToCustomLevel.Add(kp.Value.levelID, kp.Value);
            }

            // Check if we have favorites to convert to the playlist
            if (this.Favorites.Count <= 0)
            {
                return;
            }

            // check if the playlist exists
            String playlistPath   = Path.Combine(Environment.CurrentDirectory, "Playlists", DefaultConvertedFavoritesPlaylistName);
            bool   playlistExists = false;

            if (File.Exists(playlistPath))
            {
                playlistExists = true;
            }

            // abort here if playlist already exits.
            if (playlistExists)
            {
                Logger.Info("Not converting song_browser_setting.xml favorites because {0} already exists...", playlistPath);
                return;
            }

            Logger.Info("Converting {0} Favorites in song_browser_settings.xml to {1}...", this.Favorites.Count, playlistPath);

            // written like this in case we ever want to support adding to this playlist
            Playlist p = null;

            if (playlistExists)
            {
                p = Playlist.LoadPlaylist(playlistPath);
            }
            else
            {
                p = new Playlist
                {
                    playlistTitle  = "Song Browser Favorites",
                    playlistAuthor = "SongBrowser",
                    fileLoc        = "",
                    image          = Base64Sprites.SpriteToBase64(Base64Sprites.BeastSaberLogo),
                    songs          = new List <PlaylistSong>(),
                };
            }

            List <String> successfullyRemoved = new List <string>();

            this.Favorites.RemoveWhere(levelId =>
            {
                PlaylistSong playlistSong = new PlaylistSong
                {
                    levelId = levelId
                };

                if (levelIdToCustomLevel.ContainsKey(levelId))
                {
                    playlistSong.songName = levelIdToCustomLevel[levelId].songName;
                    playlistSong.levelId  = levelId;
                    playlistSong.hash     = CustomHelpers.GetSongHash(levelId);
                }
                else
                {
                    // No easy way to look up original songs... They will still work but have wrong song name in playlist.
                    playlistSong.levelId = levelId;
                    playlistSong.hash    = CustomHelpers.GetSongHash(levelId);
                    playlistSong.key     = "";
                }

                p.songs.Add(playlistSong);

                return(true);
            });

            p.SavePlaylist(playlistPath);

            if (String.IsNullOrEmpty(this.currentEditingPlaylistFile))
            {
                this.currentEditingPlaylistFile = playlistPath;
            }

            this.Save();
        }
        public static void MatchSongsForPlaylist(Playlist playlist, bool matchAll = false)
        {
            if (!SongCore.Loader.AreSongsLoaded || SongCore.Loader.AreSongsLoading || playlist.playlistTitle == "All songs" || playlist.playlistTitle == "Your favorite songs")
            {
                return;
            }

            Dictionary <string, CustomPreviewBeatmapLevel> songMap = new Dictionary <string, CustomPreviewBeatmapLevel>(StringComparer.OrdinalIgnoreCase);

            foreach (var kp in SongCore.Loader.CustomLevels)
            {
                var songHash = CustomHelpers.GetSongHash(kp.Value.levelID);
                if (songMap.ContainsKey(songHash))
                {
                    continue;
                }

                songMap.Add(songHash, kp.Value);
            }

            if (!playlist.songs.All(x => x.level != null) || matchAll)
            {
                playlist.songs.AsParallel().ForAll(x =>
                {
                    if (x.level == null || matchAll)
                    {
                        try
                        {
                            // try to use levelID
                            if (!string.IsNullOrEmpty(x.levelId))
                            {
                                string songHash = CustomHelpers.GetSongHash(x.levelId);
                                if (songMap.ContainsKey(songHash))
                                {
                                    x.level = songMap[songHash];
                                    x.hash  = songHash;
                                }
                            }

                            // failed, try again using hash
                            if (x.level == null && !string.IsNullOrEmpty(x.hash))
                            {
                                // fix broken playlists from a bug in song browser
                                if (x.hash.Contains("custom_level"))
                                {
                                    x.hash = CustomHelpers.GetSongHash(x.hash);
                                }

                                if (songMap.ContainsKey(x.hash))
                                {
                                    x.level = songMap[x.hash];
                                }
                            }

                            if (x.level == null && !string.IsNullOrEmpty(x.key))
                            {
                                x.level = SongCore.Loader.CustomLevels.FirstOrDefault(y => y.Value.customLevelPath.Contains(x.key)).Value;
                                if (x.level != null && !String.IsNullOrEmpty(x.level.levelID))
                                {
                                    x.hash = CustomHelpers.GetSongHash(x.level.levelID);
                                }
                            }
                        }
                        catch (Exception e)
                        {
                            Logger.Warning($"Unable to match song with {(string.IsNullOrEmpty(x.key) ? " unknown key!" : ("key " + x.key + " !"))}");
                        }
                    }
                });
            }
        }