/// <summary>
        /// Callback, invoked when the data property is accessed.
        /// </summary>
        protected override byte[] GetData()
        {
            if (!isDone)
            {
                Logger.Error("{0}Downloading is not completed : {1}", kLog, m_WebRequest.url);
                throw new InvalidOperationException("Downloading is not completed. " + m_WebRequest.url);
            }
            else if (m_Buffer == null)
            {
                // Etag cache hit!
                if (m_WebRequest.responseCode == 304)
                {
                    Logger.Debug("<color=green>{0}Etag cache hit : {1}</color>", kLog, m_WebRequest.url);
                    m_Buffer = LoadCache(m_WebRequest.url);
                }
                // Download is completed successfully.
                else if (m_WebRequest.responseCode == 200)
                {
                    Logger.Debug("<color=green>{0}Download is completed successfully : {1}</color>", kLog, m_WebRequest.url);
                    m_Buffer = m_Stream.GetBuffer();
                    SaveCache(m_WebRequest.url, m_WebRequest.GetResponseHeader("Etag"), m_Buffer);
                }
            }

            if (m_Stream != null)
            {
                m_Stream.Dispose();
                m_Stream = null;
            }
            return(m_Buffer);
        }
        private void FilterSearch(List <LevelSO> levels)
        {
            // Make sure we can actually search.
            if (this._settings.searchTerms.Count <= 0)
            {
                Logger.Error("Tried to search for a song with no valid search terms...");
                SortSongName(levels);
                return;
            }
            string searchTerm = this._settings.searchTerms[0];

            if (String.IsNullOrEmpty(searchTerm))
            {
                Logger.Error("Empty search term entered.");
                SortSongName(levels);
                return;
            }

            Logger.Info("Filtering song list by search term: {0}", searchTerm);
            _originalSongs.ForEach(x => Logger.Debug($"{x.songName} {x.songSubName} {x.songAuthorName}".ToLower().Contains(searchTerm.ToLower()).ToString()));

            _filteredSongs = levels
                             .Where(x => $"{x.songName} {x.songSubName} {x.songAuthorName}".ToLower().Contains(searchTerm.ToLower()))
                             .ToList();
        }
Пример #3
0
        /// <summary>
        /// Overwrite the current level pack.
        /// </summary>
        private void OverwriteCurrentLevelPack()
        {
            Logger.Debug("Overwriting levelPack [{0}] beatmapLevelCollection", this._currentLevelPack);
            IBeatmapLevelPack levelPack = this._currentLevelPack;
            var levels = _sortedSongs.ToArray();

            ReflectionUtil.SetPrivateField(levelPack.beatmapLevelCollection, "_beatmapLevels", levels);
        }
Пример #4
0
        /// <summary>
        /// SongLoader doesn't fire event when we delete a song.
        /// </summary>
        /// <param name="levelPack"></param>
        /// <param name="levelId"></param>
        public void RemoveSongFromLevelPack(IBeatmapLevelPack levelPack, String levelId)
        {
            if (!_levelPackToSongs.ContainsKey(levelPack.packName))
            {
                Logger.Debug("Trying to remove song from level pack [{0}] but we do not have any information on it...", levelPack.packName);
                return;
            }

            _levelPackToSongs[levelPack.packName].RemoveAll(x => x.levelID == levelId);
        }
