Skip to content

Commit

Permalink
Add option to sort by play count (#931)
Browse files Browse the repository at this point in the history
* Add option to sort by Play Count.

* Flesh out sorting by play count

Adds category headers for played and unplayed songs

SettingsManager now tracks previous sort order.
Unplayed songs are now sorted in whatever order was previously used before switching to play count order.

* Improve play count sort behavior when only bots are connected.

Remove unnecessary _sortPlaycounts variable in SongContainer
Disallow offering Play Count sort in PopupMenu if there are no non-bot profiles connected
Add check to MusicLibraryMenu.OnEnable to change sort order from Play Count if no non-bot profiles connected
Update SongContainer.GetPlaycounts to handle situation where bot profile manages to have Play Count sort selected or Play Count is selected when no profiles are connected

* Title sort by default when sort order from profile is unrecognized.

Only count plays with the profile's current instrument when calculating play count.
As a fallback, if the current instrument is invalid, all plays by the current/first non-bot profile will be returned.
Update PlayerContainer to also force a reload on change of active profiles when current sort is Playcount.

* Address review comments by EliteAsian123

Make GetSortedCategory switch fallback use less arcane syntax.
Add OnlyHasBots method to PlayerContainer to check for non-bot players and use that instead of repeating a LINQ expression in several different places.

* Accept songs with duplicate hash in play count sorted list.
  • Loading branch information
wyrdough authored Jan 18, 2025
1 parent 4bcb6ee commit 7e95887
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 7 deletions.
13 changes: 13 additions & 0 deletions Assets/Script/Menu/MusicLibrary/MusicLibraryMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,13 @@ private void OnEnable()
// Show no player warning
_noPlayerWarning.SetActive(PlayerContainer.Players.Count <= 0);

// Make sure sort is not by play count if there are only bots
if (PlayerContainer.OnlyHasBots() &&
SettingsManager.Settings.LibrarySort == SortAttribute.Playcount)
{
// Name makes a good fallback?
ChangeSort(SortAttribute.Name);
}
}

protected override void OnSelectedIndexChanged()
Expand Down Expand Up @@ -630,6 +637,12 @@ public void RefreshAndReselect()

public void ChangeSort(SortAttribute sort)
{
// Keep the previous sort attribute, too, so it can be used to
// sort the list of unplayed songs and possibly for other things
if (sort != SortAttribute.Playcount)
{
SettingsManager.Settings.PreviousLibrarySort = sort;
}
SettingsManager.Settings.LibrarySort = sort;
UpdateSearch(true);
}
Expand Down
8 changes: 7 additions & 1 deletion Assets/Script/Menu/MusicLibrary/PopupMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ private void CreateMainMenu()
UpdateForState();
});
}

var viewType = _musicLibrary.CurrentSelection;

// Add/remove to favorites
Expand Down Expand Up @@ -188,6 +188,12 @@ private void CreateSortSelect()
continue;
}

// Skip Play count if there are no real players
if (sort == SortAttribute.Playcount && PlayerContainer.OnlyHasBots())
{
continue;
}

if (sort >= SortAttribute.Instrument)
{
break;
Expand Down
15 changes: 14 additions & 1 deletion Assets/Script/Player/PlayerContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,8 @@ public static bool SwapPlayerToProfile(YargPlayer player, YargProfile newProfile

private static void ActiveProfilesChanged()
{
if (SettingsManager.Settings.LibrarySort == SortAttribute.Playable)
if (SettingsManager.Settings.LibrarySort == SortAttribute.Playable ||
SettingsManager.Settings.LibrarySort == SortAttribute.Playcount)
{
MusicLibraryMenu.SetReload(MusicLibraryReloadState.Full);
}
Expand Down Expand Up @@ -299,5 +300,17 @@ public static void EnsureValidInstruments()

}
}

public static bool OnlyHasBots()
{
for (int i = 0; i < _players.Count; i++)
{
if (!_players[i].Profile.IsBot)
{
return false;
}
}
return true;
}
}
}
33 changes: 33 additions & 0 deletions Assets/Script/Scores/ScoreContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Linq;
using SQLite;
using YARG.Core;
using YARG.Core.Game;
using YARG.Core.Logging;
using YARG.Core.Song;
using YARG.Helpers;
Expand Down Expand Up @@ -407,6 +408,38 @@ public static List<SongEntry> GetMostPlayedSongs(int maxCount)
return new List<SongEntry>();
}

// order true sorts descending, false ascending
// this is the same as GetMostPlayedSongs, but is limited to the one profile and returns the entire list
public static List<SongEntry> GetPlayedSongsForUserByPlaycount(YargProfile profile, bool order)
{
var sortOrder = order ? "DESC" : "ASC";
var profileId = profile.Id;
var profileInstrument = (int) profile.CurrentInstrument;
var songList = new List<SongEntry>();
var query = "SELECT COUNT(GameRecords.Id), GameRecords.SongChecksum from GameRecords, PlayerScores " +
"WHERE PlayerScores.GameRecordId = GameRecords.Id " +
$"AND PlayerScores.PlayerId = '{profileId}' ";

// If the profile instrument is bad, we can still return all scores for the profile
if (profile.HasValidInstrument)
{
query += $"AND PlayerScores.Instrument = {profileInstrument} ";
}

query += $"GROUP BY GameRecords.SongChecksum ORDER BY COUNT(GameRecords.Id) {sortOrder}";

var playCounts = _db.Query<PlayCountRecord>(query);
foreach (var record in playCounts)
{
var hash = HashWrapper.Create(record.SongChecksum);
if (SongContainer.SongsByHash.TryGetValue(hash, out var list))
{
songList.AddRange(list);
}
}
return songList;
}

