Skip to content

Commit ac6261f

Browse files
committed
Load CurseForge browse banners asynchronously
1 parent f5ff7c5 commit ac6261f

4 files changed

Lines changed: 73 additions & 0 deletions

File tree

‎launcher/src/main/java/net/modtale/launcher/api/ModtaleApiClient.java‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import net.modtale.launcher.model.project.ProjectGallery;
2424
import net.modtale.launcher.model.project.ProjectMeta;
2525
import net.modtale.launcher.model.project.ProjectPage;
26+
import net.modtale.launcher.model.project.ProjectSummary;
2627
import net.modtale.launcher.model.project.ProjectVersion;
2728
import net.modtale.launcher.model.project.ProjectVersionChangelog;
2829
import net.modtale.launcher.model.project.VersionDependenciesView;
@@ -194,6 +195,10 @@ public ProjectPage searchCurseForgeMods(ProjectSearchQuery query) {
194195
return nyoCfClient.search(query);
195196
}
196197

198+
public ProjectSummary enrichCurseForgeBrowseBanner(ProjectSummary summary) {
199+
return nyoCfClient.enrichBrowseBanner(summary);
200+
}
201+
197202
public ProjectDetail getCurseForgeProject(long projectId) {
198203
return nyoCfClient.project(projectId);
199204
}

‎launcher/src/main/java/net/modtale/launcher/api/NyoCfClient.java‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,27 @@ ProjectDetail project(long projectId) {
116116
Map.of(), false, false, null, versions(files, website, null), List.of(), List.of());
117117
}
118118