Пример #5
0
        /// <summary>
        /// Adjust button background color.
        /// </summary>
        /// <param name="button"></param>
        /// <param name="background"></param>
        static public void SetButtonBackground(Button button, Sprite background)
        {
            Image img = button.GetComponentsInChildren <Image>().FirstOrDefault(x => x.name == "BG");

            if (img != null)
            {
                img.sprite = background;
            }
            else
            {
                Logger.Debug("NULL BG");
            }
        }
        /// <summary>
        /// Get path for web-caching.
        /// </summary>
        public static string GetCachePath(string url)
        {
            if (s_WebCachePath == null)
            {
                s_WebCachePath = Application.temporaryCachePath + "/WebCache/";
                Logger.Debug("{0}WebCachePath : {1}", kLog, s_WebCachePath);
            }

            if (!Directory.Exists(s_WebCachePath))
            {
                Directory.CreateDirectory(s_WebCachePath);
            }

            return(s_WebCachePath + Convert.ToBase64String(s_SHA1.ComputeHash(UTF8Encoding.Default.GetBytes(url))).Replace('/', '_'));
        }
        private void FilterPlaylist()
        {
            // bail if no playlist, usually means the settings stored one the user then moved.
            if (this.CurrentPlaylist == null)
            {
                Logger.Error("Trying to load a null playlist...");
                _filteredSongs           = _originalSongs;
                this.Settings.filterMode = SongFilterMode.None;
                return;
            }

            Logger.Debug("Filtering songs for playlist: {0}", this.CurrentPlaylist.Title);
            LevelCollectionSO levelCollections = Resources.FindObjectsOfTypeAll <LevelCollectionSO>().FirstOrDefault();
            var levels = levelCollections.GetLevelsWithBeatmapCharacteristic(CurrentBeatmapCharacteristicSO);

            //Dictionary<String, LevelSO> levelDict = levels.Select((val, index) => new { LevelId = val.levelID, Level = val }).ToDictionary(i => i.LevelId, i => i.Level);
            Dictionary <String, LevelSO> levelDict = new Dictionary <string, LevelSO>();

            foreach (LevelSO level in levels)
            {
                if (!levelDict.ContainsKey(level.levelID))
                {
                    levelDict.Add(level.levelID, level);
                }
            }

            List <LevelSO> songList = new List <LevelSO>();

            foreach (PlaylistSong ps in this.CurrentPlaylist.Songs)
            {
                if (!String.IsNullOrEmpty(ps.LevelId))
                {
                    if (levelDict.ContainsKey(ps.LevelId))
                    {
                        songList.Add(levelDict[ps.LevelId]);
                    }
                }
                else if (!ps.Key.StartsWith("Level_") && _keyToSong.ContainsKey(ps.Key))
                {
                    songList.Add(_keyToSong[ps.Key]);
                }
            }

            _originalSongs = songList;
            _filteredSongs = _originalSongs;

            Logger.Debug("Playlist filtered song count: {0}", _filteredSongs.Count);
        }
Пример #8
0
        /// <summary>
        /// Resets all the level packs back to their original values.
        /// </summary>
        /// <param name="pack"></param>
        private void ResetLevelPacks()
        {
            Logger.Debug("Setting level packs back to their original values!");

            BeatmapLevelPackSO[] levelPacks = Resources.FindObjectsOfTypeAll <BeatmapLevelPackSO>();
            foreach (BeatmapLevelPackSO levelPack in levelPacks)
            {
                if (!_levelPackToSongs.ContainsKey(levelPack.packName))
                {
                    Logger.Debug("We know nothing about pack: {0}", levelPack.packName);
                    continue;
                }

                var levels = _levelPackToSongs[levelPack.packName].ToArray();
                ReflectionUtil.SetPrivateField(levelPack.beatmapLevelCollection, "_beatmapLevels", levels);
            }
        }
