Work on live score reload with user queue update

This commit is contained in:
nise.moe 2024-02-22 15:10:06 +01:00
parent 8d208feb24
commit ee619161d2
6 changed files with 152 additions and 133 deletions

View File

@ -25,8 +25,6 @@ class UserDetailsController(
private val userQueueService: UpdateUserQueueService
) {
data class UserDetailsResponse(
val user_details: UserDetails,
val queue_details: UserQueueDetails,

View File

@ -218,10 +218,10 @@ class ImportScores(
// Update the frontend
messagingTemplate.convertAndSend(
"/topic/live-user/${userId}",
currentQueueDetails
UpdateUserQueueService.UserQueueWebsocketPacket(message = "UPDATE_PROGRESS", data = currentQueueDetails)
)
this.insertAndProcessNewScore(topScore.beatmap.id, topScore)
this.insertAndProcessNewScore(topScore.beatmap.id, topScore, isUserQueue = true)
}
current += 1
}
@ -518,7 +518,7 @@ class ImportScores(
dslContext.batch(queries).execute()
}
private fun insertAndProcessNewScore(beatmapId: Int, score: OsuApiModels.Score) {
private fun insertAndProcessNewScore(beatmapId: Int, score: OsuApiModels.Score, isUserQueue: Boolean = false) {
// Check if the score is already in the database
val scoreExists = dslContext.fetchExists(SCORES, SCORES.REPLAY_ID.eq(score.best_id))
if (scoreExists) {
@ -627,10 +627,19 @@ class ImportScores(
return
}
if(processedReplay.ur != null && processedReplay.ur < 25.0) {
this.logger.info("Inserting user into queue for update: ${score.user_id}")
this.logger.info("UR: ${processedReplay.ur} on their replay with id = ${score.best_id}")
this.updateUserQueueService.insertUser(score.user_id)
if(processedReplay.adjusted_ur != null && processedReplay.adjusted_ur < 25.0) {
if(isUserQueue) {
messagingTemplate.convertAndSend(
"/topic/live-user/${score.user_id}",
UpdateUserQueueService.UserQueueWebsocketPacket(message = "UPDATE_SCORES")
)
}
if(!isUserQueue) {
this.logger.info("Inserting user into queue for update: ${score.user_id}")
this.logger.info("UR: ${processedReplay.ur} on their replay with id = ${score.best_id}")
this.updateUserQueueService.insertUser(score.user_id)
}
}
for (judgement in processedReplay.judgements) {

View File

@ -19,6 +19,11 @@ class UpdateUserQueueService(
private val USER_UPDATE_INTERVAL_HOURS = 4
data class UserQueueWebsocketPacket(
val message: String,
val data: UserQueueDetails? = null
)
/**
* Retrieves the user queue details for the given user ID.
*
@ -38,28 +43,10 @@ class UpdateUserQueueService(
.limit(1)
.fetchOneInto(OffsetDateTime::class.java)
val lastCompletedUpdateUser = dslContext.select(USERS.SYS_LAST_UPDATE)
.from(USERS)
.where(USERS.USER_ID.eq(userId))
.fetchOneInto(OffsetDateTime::class.java)
// Select the most recent
val lastCompletedUpdate = lastCompletedUpdateQueue?.let {
if (lastCompletedUpdateUser != null) {
if (lastCompletedUpdateUser.isAfter(lastCompletedUpdateQueue)) {
lastCompletedUpdateUser
} else {
lastCompletedUpdateQueue
}
} else {
lastCompletedUpdateQueue
}
} ?: lastCompletedUpdateUser
var canUpdate = !isProcessing
if(lastCompletedUpdate != null) {
if(lastCompletedUpdateQueue != null) {
val now = OffsetDateTime.now(ZoneOffset.UTC)
val hoursSinceLastUpdate = now.hour - lastCompletedUpdate.hour
val hoursSinceLastUpdate = now.hour - lastCompletedUpdateQueue.hour
if(hoursSinceLastUpdate < USER_UPDATE_INTERVAL_HOURS)
canUpdate = false
@ -78,7 +65,7 @@ class UpdateUserQueueService(
return UserQueueDetails(
isProcessing,
lastCompletedUpdate,
lastCompletedUpdateQueue,
canUpdate,
currentProgress?.progressCurrent,
currentProgress?.progressTotal
@ -123,7 +110,7 @@ class UpdateUserQueueService(
// Notify the user that their queue has been processed with fresh info
messagingTemplate.convertAndSend(
"/topic/live-user/${userId}",
this.getUserQueueDetails(userId)
UpdateUserQueueService.UserQueueWebsocketPacket(message = "UPDATE_PROGRESS", data = this.getUserQueueDetails(userId))
)
}

View File

@ -26,5 +26,5 @@
</div>
<router-outlet></router-outlet>
<div class="text-center version">
v20240218
v20240222
</div>

View File

@ -47,14 +47,14 @@
</a>
</ng-container>
<ng-container *ngIf="!this.userInfo.queue_details.canUpdate">
<span class="btn-info">can't force update now</span>
<span class="btn-warning">wait a bit to force update</span>
</ng-container>
<span style="margin-left: 4px">|</span>
last update: {{ this.userInfo.queue_details.lastCompletedUpdate ? this.calculateTimeAgo(this.userInfo.queue_details.lastCompletedUpdate) : 'never'}}
</ng-container>
<ng-template #updateProgress>
<div class="progress">
<span class="btn-warning">updating now!</span> <span style="margin-left: 4px">|</span> progress: {{ this.userInfo.queue_details.progressCurrent ? this.userInfo.queue_details.progressCurrent : "?" }}/{{ this.userInfo.queue_details.progressTotal ? this.userInfo.queue_details.progressTotal : "?" }}
<span class="btn-info">updating now!</span> <span style="margin-left: 4px">|</span> progress: {{ this.userInfo.queue_details.progressCurrent != null ? this.userInfo.queue_details.progressCurrent : "?" }}/{{ this.userInfo.queue_details.progressTotal != null ? this.userInfo.queue_details.progressTotal : "?" }}
<ng-container *ngIf="!this.userInfo.queue_details.progressTotal && !this.userInfo.queue_details.progressCurrent; else loading">
<span style="font-weight: bold">(in queue)</span>
</ng-container>
@ -64,29 +64,74 @@
</div>
</ng-template>
<h4>Suspicious Scores ({{ this.userInfo.suspicious_scores.length }})</h4>
<div class="table">
<ng-container *ngIf="this.userInfo.suspicious_scores.length > 0">
<h4>Suspicious Scores ({{ this.userInfo.suspicious_scores.length }})</h4>
<div class="table">
<table class="table">
<thead>
<tr>
<th>Beatmap</th>
<th>Date</th>
<th>
cvUR
</th>
<th>
PP
</th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let score of this.userInfo.suspicious_scores">
<td>
<div class="image-container">
<a href="https://osu.ppy.sh/beatmaps/{{ score.beatmap_id }}?mode=osu" target="_blank">
<img ngSrc="https://assets.ppy.sh/beatmaps/{{ score.beatmap_beatmapset_id }}/covers/cover.jpg"
alt="Beatmap Cover" loading="lazy" width="260" height="72">
<div class="overlay">
{{ score.beatmap_title }}
{{ score.beatmap_star_rating | number: '1.0-1' }}★
</div>
</a>
</div>
</td>
<td>{{ score.date }}</td>
<td class="text-center">{{ score.ur | number: '1.2-2' }}</td>
<td class="text-center">{{ score.pp | number: '1.0-1' }}</td>
<td>
<a [routerLink]="['/s/' + score.replay_id]" class="btn btn-outline-secondary btn-sm mb-2">
Details
</a>
<a [href]="'https://osu.ppy.sh/scores/osu/' + score.replay_id" class="btn btn-outline-secondary btn-sm" style="margin-left: 5px"
target="_blank">
osu!web
</a>
</td>
</tr>
</tbody>
</table>
</div>
</ng-container>
<ng-container *ngIf="this.userInfo.similar_replays.length > 0">
<h4 class="mt-2">Similar Replays ({{ this.userInfo.similar_replays.length }})</h4>
<table class="table">
<thead>
<tr>
<th>Beatmap</th>
<th>Date</th>
<th>
cvUR
</th>
<th>
PP
</th>
<th>Links</th>
<th style="text-align: start">Replay 1 Details</th>
<th style="text-align: start">Replay 2 Details</th>
<th>Similarity</th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let score of this.userInfo.suspicious_scores">
<tr *ngFor="let score of this.userInfo.similar_replays">
<td>
<div class="image-container">
<a href="https://osu.ppy.sh/beatmaps/{{ score.beatmap_id }}?mode=osu" target="_blank">
<img ngSrc="https://assets.ppy.sh/beatmaps/{{ score.beatmap_beatmapset_id }}/covers/cover.jpg"
alt="Beatmap Cover" loading="lazy" width="260" height="72">
<img ngSrc="https://assets.ppy.sh/beatmaps/{{ score.beatmap_beatmapset_id }}/covers/cover.jpg" width="260" height="72"
alt="Beatmap Cover" loading="lazy">
<div class="overlay">
{{ score.beatmap_title }}
{{ score.beatmap_star_rating | number: '1.0-1' }}★
@ -94,76 +139,34 @@
</a>
</div>
</td>
<td>{{ score.date }}</td>
<td class="text-center">{{ score.ur | number: '1.2-2' }}</td>
<td class="text-center">{{ score.pp | number: '1.0-1' }}</td>
<td>
<a [routerLink]="['/s/' + score.replay_id]" class="btn btn-outline-secondary btn-sm mb-2">
Details
<td class="replay-n-details">
<a href="https://osu.ppy.sh/scores/osu/{{ score.replay_id_1 }}" target="_blank">
{{ score.replay_date_1 }}
<br>
User: {{ score.username_1 }}
<br>
PP: {{ score.replay_pp_1 | number: '1.0-0' }}
</a>
<a [href]="'https://osu.ppy.sh/scores/osu/' + score.replay_id" class="btn btn-outline-secondary btn-sm"
target="_blank">
osu!web
</td>
<td class="replay-n-details">
<a href="https://osu.ppy.sh/scores/osu/{{ score.replay_id_2 }}" target="_blank">
{{ score.replay_date_2 }}
<br>
User: {{ score.username_2 }}
<br>
PP: {{ score.replay_pp_2 | number: '1.0-0' }}
</a>
</td>
<td class="text-center">{{ score.similarity | number: '1.0-2' }}</td>
<td>
<a [routerLink]="['/p/' + score.replay_id_1 + '/' + score.replay_id_2]" class="btn mr-1">
Details
</a>
</td>
</tr>
</tbody>
</table>
</div>
<h4 class="mt-2">Similar Replays ({{ this.userInfo.similar_replays.length }})</h4>
<table class="table">
<thead>
<tr>
<th>Beatmap</th>
<th style="text-align: start">Replay 1 Details</th>
<th style="text-align: start">Replay 2 Details</th>
<th>Similarity</th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let score of this.userInfo.similar_replays">
<td>
<div class="image-container">
<a href="https://osu.ppy.sh/beatmaps/{{ score.beatmap_id }}?mode=osu" target="_blank">
<img ngSrc="https://assets.ppy.sh/beatmaps/{{ score.beatmap_beatmapset_id }}/covers/cover.jpg" width="260" height="72"
alt="Beatmap Cover" loading="lazy">
<div class="overlay">
{{ score.beatmap_title }}
{{ score.beatmap_star_rating | number: '1.0-1' }}★
</div>
</a>
</div>
</td>
<td class="replay-n-details">
<a href="https://osu.ppy.sh/scores/osu/{{ score.replay_id_1 }}" target="_blank">
{{ score.replay_date_1 }}
<br>
User: {{ score.username_1 }}
<br>
PP: {{ score.replay_pp_1 | number: '1.0-0' }}
</a>
</td>
<td class="replay-n-details">
<a href="https://osu.ppy.sh/scores/osu/{{ score.replay_id_2 }}" target="_blank">
{{ score.replay_date_2 }}
<br>
User: {{ score.username_2 }}
<br>
PP: {{ score.replay_pp_2 | number: '1.0-0' }}
</a>
</td>
<td class="text-center">{{ score.similarity | number: '1.0-2' }}</td>
<td>
<a [routerLink]="['/p/' + score.replay_id_1 + '/' + score.replay_id_2]" class="btn mr-1">
Details
</a>
</td>
</tr>
</tbody>
</table>
</ng-container>
</div>
</ng-container>
</div>

View File

@ -21,6 +21,11 @@ interface UserInfo {
total_scores: number;
}
interface UserQueueWebsocketPacket {
message: string;
data?: UserQueueDetails;
}
@Component({
selector: 'app-view-user',
standalone: true,
@ -38,8 +43,6 @@ interface UserInfo {
})
export class ViewUserComponent implements OnInit, OnChanges, OnDestroy {
userUpdateIntervalHours = 4
isLoading = false;
notFound = false;
userId: string | null = null;
@ -70,29 +73,35 @@ export class ViewUserComponent implements OnInit, OnChanges, OnDestroy {
this.activatedRoute.params.subscribe(params => {
this.userId = params['userId'];
if (this.userId) {
this.getUserInfo().pipe(
catchError(error => {
this.userInfo = null;
if(error.status == 404) {
this.notFound = true;
}
return EMPTY;
}),
finalize(() => {
this.isLoading = false;
})
).subscribe(
(response: UserInfo) => {
this.notFound = false;
this.userInfo = response;
this.title.setTitle(`${this.userInfo.user_details.username}`);
this.subscribeToUser();
}
);
this.loadUser();
}
});
}
private loadUser(isScoreUpdate = false) {
this.getUserInfo().pipe(
catchError(error => {
this.userInfo = null;
if (error.status == 404) {
this.notFound = true;
}
return EMPTY;
}),
finalize(() => {
this.isLoading = false;
})
).subscribe(
(response: UserInfo) => {
this.notFound = false;
this.userInfo = response;
if(!isScoreUpdate) {
this.title.setTitle(`${this.userInfo.user_details.username}`);
this.subscribeToUser();
}
}
);
}
ngOnDestroy(): void {
this.liveUserSub?.unsubscribe();
}
@ -136,7 +145,20 @@ export class ViewUserComponent implements OnInit, OnChanges, OnDestroy {
this.liveUserSub = this.rxStompService
.watch(`/topic/live-user/${this.userInfo?.user_details.user_id}`)
.subscribe((message: Message) => {
this.userInfo!.queue_details = JSON.parse(message.body);
let queueDetails: UserQueueWebsocketPacket = JSON.parse(message.body);
if(queueDetails.message == "UPDATE_SCORES") {
this.loadUser(true);
} else {
if(queueDetails.data != null) {
if(queueDetails.data.progressCurrent != null && queueDetails.data.progressTotal != null) {
if (queueDetails.data.progressCurrent >= queueDetails.data.progressTotal) {
this.loadUser(true);
this.liveUserSub?.unsubscribe();
}
}
this.userInfo!.queue_details = queueDetails.data;
}
}
});
}