SongBrowserModel.cs 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862
  1. using SongBrowser.DataAccess;
  2. using SongBrowser.UI;
  3. using SongCore.OverrideClasses;
  4. using SongCore.Utilities;
  5. using System;
  6. using System.Collections.Generic;
  7. using System.Diagnostics;
  8. using System.IO;
  9. using System.Linq;
  10. using System.Text.RegularExpressions;
  11. using UnityEngine;
  12. using static StandardLevelInfoSaveData;
  13. using Logger = SongBrowser.Logging.Logger;
  14. namespace SongBrowser
  15. {
  16. public class SongBrowserModel
  17. {
  18. private readonly String CUSTOM_SONGS_DIR = Path.Combine("Beat Saber_Data", "CustomLevels");
  19. private readonly DateTime EPOCH = new DateTime(1970, 1, 1);
  20. // song_browser_settings.xml
  21. private SongBrowserSettings _settings;
  22. // song list management
  23. private double _customSongDirLastWriteTime = 0;
  24. private Dictionary<String, CustomPreviewBeatmapLevel> _levelIdToCustomLevel;
  25. private Dictionary<String, double> _cachedLastWriteTimes;
  26. private Dictionary<string, int> _weights;
  27. private Dictionary<BeatmapDifficulty, int> _difficultyWeights;
  28. private Dictionary<string, ScrappedSong> _levelHashToDownloaderData = null;
  29. private Dictionary<string, ScoreSaberData> _levelIdToScoreSaberData = null;
  30. private Dictionary<string, int> _levelIdToPlayCount;
  31. private Dictionary<string, string> _levelIdToSongVersion;
  32. public BeatmapCharacteristicSO CurrentBeatmapCharacteristicSO;
  33. public static Action<Dictionary<string, CustomPreviewBeatmapLevel>> didFinishProcessingSongs;
  34. /// <summary>
  35. /// Get the settings the model is using.
  36. /// </summary>
  37. public SongBrowserSettings Settings
  38. {
  39. get
  40. {
  41. return _settings;
  42. }
  43. }
  44. /// <summary>
  45. /// Map LevelID to score saber data.
  46. /// </summary>
  47. public Dictionary<string, ScoreSaberData> LevelIdToScoreSaberData
  48. {
  49. get
  50. {
  51. return _levelIdToScoreSaberData;
  52. }
  53. }
  54. /// <summary>
  55. /// Get the last selected (stored in settings) level id.
  56. /// </summary>
  57. public String LastSelectedLevelId
  58. {
  59. get
  60. {
  61. return _settings.currentLevelId;
  62. }
  63. set
  64. {
  65. _settings.currentLevelId = value;
  66. _settings.Save();
  67. }
  68. }
  69. private Playlist _currentPlaylist;
  70. /// <summary>
  71. /// Manage the current playlist if one exists.
  72. /// </summary>
  73. public Playlist CurrentPlaylist
  74. {
  75. get
  76. {
  77. if (_currentPlaylist == null)
  78. {
  79. _currentPlaylist = Playlist.LoadPlaylist(this._settings.currentPlaylistFile);
  80. }
  81. return _currentPlaylist;
  82. }
  83. set
  84. {
  85. _settings.currentPlaylistFile = value.fileLoc;
  86. _currentPlaylist = value;
  87. }
  88. }
  89. /// <summary>
  90. /// Current editing playlist
  91. /// </summary>
  92. public Playlist CurrentEditingPlaylist;
  93. /// <summary>
  94. /// HashSet of LevelIds for quick lookup
  95. /// </summary>
  96. public HashSet<String> CurrentEditingPlaylistLevelIds;
  97. /// <summary>
  98. /// Constructor.
  99. /// </summary>
  100. public SongBrowserModel()
  101. {
  102. _levelIdToCustomLevel = new Dictionary<string, CustomPreviewBeatmapLevel>();
  103. _cachedLastWriteTimes = new Dictionary<String, double>();
  104. _levelIdToScoreSaberData = new Dictionary<string, ScoreSaberData>();
  105. _levelIdToPlayCount = new Dictionary<string, int>();
  106. _levelIdToSongVersion = new Dictionary<string, string>();
  107. CurrentEditingPlaylistLevelIds = new HashSet<string>();
  108. // Weights used for keeping the original songs in order
  109. // Invert the weights from the game so we can order by descending and make LINQ work with us...
  110. /* Level4, Level2, Level9, Level5, Level10, Level6, Level7, Level1, Level3, Level8, Level11 */
  111. _weights = new Dictionary<string, int>
  112. {
  113. ["OneHopeLevel"] = 12,
  114. ["100Bills"] = 11,
  115. ["Escape"] = 10,
  116. ["Legend"] = 9,
  117. ["BeatSaber"] = 8,
  118. ["AngelVoices"] = 7,
  119. ["CountryRounds"] = 6,
  120. ["BalearicPumping"] = 5,
  121. ["Breezer"] = 4,
  122. ["CommercialPumping"] = 3,
  123. ["TurnMeOn"] = 2,
  124. ["LvlInsane"] = 1,
  125. ["100BillsOneSaber"] = 12,
  126. ["EscapeOneSaber"] = 11,
  127. ["LegendOneSaber"] = 10,
  128. ["BeatSaberOneSaber"] = 9,
  129. ["CommercialPumpingOneSaber"] = 8,
  130. ["TurnMeOnOneSaber"] = 8,
  131. };
  132. _difficultyWeights = new Dictionary<BeatmapDifficulty, int>
  133. {
  134. [BeatmapDifficulty.Easy] = 1,
  135. [BeatmapDifficulty.Normal] = 2,
  136. [BeatmapDifficulty.Hard] = 4,
  137. [BeatmapDifficulty.Expert] = 8,
  138. [BeatmapDifficulty.ExpertPlus] = 16,
  139. };
  140. }
  141. /// <summary>
  142. /// Init this model.
  143. /// </summary>
  144. /// <param name="songSelectionMasterView"></param>
  145. /// <param name="songListViewController"></param>
  146. public void Init()
  147. {
  148. _settings = SongBrowserSettings.Load();
  149. Logger.Info("Settings loaded, sorting mode is: {0}", _settings.sortMode);
  150. }
  151. /// <summary>
  152. /// Easy invert of toggling.
  153. /// </summary>
  154. public void ToggleInverting()
  155. {
  156. this.Settings.invertSortResults = !this.Settings.invertSortResults;
  157. }
  158. /// <summary>
  159. /// Get the song cache from the game.
  160. /// </summary>
  161. public void UpdateLevelRecords()
  162. {
  163. Stopwatch timer = new Stopwatch();
  164. timer.Start();
  165. // Calculate some information about the custom song dir
  166. String customSongsPath = Path.Combine(Environment.CurrentDirectory, CUSTOM_SONGS_DIR);
  167. String revSlashCustomSongPath = customSongsPath.Replace('\\', '/');
  168. double currentCustomSongDirLastWriteTIme = (File.GetLastWriteTimeUtc(customSongsPath) - EPOCH).TotalMilliseconds;
  169. bool customSongDirChanged = false;
  170. if (_customSongDirLastWriteTime != currentCustomSongDirLastWriteTIme)
  171. {
  172. customSongDirChanged = true;
  173. _customSongDirLastWriteTime = currentCustomSongDirLastWriteTIme;
  174. }
  175. if (!Directory.Exists(customSongsPath))
  176. {
  177. Logger.Error("CustomSong directory is missing...");
  178. return;
  179. }
  180. // Map some data for custom songs
  181. Regex r = new Regex(@"(\d+-\d+)", RegexOptions.IgnoreCase);
  182. Stopwatch lastWriteTimer = new Stopwatch();
  183. lastWriteTimer.Start();
  184. foreach (KeyValuePair<string, CustomPreviewBeatmapLevel> level in SongCore.Loader.CustomLevels)
  185. {
  186. // If we already know this levelID, don't both updating it.
  187. // SongLoader should filter duplicates but in case of failure we don't want to crash
  188. if (!_cachedLastWriteTimes.ContainsKey(level.Value.levelID) || customSongDirChanged)
  189. {
  190. // Always use the newest date.
  191. var lastWriteTime = File.GetLastWriteTimeUtc(level.Value.customLevelPath);
  192. var lastCreateTime = File.GetCreationTimeUtc(level.Value.customLevelPath);
  193. var lastTime = lastWriteTime > lastCreateTime ? lastWriteTime : lastCreateTime;
  194. _cachedLastWriteTimes[level.Value.levelID] = (lastTime - EPOCH).TotalMilliseconds;
  195. }
  196. if (!_levelIdToCustomLevel.ContainsKey(level.Value.levelID))
  197. {
  198. _levelIdToCustomLevel.Add(level.Value.levelID, level.Value);
  199. }
  200. if (!_levelIdToSongVersion.ContainsKey(level.Value.levelID))
  201. {
  202. DirectoryInfo info = new DirectoryInfo(level.Value.customLevelPath);
  203. string currentDirectoryName = info.Name;
  204. Match m = r.Match(level.Value.customLevelPath);
  205. if (m.Success)
  206. {
  207. String version = m.Groups[1].Value;
  208. _levelIdToSongVersion.Add(level.Value.levelID, version);
  209. }
  210. }
  211. }
  212. lastWriteTimer.Stop();
  213. Logger.Info("Determining song download time and determining mappings took {0}ms", lastWriteTimer.ElapsedMilliseconds);
  214. // Update song Infos, directory tree, and sort
  215. this.UpdateScoreSaberDataMapping();
  216. this.UpdatePlayCounts();
  217. // Check if we need to upgrade settings file favorites
  218. try
  219. {
  220. this.Settings.ConvertFavoritesToPlaylist(_levelIdToCustomLevel, _levelIdToSongVersion);
  221. }
  222. catch (Exception e)
  223. {
  224. Logger.Exception("FAILED TO CONVERT FAVORITES TO PLAYLIST!", e);
  225. }
  226. // load the current editing playlist or make one
  227. if (CurrentEditingPlaylist == null && !String.IsNullOrEmpty(this.Settings.currentEditingPlaylistFile))
  228. {
  229. Logger.Debug("Loading playlist for editing: {0}", this.Settings.currentEditingPlaylistFile);
  230. CurrentEditingPlaylist = Playlist.LoadPlaylist(this.Settings.currentEditingPlaylistFile);
  231. PlaylistsCollection.MatchSongsForPlaylist(CurrentEditingPlaylist);
  232. }
  233. if (CurrentEditingPlaylist == null)
  234. {
  235. Logger.Debug("Current editing playlist does not exit, create...");
  236. CurrentEditingPlaylist = new Playlist
  237. {
  238. playlistTitle = "Song Browser Favorites",
  239. playlistAuthor = "SongBrowser",
  240. fileLoc = this.Settings.currentEditingPlaylistFile,
  241. image = Base64Sprites.PlaylistIconB64,
  242. songs = new List<PlaylistSong>(),
  243. };
  244. }
  245. CurrentEditingPlaylistLevelIds = new HashSet<string>();
  246. foreach (PlaylistSong ps in CurrentEditingPlaylist.songs)
  247. {
  248. // Sometimes we cannot match a song
  249. string levelId = null;
  250. if (ps.level != null)
  251. {
  252. levelId = ps.level.levelID;
  253. }
  254. else if (!String.IsNullOrEmpty(ps.levelId))
  255. {
  256. levelId = ps.levelId;
  257. }
  258. else
  259. {
  260. continue;
  261. }
  262. CurrentEditingPlaylistLevelIds.Add(levelId);
  263. }
  264. // Signal complete
  265. if (SongCore.Loader.CustomLevels.Count > 0)
  266. {
  267. didFinishProcessingSongs?.Invoke(SongCore.Loader.CustomLevels);
  268. }
  269. timer.Stop();
  270. Logger.Info("Updating songs infos took {0}ms", timer.ElapsedMilliseconds);
  271. }
  272. /// <summary>
  273. /// SongLoader doesn't fire event when we delete a song.
  274. /// </summary>
  275. /// <param name="levelPack"></param>
  276. /// <param name="levelId"></param>
  277. public void RemoveSongFromLevelPack(IBeatmapLevelPack levelPack, String levelId)
  278. {
  279. levelPack.beatmapLevelCollection.beatmapLevels.ToList().RemoveAll(x => x.levelID == levelId);
  280. }
  281. /// <summary>
  282. /// Update the gameplay play counts.
  283. /// </summary>
  284. /// <param name="gameplayMode"></param>
  285. private void UpdatePlayCounts()
  286. {
  287. // Reset current playcounts
  288. _levelIdToPlayCount = new Dictionary<string, int>();
  289. // Build a map of levelId to sum of all playcounts and sort.
  290. PlayerDataModelSO playerData = Resources.FindObjectsOfTypeAll<PlayerDataModelSO>().FirstOrDefault();
  291. foreach (var levelData in playerData.currentLocalPlayer.levelsStatsData)
  292. {
  293. int currentCount = 0;
  294. if (!_levelIdToPlayCount.ContainsKey(levelData.levelID))
  295. {
  296. _levelIdToPlayCount.Add(levelData.levelID, currentCount);
  297. }
  298. _levelIdToPlayCount[levelData.levelID] += (currentCount + levelData.playCount);
  299. }
  300. }
  301. /// <summary>
  302. /// Parse the current pp data file.
  303. /// Public so controllers can decide when to update it.
  304. /// </summary>
  305. public void UpdateScoreSaberDataMapping()
  306. {
  307. Logger.Trace("UpdateScoreSaberDataMapping()");
  308. ScoreSaberDataFile scoreSaberDataFile = ScoreSaberDatabaseDownloader.ScoreSaberDataFile;
  309. // bail
  310. if (scoreSaberDataFile == null)
  311. {
  312. Logger.Warning("Score saber data is not ready yet...");
  313. return;
  314. }
  315. foreach (var level in SongCore.Loader.CustomLevels)
  316. {
  317. // Skip
  318. if (_levelIdToScoreSaberData.ContainsKey(level.Value.levelID))
  319. {
  320. continue;
  321. }
  322. ScoreSaberData scoreSaberData = null;
  323. // try to version match first
  324. if (_levelIdToSongVersion.ContainsKey(level.Value.levelID))
  325. {
  326. String version = _levelIdToSongVersion[level.Value.levelID];
  327. if (scoreSaberDataFile.SongVersionToScoreSaberData.ContainsKey(version))
  328. {
  329. scoreSaberData = scoreSaberDataFile.SongVersionToScoreSaberData[version];
  330. }
  331. }
  332. if (scoreSaberData != null)
  333. {
  334. //Logger.Debug("{0} = {1}pp", level.songName, pp);
  335. _levelIdToScoreSaberData.Add(level.Value.levelID, scoreSaberData);
  336. }
  337. }
  338. }
  339. /// <summary>
  340. /// Map the downloader data for quick lookup.
  341. /// </summary>
  342. /// <param name="songs"></param>
  343. public void UpdateDownloaderDataMapping(List<ScrappedSong> songs)
  344. {
  345. _levelHashToDownloaderData = new Dictionary<string, ScrappedSong>();
  346. foreach (ScrappedSong song in songs)
  347. {
  348. if (_levelHashToDownloaderData.ContainsKey(song.Hash))
  349. {
  350. continue;
  351. }
  352. _levelHashToDownloaderData.Add(song.Hash, song);
  353. }
  354. }
  355. /// <summary>
  356. /// Add Song to Editing Playlist
  357. /// </summary>
  358. /// <param name="songInfo"></param>
  359. public void AddSongToEditingPlaylist(IBeatmapLevel songInfo)
  360. {
  361. if (this.CurrentEditingPlaylist == null)
  362. {
  363. return;
  364. }
  365. this.CurrentEditingPlaylist.songs.Add(new PlaylistSong()
  366. {
  367. songName = songInfo.songName,
  368. levelId = songInfo.levelID,
  369. hash = songInfo.levelID,
  370. key = _levelIdToSongVersion.ContainsKey(songInfo.levelID) ? _levelIdToSongVersion[songInfo.levelID] : "",
  371. });
  372. this.CurrentEditingPlaylistLevelIds.Add(songInfo.levelID);
  373. this.CurrentEditingPlaylist.SavePlaylist();
  374. }
  375. /// <summary>
  376. /// Remove Song from editing playlist
  377. /// </summary>
  378. /// <param name="levelId"></param>
  379. public void RemoveSongFromEditingPlaylist(IBeatmapLevel songInfo)
  380. {
  381. if (this.CurrentEditingPlaylist == null)
  382. {
  383. return;
  384. }
  385. this.CurrentEditingPlaylist.songs.RemoveAll(x => x.level != null && x.level.levelID == songInfo.levelID);
  386. this.CurrentEditingPlaylistLevelIds.RemoveWhere(x => x == songInfo.levelID);
  387. this.CurrentEditingPlaylist.SavePlaylist();
  388. }
  389. /// <summary>
  390. /// Overwrite the current level pack.
  391. /// </summary>
  392. private void OverwriteCurrentLevelPack(IBeatmapLevelPack pack, List<IPreviewBeatmapLevel> sortedLevels)
  393. {
  394. Logger.Debug("Overwriting levelPack [{0}] beatmapLevelCollection.levels", pack);
  395. if (pack.packID == PluginConfig.CUSTOM_SONG_LEVEL_PACK_ID || (pack as SongCoreCustomBeatmapLevelPack) != null)
  396. {
  397. Logger.Debug("Overwriting SongCore Level Pack collection...");
  398. var newLevels = sortedLevels.Select(x => x as CustomPreviewBeatmapLevel);
  399. ReflectionUtil.SetPrivateField(pack.beatmapLevelCollection, "_customPreviewBeatmapLevels", newLevels.ToArray());
  400. }
  401. else
  402. {
  403. // Hack to see if level pack is purchased or not.
  404. BeatmapLevelPackSO beatmapLevelPack = pack as BeatmapLevelPackSO;
  405. if ((pack as BeatmapLevelPackSO) != null)
  406. {
  407. Logger.Debug("Owned level pack...");
  408. var newLevels = sortedLevels.Select(x => x as BeatmapLevelSO);
  409. ReflectionUtil.SetPrivateField(pack.beatmapLevelCollection, "_beatmapLevels", newLevels.ToArray());
  410. }
  411. else
  412. {
  413. Logger.Debug("Unowned DLC Detected...");
  414. var newLevels = sortedLevels.Select(x => x as PreviewBeatmapLevelSO);
  415. ReflectionUtil.SetPrivateField(pack.beatmapLevelCollection, "_beatmapLevels", newLevels.ToArray());
  416. }
  417. Logger.Debug("Overwriting Regular Collection...");
  418. }
  419. }
  420. /// <summary>
  421. /// Sort the song list based on the settings.
  422. /// </summary>
  423. public void ProcessSongList(IBeatmapLevelPack pack)
  424. {
  425. Logger.Trace("ProcessSongList()");
  426. List<IPreviewBeatmapLevel> unsortedSongs = null;
  427. List<IPreviewBeatmapLevel> filteredSongs = null;
  428. List<IPreviewBeatmapLevel> sortedSongs = null;
  429. // This has come in handy many times for debugging issues with Newest.
  430. /*foreach (BeatmapLevelSO level in _originalSongs)
  431. {
  432. if (_levelIdToCustomLevel.ContainsKey(level.levelID))
  433. {
  434. Logger.Debug("HAS KEY {0}: {1}", _levelIdToCustomLevel[level.levelID].customSongInfo.path, level.levelID);
  435. }
  436. else
  437. {
  438. Logger.Debug("Missing KEY: {0}", level.levelID);
  439. }
  440. }*/
  441. // Abort
  442. if (pack == null)
  443. {
  444. Logger.Debug("Cannot process songs yet, no level pack selected...");
  445. return;
  446. }
  447. // fetch unsorted songs.
  448. if (this._settings.filterMode == SongFilterMode.Playlist && this.CurrentPlaylist != null)
  449. {
  450. unsortedSongs = null;
  451. }
  452. else
  453. {
  454. Logger.Debug("Using songs from level pack: {0}", pack.packID);
  455. unsortedSongs = pack.beatmapLevelCollection.beatmapLevels.ToList();
  456. }
  457. // filter
  458. Logger.Debug("Starting filtering songs...");
  459. Stopwatch stopwatch = Stopwatch.StartNew();
  460. switch (_settings.filterMode)
  461. {
  462. case SongFilterMode.Favorites:
  463. filteredSongs = FilterFavorites(pack);
  464. break;
  465. case SongFilterMode.Search:
  466. filteredSongs = FilterSearch(unsortedSongs);
  467. break;
  468. case SongFilterMode.Playlist:
  469. filteredSongs = FilterPlaylist(pack);
  470. break;
  471. case SongFilterMode.None:
  472. default:
  473. Logger.Info("No song filter selected...");
  474. filteredSongs = unsortedSongs;
  475. break;
  476. }
  477. stopwatch.Stop();
  478. Logger.Info("Filtering songs took {0}ms", stopwatch.ElapsedMilliseconds);
  479. // sort
  480. Logger.Debug("Starting to sort songs...");
  481. stopwatch = Stopwatch.StartNew();
  482. switch (_settings.sortMode)
  483. {
  484. case SongSortMode.Original:
  485. sortedSongs = SortOriginal(filteredSongs);
  486. break;
  487. case SongSortMode.Newest:
  488. sortedSongs = SortNewest(filteredSongs);
  489. break;
  490. case SongSortMode.Author:
  491. sortedSongs = SortAuthor(filteredSongs);
  492. break;
  493. case SongSortMode.UpVotes:
  494. sortedSongs = SortUpVotes(filteredSongs);
  495. break;
  496. case SongSortMode.PlayCount:
  497. sortedSongs = SortPlayCount(filteredSongs);
  498. break;
  499. case SongSortMode.PP:
  500. sortedSongs = SortPerformancePoints(filteredSongs);
  501. break;
  502. case SongSortMode.Difficulty:
  503. sortedSongs = SortDifficulty(filteredSongs);
  504. break;
  505. case SongSortMode.Random:
  506. sortedSongs = SortRandom(filteredSongs);
  507. break;
  508. case SongSortMode.Default:
  509. default:
  510. sortedSongs = SortSongName(filteredSongs);
  511. break;
  512. }
  513. if (this.Settings.invertSortResults && _settings.sortMode != SongSortMode.Random)
  514. {
  515. sortedSongs.Reverse();
  516. }
  517. stopwatch.Stop();
  518. Logger.Info("Sorting songs took {0}ms", stopwatch.ElapsedMilliseconds);
  519. this.OverwriteCurrentLevelPack(pack, sortedSongs);
  520. //_sortedSongs.ForEach(x => Logger.Debug(x.levelID));
  521. }
  522. /// <summary>
  523. /// For now the editing playlist will be considered the favorites playlist.
  524. /// Users can edit the settings file themselves.
  525. /// </summary>
  526. private List<IPreviewBeatmapLevel> FilterFavorites(IBeatmapLevelPack pack)
  527. {
  528. Logger.Info("Filtering song list as favorites playlist...");
  529. if (this.CurrentEditingPlaylist != null)
  530. {
  531. this.CurrentPlaylist = this.CurrentEditingPlaylist;
  532. }
  533. return this.FilterPlaylist(pack);
  534. }
  535. /// <summary>
  536. /// Filter for a search query.
  537. /// </summary>
  538. /// <param name="levels"></param>
  539. /// <returns></returns>
  540. private List<IPreviewBeatmapLevel> FilterSearch(List<IPreviewBeatmapLevel> levels)
  541. {
  542. // Make sure we can actually search.
  543. if (this._settings.searchTerms.Count <= 0)
  544. {
  545. Logger.Error("Tried to search for a song with no valid search terms...");
  546. SortSongName(levels);
  547. return levels;
  548. }
  549. string searchTerm = this._settings.searchTerms[0];
  550. if (String.IsNullOrEmpty(searchTerm))
  551. {
  552. Logger.Error("Empty search term entered.");
  553. SortSongName(levels);
  554. return levels;
  555. }
  556. Logger.Info("Filtering song list by search term: {0}", searchTerm);
  557. var terms = searchTerm.Split(' ');
  558. foreach (var term in terms)
  559. {
  560. levels = levels.Intersect(
  561. levels
  562. .Where(x => $"{x.songName} {x.songSubName} {x.songAuthorName} {x.levelAuthorName}".ToLower().Contains(term.ToLower()))
  563. .ToList(
  564. )
  565. ).ToList();
  566. }
  567. return levels;
  568. }
  569. /// <summary>
  570. /// Filter for a playlist (favorites uses this).
  571. /// </summary>
  572. /// <param name="pack"></param>
  573. /// <returns></returns>
  574. private List<IPreviewBeatmapLevel> FilterPlaylist(IBeatmapLevelPack pack)
  575. {
  576. // bail if no playlist, usually means the settings stored one the user then moved.
  577. if (this.CurrentPlaylist == null)
  578. {
  579. Logger.Error("Trying to load a null playlist...");
  580. this.Settings.filterMode = SongFilterMode.None;
  581. return null;
  582. }
  583. // Get song keys
  584. PlaylistsCollection.MatchSongsForPlaylist(this.CurrentPlaylist, true);
  585. Logger.Debug("Filtering songs for playlist: {0}", this.CurrentPlaylist.playlistTitle);
  586. Dictionary<String, IPreviewBeatmapLevel> levelDict = new Dictionary<string, IPreviewBeatmapLevel>();
  587. foreach (var level in pack.beatmapLevelCollection.beatmapLevels)
  588. {
  589. if (!levelDict.ContainsKey(level.levelID))
  590. {
  591. levelDict.Add(level.levelID, level);
  592. }
  593. }
  594. List<IPreviewBeatmapLevel> songList = new List<IPreviewBeatmapLevel>();
  595. foreach (PlaylistSong ps in this.CurrentPlaylist.songs)
  596. {
  597. if (ps.level != null && levelDict.ContainsKey(ps.level.levelID))
  598. {
  599. songList.Add(levelDict[ps.level.levelID]);
  600. }
  601. else
  602. {
  603. Logger.Debug("Could not find song in playlist: {0}", ps.songName);
  604. }
  605. }
  606. Logger.Debug("Playlist filtered song count: {0}", songList.Count);
  607. return songList;
  608. }
  609. /// <summary>
  610. /// Sorting returns original list.
  611. /// </summary>
  612. /// <param name="levels"></param>
  613. /// <returns></returns>
  614. private List<IPreviewBeatmapLevel> SortOriginal(List<IPreviewBeatmapLevel> levels)
  615. {
  616. Logger.Info("Sorting song list as original");
  617. return levels;
  618. }
  619. /// <summary>
  620. /// Sorting by newest (file time, creation+modified).
  621. /// </summary>
  622. /// <param name="levels"></param>
  623. /// <returns></returns>
  624. private List<IPreviewBeatmapLevel> SortNewest(List<IPreviewBeatmapLevel> levels)
  625. {
  626. Logger.Info("Sorting song list as newest.");
  627. return levels
  628. .OrderBy(x => _weights.ContainsKey(x.levelID) ? _weights[x.levelID] : 0)
  629. .ThenByDescending(x => !_levelIdToCustomLevel.ContainsKey(x.levelID) ? (_weights.ContainsKey(x.levelID) ? _weights[x.levelID] : 0) : _cachedLastWriteTimes[x.levelID])
  630. .ToList();
  631. }
  632. /// <summary>
  633. /// Sorting by the song author.
  634. /// </summary>
  635. /// <param name="levelIds"></param>
  636. /// <returns></returns>
  637. private List<IPreviewBeatmapLevel> SortAuthor(List<IPreviewBeatmapLevel> levelIds)
  638. {
  639. Logger.Info("Sorting song list by author");
  640. return levelIds
  641. .OrderBy(x => x.songAuthorName)
  642. .ThenBy(x => x.songName)
  643. .ToList();
  644. }
  645. /// <summary>
  646. /// Sorting by song play count.
  647. /// </summary>
  648. /// <param name="levels"></param>
  649. /// <returns></returns>
  650. private List<IPreviewBeatmapLevel> SortPlayCount(List<IPreviewBeatmapLevel> levels)
  651. {
  652. Logger.Info("Sorting song list by playcount");
  653. return levels
  654. .OrderByDescending(x => _levelIdToPlayCount.ContainsKey(x.levelID) ? _levelIdToPlayCount[x.levelID] : 0)
  655. .ThenBy(x => x.songName)
  656. .ToList();
  657. }
  658. /// <summary>
  659. /// Sorting by PP.
  660. /// </summary>
  661. /// <param name="levels"></param>
  662. /// <returns></returns>
  663. private List<IPreviewBeatmapLevel> SortPerformancePoints(List<IPreviewBeatmapLevel> levels)
  664. {
  665. Logger.Info("Sorting song list by performance points...");
  666. return levels
  667. .OrderByDescending(x => _levelIdToScoreSaberData.ContainsKey(x.levelID) ? _levelIdToScoreSaberData[x.levelID].maxPp : 0)
  668. .ToList();
  669. }
  670. private List<IPreviewBeatmapLevel> SortDifficulty(List<IPreviewBeatmapLevel> levels)
  671. {
  672. Logger.Info("Sorting song list by difficulty...");
  673. IEnumerable<BeatmapDifficulty> difficultyIterator = Enum.GetValues(typeof(BeatmapDifficulty)).Cast<BeatmapDifficulty>();
  674. Dictionary<string, int> levelIdToDifficultyValue = new Dictionary<string, int>();
  675. foreach (IPreviewBeatmapLevel level in levels)
  676. {
  677. // only need to process a level once
  678. if (levelIdToDifficultyValue.ContainsKey(level.levelID))
  679. {
  680. continue;
  681. }
  682. // TODO - fix, not honoring beatmap characteristic.
  683. int difficultyValue = 0;
  684. if (level as BeatmapLevelSO != null)
  685. {
  686. var beatmapSet = (level as BeatmapLevelSO).difficultyBeatmapSets;
  687. difficultyValue = beatmapSet
  688. .SelectMany(x => x.difficultyBeatmaps)
  689. .Sum(x => _difficultyWeights[x.difficulty]);
  690. }
  691. else if (_levelIdToCustomLevel.ContainsKey(level.levelID))
  692. {
  693. var beatmapSet = (_levelIdToCustomLevel[level.levelID] as CustomPreviewBeatmapLevel).standardLevelInfoSaveData.difficultyBeatmapSets;
  694. difficultyValue = beatmapSet
  695. .SelectMany(x => x.difficultyBeatmaps)
  696. .Sum(x => _difficultyWeights[(BeatmapDifficulty)Enum.Parse(typeof(BeatmapDifficulty), x.difficulty)]);
  697. }
  698. levelIdToDifficultyValue.Add(level.levelID, difficultyValue);
  699. }
  700. return levels
  701. .OrderBy(x => levelIdToDifficultyValue[x.levelID])
  702. .ThenBy(x => x.songName)
  703. .ToList();
  704. }
  705. /// <summary>
  706. /// Randomize the sorting.
  707. /// </summary>
  708. /// <param name="levelIds"></param>
  709. /// <returns></returns>
  710. private List<IPreviewBeatmapLevel> SortRandom(List<IPreviewBeatmapLevel> levelIds)
  711. {
  712. Logger.Info("Sorting song list by random (seed={0})...", this.Settings.randomSongSeed);
  713. System.Random rnd = new System.Random(this.Settings.randomSongSeed);
  714. return levelIds
  715. .OrderBy(x => rnd.Next())
  716. .ToList();
  717. }
  718. /// <summary>
  719. /// Sorting by the song name.
  720. /// </summary>
  721. /// <param name="levels"></param>
  722. /// <returns></returns>
  723. private List<IPreviewBeatmapLevel> SortSongName(List<IPreviewBeatmapLevel> levels)
  724. {
  725. Logger.Info("Sorting song list as default (songName)");
  726. return levels
  727. .OrderBy(x => x.songName)
  728. .ThenBy(x => x.songAuthorName)
  729. .ToList();
  730. }
  731. /// <summary>
  732. /// Sorting by Downloader UpVotes.
  733. /// </summary>
  734. /// <param name="levelIds"></param>
  735. /// <returns></returns>
  736. private List<IPreviewBeatmapLevel> SortUpVotes(List<IPreviewBeatmapLevel> levelIds)
  737. {
  738. Logger.Info("Sorting song list by UpVotes");
  739. // Do not always have data when trying to sort by UpVotes
  740. if (_levelHashToDownloaderData == null)
  741. {
  742. return levelIds;
  743. }
  744. return levelIds
  745. .OrderByDescending(x => {
  746. var hash = x.levelID.Split('_')[2];
  747. if (_levelHashToDownloaderData.ContainsKey(hash))
  748. {
  749. return _levelHashToDownloaderData[hash].Upvotes;
  750. }
  751. else
  752. {
  753. return int.MinValue;
  754. }
  755. })
  756. .ToList();
  757. }
  758. }
  759. }