Пример #9
0
        private List <BeatmapLevelSO> FilterPlaylist()
        {
            // bail if no playlist, usually means the settings stored one the user then moved.
            if (this.CurrentPlaylist == null)
            {
                Logger.Error("Trying to load a null playlist...");
                this.Settings.filterMode = SongFilterMode.None;
                return(null);
            }

            // Get song keys
            PlaylistsCollection.MatchSongsForPlaylist(this.CurrentPlaylist, true);

            Logger.Debug("Filtering songs for playlist: {0}", this.CurrentPlaylist.playlistTitle);

            Dictionary <String, BeatmapLevelSO> levelDict = new Dictionary <string, BeatmapLevelSO>();

            foreach (KeyValuePair <string, List <BeatmapLevelSO> > entry in _levelPackToSongs)
            {
                foreach (BeatmapLevelSO level in entry.Value)
                {
                    if (!levelDict.ContainsKey(level.levelID))
                    {
                        levelDict.Add(level.levelID, level);
                    }
                }
            }

            List <BeatmapLevelSO> songList = new List <BeatmapLevelSO>();

            foreach (PlaylistSong ps in this.CurrentPlaylist.songs)
            {
                if (ps.level != null)
                {
                    songList.Add(levelDict[ps.level.levelID]);
                }
                else
                {
                    Logger.Warning("Could not find song in playlist: {0}", ps.songName);
                }
            }

            Logger.Debug("Playlist filtered song count: {0}", songList.Count);
            return(songList);
        }
        /// <summary>
        /// Push a dir onto the stack.
        /// </summary>
        public void PushDirectory(IBeatmapLevel level)
        {
            DirectoryNode currentNode = _directoryStack.Peek();

            Logger.Debug("Pushing directory {0}", level.songName);

            if (!currentNode.Nodes.ContainsKey(level.songName))
            {
                Logger.Debug("Trying to push a directory that doesn't exist at this level.");
                return;
            }

            _directoryStack.Push(currentNode.Nodes[level.songName]);

            this.CurrentDirectory = level.levelID;

            ProcessSongList();
        }
        protected override void DidActivate(bool firstActivation, ActivationType activationType)
        {
            try
            {
                if (firstActivation && activationType == ActivationType.AddedToHierarchy)
                {
                    title = "Playlists";

                    if (_playlistsReader == null)
                    {
                        _playlistsReader = new PlaylistsReader();
                        _playlistsReader.UpdatePlaylists();
                        Logger.Debug("Reader found {0} playlists!", _playlistsReader.Playlists.Count);

                        this.MatchSongsForAllPlaylists(true);
                    }

                    _playlistsNavigationController.didFinishEvent += _playlistsNavigationController_didFinishEvent;

                    _playlistListViewController = BeatSaberUI.CreateViewController <PlaylistListViewController>();
                    _playlistListViewController.didSelectRow += _playlistListViewController_didSelectRow;

                    _playlistDetailViewController.downloadButtonPressed += _playlistDetailViewController_downloadButtonPressed;
                    _playlistDetailViewController.selectButtonPressed   += _playlistDetailViewController_selectButtonPressed;

                    _downloadQueueViewController = BeatSaberUI.CreateViewController <DownloadQueueViewController>();

                    SetViewControllersToNavigationConctroller(_playlistsNavigationController, new VRUIViewController[]
                    {
                        _playlistListViewController
                    });

                    ProvideInitialViewControllers(_playlistsNavigationController, _downloadQueueViewController, null);
                }
                _downloadingPlaylist = false;
                _playlistListViewController.SetContent(_playlistsReader.Playlists);

                _downloadQueueViewController.allSongsDownloaded += _downloadQueueViewController_allSongsDownloaded;
            }
            catch (Exception e)
            {
                Logger.Exception("Error activating playlist flow coordinator: ", e);
            }
        }
Пример #12
0
        /// <summary>
        /// Wait for the song list to be visible to draw it.
        /// </summary>
        /// <returns></returns>
        private IEnumerator WaitForSongListUI()
        {
            Logger.Trace("WaitForSongListUI()");

            yield return(new WaitUntil(delegate() { return Resources.FindObjectsOfTypeAll <SoloFreePlayFlowCoordinator>().Any() && Resources.FindObjectsOfTypeAll <SoloFreePlayFlowCoordinator>().Any(); }));

            Logger.Debug("Found Solo and Party FreePlayFlowCoordinators...");

            if (SongLoaderPlugin.SongLoader.AreSongsLoaded)
            {
                OnSongLoaderLoadedSongs(null, SongLoader.CustomLevels);
            }
            else
            {
                SongLoader.SongsLoadedEvent += OnSongLoaderLoadedSongs;
            }

            _songBrowserUI.RefreshSongList();
        }