public static void Destroy()
{
_db.Dispose();
Expand Down
1 change: 1 addition & 0 deletions Assets/Script/Settings/SettingsManager.Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ public class SettingContainer
public bool ShowExperimentalWarningDialog = true;

public SortAttribute LibrarySort = SortAttribute.Name;
public SortAttribute PreviousLibrarySort = SortAttribute.Name;

public Dictionary<string, HUDPositionProfile> HUDPositionProfiles = new();

Expand Down
64 changes: 60 additions & 4 deletions Assets/Script/Song/SongContainer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using YARG.Core.Song.Cache;
using YARG.Core.Song;
using System;
using System.Linq;
using YARG.Helpers.Extensions;
using YARG.Settings;
using YARG.Helpers;
Expand All @@ -12,6 +13,7 @@
using YARG.Player;
using YARG.Localization;
using YARG.Core.Extensions;
using YARG.Scores;

namespace YARG.Song
{
Expand All @@ -30,6 +32,7 @@ public enum SortAttribute
SongLength,
DateAdded,
Playable,
Playcount,

Instrument,
FiveFretGuitar,
Expand Down Expand Up @@ -155,7 +158,7 @@ public static async UniTask RunRefresh(bool quick, LoadingContext? context = nul

public static SongCategory[] GetSortedCategory(SortAttribute sort)
{
return sort switch
var proposedSort = sort switch
{
SortAttribute.Name => _sortTitles,
SortAttribute.Artist => _sortArtists,
Expand All @@ -168,6 +171,7 @@ public static SongCategory[] GetSortedCategory(SortAttribute sort)
SortAttribute.Artist_Album => _sortArtistAlbums,
SortAttribute.SongLength => _sortSongLengths,
SortAttribute.DateAdded => _sortDatesAdded,
SortAttribute.Playcount => GetPlaycounts(),
SortAttribute.Playable => GetPlayableSongs(),

SortAttribute.FiveFretGuitar => _sortInstruments[Instrument.FiveFretGuitar],
Expand All @@ -191,8 +195,19 @@ public static SongCategory[] GetSortedCategory(SortAttribute sort)
SortAttribute.Vocals => _sortInstruments[Instrument.Vocals],
SortAttribute.Harmony => _sortInstruments[Instrument.Harmony],
SortAttribute.Band => _sortInstruments[Instrument.Band],
_ => throw new Exception("stoopid"),
_ => null
};

// Make life better when people go back a version and we
// encounter sorts we don't understand by providing a
// default rather than a blank song library
if (proposedSort != null)
{
return proposedSort;
}

YargLogger.LogInfo("Invalid Sort Attribute. Defaulting to Name sort.");
return _sortTitles;
}

public static bool HasInstrument(Instrument instrument)
Expand Down Expand Up @@ -280,6 +295,47 @@ public static SongEntry GetRandomSong()
return _songs.Pick();
}

// Play count sorting is intentionally not cached, as it must be regenerated after
// every play, when profiles change, and probably a bunch of other stuff
private static SongCategory[] GetPlaycounts()
{
// This should never happen since play count shouldn't be selectable without
// a non-bot profile and MusicLibraryMenu already checks for this, but let's double check
if (PlayerContainer.OnlyHasBots())
{
// Titles seems like a reasonable fallback
return _sortTitles;
}
var player = PlayerContainer.Players.First(e => !e.Profile.IsBot);

var counts = ScoreContainer.GetPlayedSongsForUserByPlaycount(player.Profile, true);
// Get all the unplayed songs and stuff them on the end of the list
var zeroPlaySongs = new List<SongEntry>();
var previousSort = SettingsManager.Settings.PreviousLibrarySort;

if (previousSort == SortAttribute.Unspecified)
{
// I don't think this should ever happen, but I'm not certain,
// so belt and suspenders wins.
previousSort = SortAttribute.Name;
}

foreach (var category in GetSortedCategory(previousSort))
{
foreach (var song in category.Songs)
{
if (!counts.Contains(song))
{
zeroPlaySongs.Add(song);
}
}
}
var countCategories = new SongCategory[2];
countCategories[0] = new SongCategory("PLAYED SONGS", counts.ToArray(), "Played Songs");
countCategories[1] = new SongCategory("UNPLAYED SONGS", zeroPlaySongs.ToArray(), "Unplayed Songs");
return countCategories;
}

private static void UpdateSongUi(LoadingContext context)
{
var tracker = CacheHandler.Progress;
Expand Down Expand Up @@ -322,7 +378,7 @@ private static void UpdateSongUi(LoadingContext context)
private static void FillContainers()
{
_songs = SetAllSongs(_songCache.Entries);

_sortArtists = Convert(_songCache.Artists, SongAttribute.Artist, true);
_sortAlbums = Convert(_songCache.Albums, SongAttribute.Album, true);
_sortGenres = Convert(_songCache.Genres, SongAttribute.Genre, false);
Expand Down Expand Up @@ -388,7 +444,7 @@ static SongEntry[] SetAllSongs(Dictionary<HashWrapper, List<SongEntry>> entries)
static SongCategory[] Convert(SortedDictionary<SortString, List<SongEntry>> list, SongAttribute attribute, bool createCategoryGroups)
{
var sections = new SongCategory[list.Count];

int index = 0;
foreach (var node in list)
{
Expand Down
3 changes: 2 additions & 1 deletion Assets/StreamingAssets/lang/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@
"ProKeys": "Pro Keys",
"Vocals": "Vocals",
"Harmony": "Harmony",
"Band": "Band"
"Band": "Band",
"Playcount": "Play Count"
}
},
"Menu": {
Expand Down

0 comments on commit 7e95887

Please sign in to comment.