Public Access
feat(library): use set total playtime in sort and stats
Sort, total playtime, most played, genres, moods and the public profile use the set total of an entry, as the library card does. New view user_game_entry_playtime (V45) is the one SQL rule.
This commit is contained in:
1 parent
1ff1e061c7
commit
4ed8a341af
11 files changed
+253
-62
No files matched your search
@@ -3,6 +3,7 @@ package core_service.modules.library.api;
|
||||
import core_service.modules.library.domain.GameAchievementSchemaMeta;
|
||||
import core_service.modules.library.domain.GamePriority;
|
||||
import core_service.modules.library.domain.PsnTrophySet;
|
||||
import core_service.modules.library.domain.StatGroup;
|
||||
import core_service.modules.library.domain.GameStatus;
|
||||
import core_service.modules.library.domain.SteamGame;
|
||||
import core_service.modules.library.domain.UserGame;
|
||||
@@ -13,6 +14,7 @@ import core_service.modules.library.repo.PsnTrophyRepository;
|
||||
import core_service.modules.library.repo.SteamGameRepository;
|
||||
import core_service.modules.library.repo.UserGameAchievementMetaRepository;
|
||||
import core_service.modules.library.repo.UserGameRepository;
|
||||
import core_service.modules.library.service.UserStatsService;
|
||||
import core_service.shared.test.AbstractIntegrationTest;
|
||||
import core_service.shared.test.ContractTokens;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -28,10 +30,12 @@ import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.hamcrest.Matchers.nullValue;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
@@ -86,6 +90,9 @@ class PublicProfileControllerTest extends AbstractIntegrationTest {
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@Autowired
|
||||
private UserStatsService userStatsService;
|
||||
|
||||
private void platinum(long appid) {
|
||||
psnTrophyRepository.saveSet(USER_ID, appid, new PsnTrophySet("NPWR1_00", "trophy2", "01.00", "Set", null, "PS5",
|
||||
50, 0, 0, 0, 30, true, Instant.now()));
|
||||
@@ -211,6 +218,25 @@ class PublicProfileControllerTest extends AbstractIntegrationTest {
|
||||
.andExpect(jsonPath("$.stats").value(nullValue()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void publicProfile_PlaytimeNumbersEqualTheOwnersStats() throws Exception {
|
||||
jdbc.update("insert into user_psn_title (user_id, psn_key, state, appid, platform, playtime_minutes) "
|
||||
+ "values (?, 'c1', 'STEAM_OWNED', 220, 'PS5', 700)", USER_ID);
|
||||
jdbc.update("update user_game set status = 'PLAYING' where user_id = ? and appid = 620", USER_ID);
|
||||
var own = userStatsService.getStats(USER_ID, Set.of(StatGroup.OVERVIEW, StatGroup.TOP_GAMES));
|
||||
|
||||
assertEquals(1000L + 900 + 60, own.overview().totalPlaytimeMinutes());
|
||||
mockMvc.perform(get("/api/public/profiles/library")
|
||||
.header("X-Profile-Ticket", ticket(USER_ID, List.of("TOP_GAMES", "OVERVIEW"))))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.totalPlaytimeMinutes").value(own.overview().totalPlaytimeMinutes()))
|
||||
.andExpect(jsonPath("$.playing", hasSize(2)))
|
||||
.andExpect(jsonPath("$.playing[0].name").value("Half-Life 2"))
|
||||
.andExpect(jsonPath("$.playing[0].playtimeMinutes").value(1000))
|
||||
.andExpect(jsonPath("$.stats.topGames.mostPlayed[0].appid").value(220))
|
||||
.andExpect(jsonPath("$.stats.topGames.mostPlayed[0].minutes").value(1000));
|
||||
}
|
||||
|
||||
@Test
|
||||
void publicLibrary_OnlyTheSharedStatGroups() throws Exception {
|
||||
mockMvc.perform(get("/api/public/profiles/library")
|
||||
|
||||
@@ -15,6 +15,7 @@ import jakarta.persistence.EntityManager;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
@@ -31,10 +32,13 @@ import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.tuple;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@Import(GameLinkRepository.class)
|
||||
class UserGameRepositoryTest extends AbstractPersistenceTest {
|
||||
|
||||
private static final long USER_ID = 1L;
|
||||
@@ -45,6 +49,9 @@ class UserGameRepositoryTest extends AbstractPersistenceTest {
|
||||
@Autowired
|
||||
private UserGameRepository userGameRepository;
|
||||
|
||||
@Autowired
|
||||
private GameLinkRepository gameLinkRepository;
|
||||
|
||||
@Autowired
|
||||
private JdbcTemplate jdbc;
|
||||
|
||||
@@ -218,6 +225,28 @@ class UserGameRepositoryTest extends AbstractPersistenceTest {
|
||||
assertEquals(2, page.getTotalElements());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findOwnedGames_CountQueryWithFilters_MatchesTheListedEntries() {
|
||||
var page = userGameRepository.findOwnedGames(USER_ID, false, GameStatus.PLAYING, null, null, ALL_SOURCES,
|
||||
PageRequest.of(0, 1, Sort.by("appid")));
|
||||
|
||||
assertEquals(1, page.getContent().size());
|
||||
assertEquals(appids(GameStatus.PLAYING, null).size(), page.getTotalElements());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findOwnedGames_HiddenGames_SortByTheirOwnPlaytime() {
|
||||
hide(620L, 700L);
|
||||
titlePlay("c1", 700L, "STEAM_OWNED", "PS5", 2000);
|
||||
|
||||
// hidden games are not in the entry view: they sort by their own playtime
|
||||
var desc = userGameRepository.findOwnedGames(USER_ID, true, null, null, null, ALL_SOURCES,
|
||||
PageRequest.of(0, 20, UserGameRepository.querySort(Sort.by(Sort.Direction.DESC, "playtimeMinutes"))));
|
||||
|
||||
assertEquals(List.of(620L, 700L), desc.map(UserGame::getAppid).getContent());
|
||||
assertEquals(2, desc.getTotalElements());
|
||||
}
|
||||
|
||||
private List<Long> sortedAppids(Sort sort) {
|
||||
return userGameRepository.findOwnedGames(USER_ID, false, null, null, null, ALL_SOURCES, PageRequest.of(0, 20, UserGameRepository.querySort(sort)))
|
||||
.map(UserGame::getAppid).getContent();
|
||||
@@ -241,6 +270,49 @@ class UserGameRepositoryTest extends AbstractPersistenceTest {
|
||||
assertEquals(List.of(380L, 700L, 220L, 620L), sortedAppids(Sort.by(Sort.Direction.DESC, "priority")));
|
||||
}
|
||||
|
||||
private void titlePlay(String key, long appid, String state, String platform, int minutes) {
|
||||
jdbc.update("insert into user_psn_title (user_id, psn_key, state, appid, platform, playtime_minutes) "
|
||||
+ "values (?, ?, ?, ?, ?, ?)", USER_ID, key, state, appid, platform, minutes);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findOwnedGames_SortsByPlaytimeByTheEntryTotal() {
|
||||
titlePlay("c1", 220L, "STEAM_OWNED", "PS5", 700);
|
||||
|
||||
assertEquals(List.of(220L, 620L, 380L, 700L), sortedAppids(Sort.by(Sort.Direction.DESC, "playtimeMinutes")));
|
||||
assertEquals(List.of(380L, 700L, 620L, 220L), sortedAppids(Sort.by(Sort.Direction.ASC, "playtimeMinutes")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sumPlaytime_IsTheSumOfTheEntryTotals() {
|
||||
titlePlay("c1", 220L, "STEAM_OWNED", "PS5", 700);
|
||||
steamGameRepository.saveAllAndFlush(List.of(new SteamGame(800L, "Bloodborne", null, Instant.now()),
|
||||
new SteamGame(-7L, "Manual", null, Instant.now())));
|
||||
jdbc.update("insert into user_game (user_id, appid, status, source, platform, playtime_minutes) "
|
||||
+ "values (?, 800, 'BACKLOG', 'PSN', 'PS5', 50)", USER_ID);
|
||||
titlePlay("c2", 800L, "LINKED", "PS5", 400);
|
||||
jdbc.update("insert into user_game (user_id, appid, status, source, platform, playtime_minutes) "
|
||||
+ "values (?, -7, 'BACKLOG', 'MANUAL', 'PS5', 30)", USER_ID);
|
||||
titlePlay("c3", -7L, "LINKED", "PS5", 90);
|
||||
|
||||
long sum = userGameRepository.sumPlaytime(USER_ID);
|
||||
|
||||
assertEquals(1000 + 900 + 400 + 90, sum);
|
||||
assertEquals(sum, gameLinkRepository.entryPlaytimes(USER_ID, List.of(220L, 380L, 620L, 700L, 800L, -7L))
|
||||
.values().stream().mapToLong(Integer::longValue).sum());
|
||||
}
|
||||
|
||||
@Test
|
||||
void findEntriesPlayedInTwoWeeks_ShowsTheEntryTotal() {
|
||||
titlePlay("c1", 220L, "STEAM_OWNED", "PS5", 700);
|
||||
userGameRepository.findById(new UserGameId(USER_ID, 220L)).orElseThrow().setPlaytime2Weeks(30);
|
||||
userGameRepository.flush();
|
||||
|
||||
assertThat(userGameRepository.findEntriesPlayedInTwoWeeks(USER_ID, 20))
|
||||
.extracting(RecentEntry::getAppid, RecentEntry::getPlaytimeMinutes)
|
||||
.containsExactly(tuple(220L, 1000L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void querySort_EqualPlaytime_BreaksTiesByAppid() {
|
||||
assertEquals(List.of(620L, 220L, 380L, 700L), sortedAppids(Sort.by(Sort.Direction.DESC, "playtimeMinutes")));
|
||||
|
||||
@@ -179,6 +179,35 @@ class UserStatsRepositoryTest extends AbstractPersistenceTest {
|
||||
.containsExactly(620L, 220L);
|
||||
}
|
||||
|
||||
private void seedEntryTotal() {
|
||||
userGameRepository.saveAllAndFlush(List.of(
|
||||
game(220L, 100, GameStatus.PLAYING, Instant.now()),
|
||||
game(620L, 300, GameStatus.PLAYING, Instant.now())));
|
||||
jdbc.update("insert into user_psn_title (user_id, psn_key, state, appid, platform, playtime_minutes) "
|
||||
+ "values (?, 'c1', 'STEAM_OWNED', 220, 'PS5', 500)", USER_ID);
|
||||
}
|
||||
|
||||
@Test
|
||||
void mostPlayed_UsesTheEntryTotal() {
|
||||
seedEntryTotal();
|
||||
|
||||
assertThat(repository.mostPlayed(USER_ID, 5)).extracting(UserStats.GameMinutes::appid, UserStats.GameMinutes::minutes)
|
||||
.containsExactly(tuple(220L, 600L), tuple(620L, 300L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void games_ListsShowTheEntryTotal() {
|
||||
seedEntryTotal();
|
||||
|
||||
for (StatKey key : List.of(StatKey.MOST_PLAYED, StatKey.TOTAL)) {
|
||||
assertThat(repository.games(USER_ID, key, "", BERLIN, 0, 10))
|
||||
.extracting(UserStats.StatGame::appid, UserStats.StatGame::playtimeMinutes)
|
||||
.contains(tuple(220L, 600L));
|
||||
}
|
||||
assertThat(repository.games(USER_ID, StatKey.MOST_PLAYED, "", BERLIN, 0, 10))
|
||||
.extracting(UserStats.StatGame::appid).containsExactly(220L, 620L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void mostLoyal_MostMonthsWins_MoreMinutesBreakATie() {
|
||||
userGameRepository.saveAllAndFlush(List.of(
|
||||
|
||||
Reference in new issue
Block a user