Пример #13
0
        /// <summary>
        /// Get a copy of the unfiltered, unsorted list of songs from level packs.
        /// </summary>
        public void UpdateLevelPackOriginalLists()
        {
            BeatmapLevelPackSO[] levelPacks = Resources.FindObjectsOfTypeAll <BeatmapLevelPackSO>();
            foreach (BeatmapLevelPackSO levelPack in levelPacks)
            {
                Logger.Debug("Attempting to get song list from levelPack: {0}...", levelPack);
                var beatmapLevelPack = levelPack as BeatmapLevelPackSO;

                // TODO - need to rethink interface here, not all level packs can be cast this high, some sort functions need it.
                //      - this helps prevent DLC from breaking everything
                if (beatmapLevelPack == null)
                {
                    continue;
                }

                _levelPackToSongs[levelPack.packName] = (beatmapLevelPack.beatmapLevelCollection as BeatmapLevelCollectionSO).GetPrivateField <BeatmapLevelSO[]>("_beatmapLevels").ToList();
                Logger.Debug("Got {0} songs from level collections...", _levelPackToSongs[levelPack.packName].Count);
                //_levelPackToSongs[levelPack.packName].ForEach(x => Logger.Debug("{0} by {1} = {2}", x.name, x.levelAuthorName, x.levelID));
            }
        }
        /// <summary>
        /// Wait for score saber related files to download.
        /// </summary>
        /// <returns></returns>
        private IEnumerator WaitForDownload()
        {
            if (ScoreSaberDatabaseDownloader.ScoreSaberDataFile != null)
            {
                Logger.Info("Using cached copy of ScoreSaberData...");
            }
            else
            {
                SongBrowserApplication.MainProgressBar.ShowMessage("Downloading BeatStar data...");

                Logger.Info("Attempting to download: {0}", ScoreSaberDatabaseDownloader.SCRAPED_SCORE_SABER_JSON_URL);
                using (UnityWebRequest www = UnityWebRequest.Get(ScoreSaberDatabaseDownloader.SCRAPED_SCORE_SABER_JSON_URL))
                {
                    // Use 4MB cache, large enough for this file to grow for awhile.
                    www.SetCacheable(new CacheableDownloadHandlerScoreSaberData(www, _buffer));
                    yield return(www.SendWebRequest());

                    Logger.Debug("Returned from web request!...");

                    try
                    {
                        ScoreSaberDatabaseDownloader.ScoreSaberDataFile = (www.downloadHandler as CacheableDownloadHandlerScoreSaberData).ScoreSaberDataFile;
                        Logger.Info("Success downloading ScoreSaber data!");

                        SongBrowserApplication.MainProgressBar.ShowMessage("Success downloading BeatStar data...", 10.0f);
                        onScoreSaberDataDownloaded?.Invoke();
                    }
                    catch (System.InvalidOperationException)
                    {
                        Logger.Error("Failed to download ScoreSaber data file...");
                    }
                    catch (Exception e)
                    {
                        Logger.Exception("Exception trying to download ScoreSaber data file...", e);
                    }
                }
            }
        }
Пример #15
0
        /// <summary>
        /// Set current level pack, reset all packs just in case.
        /// </summary>
        /// <param name="pack"></param>
        public void SetCurrentLevelPack(IBeatmapLevelPack pack)
        {
            Logger.Debug("Setting current level pack [{0}]: {1}", pack.packID, pack.packName);

            this.ResetLevelPacks();

            this._currentLevelPack = pack;

            var beatmapLevelPack = pack as BeatmapLevelPackSO;

            if (beatmapLevelPack == null)
            {
                Logger.Debug("DLC Detected...  Disabling SongBrowser...");
                _isPreviewLevelPack = true;
            }
            else
            {
                Logger.Debug("Owned level pack...  Enabling SongBrowser...");
                _isPreviewLevelPack = false;
            }

            this.Settings.currentLevelPackId = pack.packID;
            this.Settings.Save();
        }
