From cd4331881c2317e8477b6478a2ca0537e2a36b43 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Fri, 9 Oct 2026 16:55:39 +0200 Subject: [PATCH] Don't list unplayed songs on the statistics cards Cards only list songs that have been played. With fewer than six played songs they are all listed as most played, so no song is listed twice. The soundtrack popup still lists every song. Closes #46 --- statistics/soundtrack_card.gd | 42 ++++++++++++++++++----- tests/integration/test_statistics_view.gd | 40 ++++++++++++++++++--- 2 files changed, 69 insertions(+), 13 deletions(-) diff --git a/statistics/soundtrack_card.gd b/statistics/soundtrack_card.gd index f48dd4e..bfe5178 100644 --- a/statistics/soundtrack_card.gd +++ b/statistics/soundtrack_card.gd @@ -1,12 +1,15 @@ class_name SoundtrackCard extends PanelContainer ## One soundtrack in the statistics view: its name, how often and when it was last -## played, and its most and least played songs. Clicking it asks for all its songs. +## played, and its most and least played songs. Songs never played are not listed, and +## the least played are only shown when there are enough played songs to not repeat +## any. Clicking it asks for all its songs. ## Emitted when the card is clicked. signal pressed(soundtrack: SoundtrackWithSongs) -## How many of the most and least played songs the card lists. +## How many of the most and least played songs the card lists. With fewer than twice +## as many played songs, all of them are listed as most played. const SONGS_SHOWN: int = 3 ## The soundtrack the card shows. @@ -36,13 +39,17 @@ func show_soundtrack(data: SoundtrackWithSongs) -> void: last_played_label.text = last_played_text(data.soundtrack_last_played) last_played_label.visible = !data.soundtrack_last_played.is_empty() - var most_played: Array[SongInfoForStats] = songs_by_plays(data.songs).slice(0, SONGS_SHOWN) - # Without the songs already shown as most played - var least_played: Array[SongInfoForStats] = songs_by_plays(data.songs, true).filter( - func(song: SongInfoForStats) -> bool: return !most_played.has(song) - ).slice(0, SONGS_SHOWN) + var most_played: Array[SongInfoForStats] = [] + var least_played: Array[SongInfoForStats] = [] + var played: Array[SongInfoForStats] = played_songs(data.songs) + if played.size() >= SONGS_SHOWN * 2: + most_played = played.slice(0, SONGS_SHOWN) + least_played = songs_by_plays(played, true).slice(0, SONGS_SHOWN) + else: + # Too few to split without listing a song twice, so all of them are most played + most_played = played - most_played_header.text = "Most played" if !data.songs.is_empty() else "No songs" + most_played_header.text = song_list_header(data.songs.size(), played.size()) _fill_song_list(most_played_list, most_played) least_played_header.visible = !least_played.is_empty() _fill_song_list(least_played_list, least_played) @@ -63,6 +70,25 @@ static func last_played_text(last_played: String) -> String: return "Last played " + last_played.substr(0, 10) +## The songs of [param songs] that have been played, most played first. Songs never +## played are left out: the card only lists them in the popup with every song. +static func played_songs(songs: Array[SongInfoForStats]) -> Array[SongInfoForStats]: + var played: Array[SongInfoForStats] = [] + played.assign(songs_by_plays(songs).filter( + func(song: SongInfoForStats) -> bool: return song.times_played > 0)) + return played + + +## The header above the most played songs, for a soundtrack with [param song_count] +## songs of which [param played_count] have been played. +static func song_list_header(song_count: int, played_count: int) -> String: + if song_count == 0: + return "No songs" + if played_count == 0: + return "No songs played yet" + return "Most played" + + ## One line of a song list: how often the song was played, then its name. static func song_text(song: SongInfoForStats) -> String: return "%d × %s" % [song.times_played, song.song_name] diff --git a/tests/integration/test_statistics_view.gd b/tests/integration/test_statistics_view.gd index 804f885..b36bff5 100644 --- a/tests/integration/test_statistics_view.gd +++ b/tests/integration/test_statistics_view.gd @@ -12,6 +12,7 @@ const STATISTICS: Array[Dictionary] = [ {"song_name": "To Far Away Times", "times_played": 0}, {"song_name": "Schala's Theme", "times_played": 4}, {"song_name": "Peaceful Days", "times_played": 1}, + {"song_name": "Zeal Palace", "times_played": 1}, ]}, {"soundtrack_uuid": "st-2", "soundtrack_name": "Terranigma", "soundtrack_played": 0, "songs": [{"song_name": "Underworld", "times_played": 0}]}, @@ -151,20 +152,48 @@ func test_card_shows_plays_last_played_and_most_and_least_played_songs() -> void assert_str(card.last_played_label.text).is_equal("Last played 2026-09-30") assert_array(_visible_texts(card.most_played_list)).contains_exactly( ["4 × Schala's Theme", "3 × Frog's Theme", "2 × Battle with Magus"]) + # Songs never played are left out, so the least played are the least played of those assert_array(_visible_texts(card.least_played_list)).contains_exactly( - ["0 × To Far Away Times", "0 × Wind Scene", "1 × Corridors of Time"]) + ["1 × Corridors of Time", "1 × Peaceful Days", "1 × Zeal Palace"]) + assert_bool(card.least_played_header.visible).is_true() -func test_card_of_never_played_soundtrack() -> void: +func test_card_of_never_played_soundtrack_lists_no_songs() -> void: await _open_statistics() var card: SoundtrackCard = _card("Terranigma") assert_str(card.played_label.text).is_equal("Never played") assert_bool(card.last_played_label.visible).is_false() - assert_array(_visible_texts(card.most_played_list)).contains_exactly(["0 × Underworld"]) + assert_str(card.most_played_header.text).is_equal("No songs played yet") + assert_array(_visible_texts(card.most_played_list)).is_empty() assert_bool(card.least_played_header.visible).is_false() +func test_card_with_one_played_song_lists_only_it() -> void: + await _open_statistics() + var card: SoundtrackCard = _card("Final Fantasy VI") + + assert_str(card.most_played_header.text).is_equal("Most played") + assert_array(_visible_texts(card.most_played_list)).contains_exactly(["1 × Dancing Mad"]) + assert_bool(card.least_played_header.visible).is_false() + assert_array(_visible_texts(card.least_played_list)).is_empty() + + +func test_card_with_fewer_than_six_played_songs_lists_them_all_as_most_played() -> void: + var songs: Array[Dictionary] = [{"song_name": "Never", "times_played": 0}] + for i: int in 5: + songs.append({"song_name": "Song %d" % (i + 1), "times_played": i + 1}) + _server.respond_json("/statistics/soundtracks", [ + {"soundtrack_name": "Lufia II", "soundtrack_played": 15, "songs": songs}]) + await _open_statistics() + var card: SoundtrackCard = _card("Lufia II") + + assert_array(_visible_texts(card.most_played_list)).contains_exactly( + ["5 × Song 5", "4 × Song 4", "3 × Song 3", "2 × Song 2", "1 × Song 1"]) + assert_bool(card.least_played_header.visible).is_false() + assert_array(_visible_texts(card.least_played_list)).is_empty() + + func test_card_without_songs() -> void: await _open_statistics() var card: SoundtrackCard = _card("earthbound") @@ -300,13 +329,14 @@ func test_clicking_a_card_shows_all_its_songs_most_played_first() -> void: assert_bool(popup.visible).is_true() assert_str(popup.title_label.text).is_equal("Chrono Trigger") assert_str(popup.summary_label.text) \ - .is_equal("Played 5 times · Last played 2026-09-30 · 7 songs") + .is_equal("Played 5 times · Last played 2026-09-30 · 8 songs") var rows: Array[String] = [] for row: ListRow in popup.song_list.get_children(): rows.append(row.text) assert_array(rows).contains_exactly([ "4 × Schala's Theme", "3 × Frog's Theme", "2 × Battle with Magus", - "1 × Corridors of Time", "1 × Peaceful Days", "0 × To Far Away Times", "0 × Wind Scene", + "1 × Corridors of Time", "1 × Peaceful Days", "1 × Zeal Palace", + "0 × To Far Away Times", "0 × Wind Scene", ])