119+
ProjectSummary enrichBrowseBanner(ProjectSummary summary) {
120+
if (summary == null || !summary.isCurseForge()
121+
|| (summary.bannerUrl() != null && !summary.bannerUrl().isBlank())) {
122+
return summary;
123+
}
124+
try {
125+
long projectId = summary.curseForgeProjectId();
126+
JsonNode project = metadata(projectId);
127+
validateProject(project, projectId);
128+
String bannerUrl = firstScreenshot(project, "thumbnail_url");
129+
if (bannerUrl == null) return summary;
130+
return new ProjectSummary(
131+
summary.id(), summary.slug(), summary.title(), summary.description(), summary.authorId(),
132+
summary.author(), summary.imageUrl(), bannerUrl, summary.classification(), summary.downloadCount(),
133+
summary.favoriteCount(), summary.updatedAt(), summary.versions(), summary.source(),
134+
summary.websiteUrl(), summary.distributionAllowed());
135+
} catch (RuntimeException ignored) {
136+
return summary;
137+
}
138+
}
139+
119140
DownloadUrlResponse download(long projectId, long fileId) {
120141
JsonNode file = get("/api/v1/hytale/mods/" + positive(projectId) + "/files/" + positive(fileId));
121142
if (file.path("mod_id").asLong() != projectId || file.path("game_id").asLong() != HYTALE_GAME_ID

‎launcher/src/main/java/net/modtale/launcher/ui/browse/ProjectBrowseController.java‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ public final class ProjectBrowseController {
6969

7070
private static final double RESULTS_INDICATOR_WIDTH = 126;
7171
private static final double RESULTS_INDICATOR_SHOW_BUFFER = 10;
72+
private static final int CURSEFORGE_BANNER_BATCH_SIZE = 4;
7273
private final ModtaleApiClient apiClient;
7374
private final Executor executor;
7475
private final Runnable applySettings;
@@ -126,6 +127,7 @@ public final class ProjectBrowseController {
126127
private int currentPage;
127128
private int totalPageCount;
128129
private long totalResultCount;
130+
private long bannerEnrichmentGeneration;
129131
private BrowseOptions.BrowseViewOption activeBrowseView = BrowseOptions.BrowseViewOption.defaultOption();
130132

131133
public ProjectBrowseController(
@@ -812,13 +814,56 @@ private void finishSearch(ProjectPage page, ProjectSearchQuery query) {
812814
updateResultsIndicator(totalResultCount, false);
813815
updatePaginationControls();
814816
renderProjects();
817+
if (sourceSelector.source() == ProjectBrowseSource.CURSEFORGE) {
818+
loadCurseForgeBanners(++bannerEnrichmentGeneration, page.content());
819+
} else {
820+
bannerEnrichmentGeneration++;
821+
}
815822
log.accept("Found " + page.content().size() + " projects.");
816823
ProjectSearchQuery nextQuery = searchQuery();
817824
if (!nextQuery.equals(query) && searchState.shouldSearchForLayout(nextQuery, currentProjects)) {
818825
requestProjects();
819826
}
820827
}
821828

829+
private void loadCurseForgeBanners(long generation, List<ProjectSummary> projects) {
830+
List<ProjectSummary> missing = projects.stream()
831+
.filter(ProjectSummary::isCurseForge)
832+
.filter(project -> project.bannerUrl() == null || project.bannerUrl().isBlank())
833+
.toList();
834+
loadCurseForgeBannerBatch(generation, missing, 0);
835+
}
836+
837+
private void loadCurseForgeBannerBatch(long generation, List<ProjectSummary> projects, int start) {
838+
if (start >= projects.size() || generation != bannerEnrichmentGeneration) return;
839+
int end = Math.min(start + CURSEFORGE_BANNER_BATCH_SIZE, projects.size());
840+
List<CompletableFuture<ProjectSummary>> requests = projects.subList(start, end).stream()
841+
.map(project -> CompletableFuture.supplyAsync(
842+
() -> apiClient.enrichCurseForgeBrowseBanner(project), executor))
843+
.toList();
844+
CompletableFuture.allOf(requests.toArray(CompletableFuture[]::new))
845+
.whenComplete((ignored, error) -> Platform.runLater(() -> {
846+
if (error != null || generation != bannerEnrichmentGeneration
847+
|| sourceSelector.source() != ProjectBrowseSource.CURSEFORGE) {
848+
return;
849+
}
850+
Map<String, ProjectSummary> enriched = new LinkedHashMap<>();
851+
for (CompletableFuture<ProjectSummary> request : requests) {
852+
ProjectSummary project = request.getNow(null);
853+
if (project != null && project.bannerUrl() != null && !project.bannerUrl().isBlank()) {
854+
enriched.put(project.id(), project);
855+
}
856+
}
857+
if (!enriched.isEmpty()) {
858+
currentProjects = currentProjects.stream()
859+
.map(project -> enriched.getOrDefault(project.id(), project))
860+
.toList();
861+
renderProjects();
862+
}
863+
loadCurseForgeBannerBatch(generation, projects, end);
864+
}));
865+
}
866+
822867
private void renderProjectsForLayoutChange() {
823868
if (currentProjects.isEmpty()) {
824869
return;

‎launcher/src/test/java/net/modtale/launcher/api/NyoCfClientTest.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,13 @@ void mapsBrowseDetailVersionsAndExactVerifiedDownload() {
6969
client.search(new ProjectSearchQuery("compost", "mods", "Early Access", "downloads",
7070
0, 20, null, null, null, null, null, null));
7171
assertEquals(0, detailRequests.get());
72+
var enrichedBrowseProject = client.enrichBrowseBanner(browse.content().getFirst());
7273
ProjectDetail detail = client.project(1450386);
7374
DownloadUrlResponse download = client.download(1450386, 8747324);
7475

7576
assertEquals("curseforge:1450386", browse.content().getFirst().routeKey());
7677
assertEquals(null, browse.content().getFirst().bannerUrl());
78+
assertEquals("https://media.forgecdn.net/screenshot-thumb.png", enrichedBrowseProject.bannerUrl());
7779
assertEquals(2, browseRequests.get());
7880
assertTrue(browse.content().getFirst().isCurseForge());
7981
assertEquals("<p>Rich project description</p>", detail.about());

0 commit comments

Comments
 (0)