Пример #16
0
        /// <summary>
        /// Get the song cache from the game.
        /// </summary>
        public void UpdateLevelRecords()
        {
            Stopwatch timer = new Stopwatch();

            timer.Start();

            // Calculate some information about the custom song dir
            String customSongsPath                   = Path.Combine(Environment.CurrentDirectory, CUSTOM_SONGS_DIR);
            String revSlashCustomSongPath            = customSongsPath.Replace('\\', '/');
            double currentCustomSongDirLastWriteTIme = (File.GetLastWriteTimeUtc(customSongsPath) - EPOCH).TotalMilliseconds;
            bool   customSongDirChanged              = false;

            if (_customSongDirLastWriteTime != currentCustomSongDirLastWriteTIme)
            {
                customSongDirChanged        = true;
                _customSongDirLastWriteTime = currentCustomSongDirLastWriteTIme;
            }

            if (!Directory.Exists(customSongsPath))
            {
                Logger.Error("CustomSong directory is missing...");
                return;
            }
            IEnumerable <string> directories = Directory.EnumerateDirectories(customSongsPath, "*.*", SearchOption.AllDirectories);

            // Get LastWriteTimes
            Stopwatch lastWriteTimer = new Stopwatch();

            lastWriteTimer.Start();
            foreach (var level in SongLoader.CustomLevels)
            {
                // If we already know this levelID, don't both updating it.
                // SongLoader should filter duplicates but in case of failure we don't want to crash
                if (!_cachedLastWriteTimes.ContainsKey(level.levelID) || customSongDirChanged)
                {
                    // Always use the newest date.
                    var lastWriteTime  = File.GetLastWriteTimeUtc(level.customSongInfo.path);
                    var lastCreateTime = File.GetCreationTimeUtc(level.customSongInfo.path);
                    var lastTime       = lastWriteTime > lastCreateTime ? lastWriteTime : lastCreateTime;
                    _cachedLastWriteTimes[level.levelID] = (lastTime - EPOCH).TotalMilliseconds;
                }

                if (!_levelIdToCustomLevel.ContainsKey(level.levelID))
                {
                    _levelIdToCustomLevel.Add(level.levelID, level);
                }

                if (!_levelIdToSongVersion.ContainsKey(level.levelID))
                {
                    DirectoryInfo info = new DirectoryInfo(level.customSongInfo.path);
                    string        currentDirectoryName = info.Name;

                    String version = level.customSongInfo.path.Replace(revSlashCustomSongPath, "").Replace(currentDirectoryName, "").Replace("/", "");
                    if (!String.IsNullOrEmpty(version))
                    {
                        _levelIdToSongVersion.Add(level.levelID, version);

                        if (!_keyToSong.ContainsKey(version))
                        {
                            _keyToSong.Add(version, level);
                        }
                    }
                }
            }

            lastWriteTimer.Stop();
            Logger.Info("Determining song download time and determining mappings took {0}ms", lastWriteTimer.ElapsedMilliseconds);

            // Update song Infos, directory tree, and sort
            this.UpdateScoreSaberDataMapping();
            this.UpdatePlayCounts();

            // Check if we need to upgrade settings file favorites
            try
            {
                this.Settings.ConvertFavoritesToPlaylist(_levelIdToCustomLevel, _levelIdToSongVersion);
            }
            catch (Exception e)
            {
                Logger.Exception("FAILED TO CONVERT FAVORITES TO PLAYLIST!", e);
            }

            // load the current editing playlist or make one
            if (CurrentEditingPlaylist == null && !String.IsNullOrEmpty(this.Settings.currentEditingPlaylistFile))
            {
                Logger.Debug("Loading playlist for editing: {0}", this.Settings.currentEditingPlaylistFile);
                CurrentEditingPlaylist = Playlist.LoadPlaylist(this.Settings.currentEditingPlaylistFile);
                PlaylistsCollection.MatchSongsForPlaylist(CurrentEditingPlaylist);
            }

            if (CurrentEditingPlaylist == null)
            {
                Logger.Debug("Current editing playlist does not exit, create...");
                CurrentEditingPlaylist = new Playlist
                {
                    playlistTitle  = "Song Browser Favorites",
                    playlistAuthor = "SongBrowserPlugin",
                    fileLoc        = this.Settings.currentEditingPlaylistFile,
                    image          = Base64Sprites.PlaylistIconB64,
                    songs          = new List <PlaylistSong>(),
                };
            }

            CurrentEditingPlaylistLevelIds = new HashSet <string>();
            foreach (PlaylistSong ps in CurrentEditingPlaylist.songs)
            {
                // Sometimes we cannot match a song
                if (ps.level == null)
                {
                    continue;
                }

                CurrentEditingPlaylistLevelIds.Add(ps.level.levelID);
            }

            // Actually sort and filter
            //this.ProcessSongList();

            // Signal complete
            if (SongLoader.CustomLevels.Count > 0)
            {
                didFinishProcessingSongs?.Invoke(SongLoader.CustomLevels);
            }

            timer.Stop();

            Logger.Info("Updating songs infos took {0}ms", timer.ElapsedMilliseconds);
        }
Пример #17
0
        /// <summary>
        /// Sort the song list based on the settings.
        /// </summary>
        public void ProcessSongList()
        {
            Logger.Trace("ProcessSongList()");

            // This has come in handy many times for debugging issues with Newest.

            /*foreach (BeatmapLevelSO level in _originalSongs)
             * {
             *  if (_levelIdToCustomLevel.ContainsKey(level.levelID))
             *  {
             *      Logger.Debug("HAS KEY {0}: {1}", _levelIdToCustomLevel[level.levelID].customSongInfo.path, level.levelID);
             *  }
             *  else
             *  {
             *      Logger.Debug("Missing KEY: {0}", level.levelID);
             *  }
             * }*/

            // TODO - remove as part of unifying song list interface
            if (_isPreviewLevelPack)
            {
                return;
            }

            if (_levelPackToSongs.Count == 0)
            {
                Logger.Debug("Cannot process songs yet, level packs have not been processed...");
                return;
            }

            if (this._currentLevelPack == null || !this._levelPackToSongs.ContainsKey(this._currentLevelPack.packName))
            {
                Logger.Debug("Cannot process songs yet, no level pack selected...");
                return;
            }

            // Playlist filter will load the original songs.
            List <BeatmapLevelSO> unsortedSongs = null;
            List <BeatmapLevelSO> filteredSongs = null;

            if (this._settings.filterMode == SongFilterMode.Playlist && this.CurrentPlaylist != null)
            {
                unsortedSongs = null;
            }
            else
            {
                Logger.Debug("Using songs from level pack: {0}", this._currentLevelPack.packName);
                unsortedSongs = new List <BeatmapLevelSO>(_levelPackToSongs[this._currentLevelPack.packName]);
            }

            // filter
            Logger.Debug("Starting filtering songs...");
            Stopwatch stopwatch = Stopwatch.StartNew();

            switch (_settings.filterMode)
            {
            case SongFilterMode.Favorites:
                filteredSongs = FilterFavorites();
                break;

            case SongFilterMode.Search:
                filteredSongs = FilterSearch(unsortedSongs);
                break;

            case SongFilterMode.Playlist:
                filteredSongs = FilterPlaylist();
                break;

            case SongFilterMode.None:
            default:
                Logger.Info("No song filter selected...");
                filteredSongs = unsortedSongs;
                break;
            }

            stopwatch.Stop();
            Logger.Info("Filtering songs took {0}ms", stopwatch.ElapsedMilliseconds);

            // sort
            Logger.Debug("Starting to sort songs...");
            stopwatch = Stopwatch.StartNew();

            switch (_settings.sortMode)
            {
            case SongSortMode.Original:
                SortOriginal(filteredSongs);
                break;

            case SongSortMode.Newest:
                SortNewest(filteredSongs);
                break;

            case SongSortMode.Author:
                SortAuthor(filteredSongs);
                break;

            case SongSortMode.PlayCount:
                SortPlayCount(filteredSongs);
                break;

            case SongSortMode.PP:
                SortPerformancePoints(filteredSongs);
                break;

            case SongSortMode.Difficulty:
                SortDifficulty(filteredSongs);
                break;

            case SongSortMode.Random:
                SortRandom(filteredSongs);
                break;

            case SongSortMode.Default:
            default:
                SortSongName(filteredSongs);
                break;
            }

            if (this.Settings.invertSortResults && _settings.sortMode != SongSortMode.Random)
            {
                _sortedSongs.Reverse();
            }

            stopwatch.Stop();
            Logger.Info("Sorting songs took {0}ms", stopwatch.ElapsedMilliseconds);

            this.OverwriteCurrentLevelPack();
            //_sortedSongs.ForEach(x => Logger.Debug(x.levelID));
        }
Пример #18
0
 private void SceneManager_sceneLoaded(Scene to, LoadSceneMode loadMode)
 {
     Logger.Debug($"Loaded scene \"{to.name}\"");
 }
        /// <summary>
        /// Get the song cache from the game.
        /// </summary>
        public void UpdateSongLists(BeatmapCharacteristicSO gameplayMode)
        {
            // give up
            if (gameplayMode == null)
            {
                Logger.Debug("Always null first time if user waits for SongLoader event, which they should...");
                return;
            }

            Stopwatch timer = new Stopwatch();

            timer.Start();

            // Get the level collection from song loader
            LevelCollectionSO levelCollections = Resources.FindObjectsOfTypeAll <LevelCollectionSO>().FirstOrDefault();

            // Stash everything we need
            _originalSongs = levelCollections.GetLevelsWithBeatmapCharacteristic(gameplayMode).ToList();

            Logger.Debug("Got {0} songs from level collections...", _originalSongs.Count);
            //_originalSongs.ForEach(x => Logger.Debug("{0} by {1} = {2}", x.name, x.levelAuthorName, x.levelID));

            _sortedSongs = _originalSongs;
            CurrentBeatmapCharacteristicSO = gameplayMode;

            // Calculate some information about the custom song dir
            String customSongsPath                   = Path.Combine(Environment.CurrentDirectory, CUSTOM_SONGS_DIR);
            String revSlashCustomSongPath            = customSongsPath.Replace('\\', '/');
            double currentCustomSongDirLastWriteTIme = (File.GetLastWriteTimeUtc(customSongsPath) - EPOCH).TotalMilliseconds;
            bool   customSongDirChanged              = false;

            if (_customSongDirLastWriteTime != currentCustomSongDirLastWriteTIme)
            {
                customSongDirChanged        = true;
                _customSongDirLastWriteTime = currentCustomSongDirLastWriteTIme;
            }

            // This operation scales well
            if (!Directory.Exists(customSongsPath))
            {
                Logger.Error("CustomSong directory is missing...");
                return;
            }
            IEnumerable <string> directories = Directory.EnumerateDirectories(customSongsPath, "*.*", SearchOption.AllDirectories);

            // Get LastWriteTimes
            Stopwatch lastWriteTimer = new Stopwatch();

            lastWriteTimer.Start();
            foreach (var level in SongLoader.CustomLevels)
            {
                // If we already know this levelID, don't both updating it.
                // SongLoader should filter duplicates but in case of failure we don't want to crash

                if (!_cachedLastWriteTimes.ContainsKey(level.levelID) || customSongDirChanged)
                {
                    _cachedLastWriteTimes[level.levelID] = (File.GetLastWriteTimeUtc(level.customSongInfo.path) - EPOCH).TotalMilliseconds;
                }

                if (!_levelIdToCustomLevel.ContainsKey(level.levelID))
                {
                    _levelIdToCustomLevel.Add(level.levelID, level);
                }

                if (!_levelIdToSongVersion.ContainsKey(level.levelID))
                {
                    DirectoryInfo info = new DirectoryInfo(level.customSongInfo.path);
                    string        currentDirectoryName = info.Name;

                    String version = level.customSongInfo.path.Replace(revSlashCustomSongPath, "").Replace(currentDirectoryName, "").Replace("/", "");
                    if (!String.IsNullOrEmpty(version))
                    {
                        //Logger.Debug("MATCH");
                        _levelIdToSongVersion.Add(level.levelID, version);
                        _keyToSong.Add(version, level);
                    }
                }
            }

            lastWriteTimer.Stop();
            Logger.Info("Determining song download time and determining mappings took {0}ms", lastWriteTimer.ElapsedMilliseconds);

            // Update song Infos, directory tree, and sort
            this.UpdateScoreSaberDataMapping();
            this.UpdatePlayCounts();
            this.UpdateDirectoryTree(customSongsPath);

            // Check if we need to upgrade settings file favorites
            try
            {
                this.Settings.ConvertFavoritesToPlaylist(_levelIdToCustomLevel, _levelIdToSongVersion);
            }
            catch (Exception e)
            {
                Logger.Exception("FAILED TO CONVERT FAVORITES TO PLAYLIST!", e);
            }

            // load the current editing playlist or make one
            if (!String.IsNullOrEmpty(this.Settings.currentEditingPlaylistFile))
            {
                CurrentEditingPlaylist = PlaylistsReader.ParsePlaylist(this.Settings.currentEditingPlaylistFile);
            }

            if (CurrentEditingPlaylist == null)
            {
                CurrentEditingPlaylist = new Playlist
                {
                    Title  = "Song Browser Favorites",
                    Author = "SongBrowserPlugin",
                    Path   = this.Settings.currentEditingPlaylistFile,
                    Image  = Base64Sprites.PlaylistIconB64,
                    Songs  = new List <PlaylistSong>(),
                };
            }

            CurrentEditingPlaylistLevelIds = new HashSet <string>();
            foreach (PlaylistSong ps in CurrentEditingPlaylist.Songs)
            {
                CurrentEditingPlaylistLevelIds.Add(ps.LevelId);
            }

            // Actually sort and filter
            this.ProcessSongList();

            // Signal complete
            if (SongLoader.CustomLevels.Count > 0)
            {
                didFinishProcessingSongs?.Invoke(SongLoader.CustomLevels);
            }

            timer.Stop();

            Logger.Info("Updating songs infos took {0}ms", timer.ElapsedMilliseconds);
            Logger.Debug("Song Browser knows about {0} songs from SongLoader...", _originalSongs.Count);
        }
        /// <summary>
        /// Sort the song list based on the settings.
        /// </summary>
        public void ProcessSongList()
        {
            Logger.Trace("ProcessSongList()");

            // This has come in handy many times for debugging issues with Newest.

            /*foreach (LevelSO level in _originalSongs)
             * {
             *  if (_levelIdToCustomLevel.ContainsKey(level.levelID))
             *  {
             *      Logger.Debug("HAS KEY {0}: {1}", _levelIdToCustomLevel[level.levelID].customSongInfo.path, level.levelID);
             *  }
             *  else
             *  {
             *      Logger.Debug("Missing KEY: {0}", level.levelID);
             *  }
             * }*/

            if (_directoryStack.Count <= 0)
            {
                Logger.Debug("Cannot process songs yet, songs infos have not been processed...");
                return;
            }

            // Playlist filter will load the original songs.
            if (this._settings.filterMode == SongFilterMode.Playlist && this.CurrentPlaylist != null)
            {
                _originalSongs = null;
            }
            else
            {
                Logger.Debug("Showing songs for directory: {0}", _directoryStack.Peek().Key);
                _originalSongs = _directoryStack.Peek().Levels;
            }

            // filter
            Logger.Debug("Starting filtering songs...");
            Stopwatch stopwatch = Stopwatch.StartNew();

            switch (_settings.filterMode)
            {
            case SongFilterMode.Favorites:
                FilterFavorites();
                break;

            case SongFilterMode.Search:
                FilterSearch(_originalSongs);
                break;

            case SongFilterMode.Playlist:
                FilterPlaylist();
                break;

            case SongFilterMode.None:
            default:
                Logger.Info("No song filter selected...");
                _filteredSongs = _originalSongs;
                break;
            }

            stopwatch.Stop();
            Logger.Info("Filtering songs took {0}ms", stopwatch.ElapsedMilliseconds);

            // sort
            Logger.Debug("Starting to sort songs...");
            stopwatch = Stopwatch.StartNew();

            switch (_settings.sortMode)
            {
            case SongSortMode.Original:
                SortOriginal(_filteredSongs);
                break;

            case SongSortMode.Newest:
                SortNewest(_filteredSongs);
                break;

            case SongSortMode.Author:
                SortAuthor(_filteredSongs);
                break;

            case SongSortMode.PlayCount:
                SortPlayCount(_filteredSongs);
                break;

            case SongSortMode.PP:
                SortPerformancePoints(_filteredSongs);
                break;

            case SongSortMode.Difficulty:
                SortDifficulty(_filteredSongs);
                break;

            case SongSortMode.Random:
                SortRandom(_filteredSongs);
                break;

            case SongSortMode.Default:
            default:
                SortSongName(_filteredSongs);
                break;
            }

            if (this.Settings.invertSortResults && _settings.sortMode != SongSortMode.Random)
            {
                _sortedSongs.Reverse();
            }

            stopwatch.Stop();
            Logger.Info("Sorting songs took {0}ms", stopwatch.ElapsedMilliseconds);

            //_sortedSongs.ForEach(x => Logger.Debug(x.levelID));
        }