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 private val userQueueService: UpdateUserQueueService
) { ) {
data class UserDetailsResponse( data class UserDetailsResponse(
val user_details: UserDetails, val user_details: UserDetails,
val queue_details: UserQueueDetails, val queue_details: UserQueueDetails,

View File

@ -218,10 +218,10 @@ class ImportScores(
// Update the frontend // Update the frontend
messagingTemplate.convertAndSend( messagingTemplate.convertAndSend(
"/topic/live-user/${userId}", "/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 current += 1
} }
@ -518,7 +518,7 @@ class ImportScores(
dslContext.batch(queries).execute() 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 // Check if the score is already in the database
val scoreExists = dslContext.fetchExists(SCORES, SCORES.REPLAY_ID.eq(score.best_id)) val scoreExists = dslContext.fetchExists(SCORES, SCORES.REPLAY_ID.eq(score.best_id))
if (scoreExists) { if (scoreExists) {
@ -627,10 +627,19 @@ class ImportScores(
return return
} }
if(processedReplay.ur != null && processedReplay.ur < 25.0) { if(processedReplay.adjusted_ur != null && processedReplay.adjusted_ur < 25.0) {
this.logger.info("Inserting user into queue for update: ${score.user_id}") if(isUserQueue) {
this.logger.info("UR: ${processedReplay.ur} on their replay with id = ${score.best_id}") messagingTemplate.convertAndSend(
this.updateUserQueueService.insertUser(score.user_id) "/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) { for (judgement in processedReplay.judgements) {

View File

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

View File

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

View File

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

View File

@ -21,6 +21,11 @@ interface UserInfo {
total_scores: number; total_scores: number;
} }
interface UserQueueWebsocketPacket {
message: string;
data?: UserQueueDetails;
}
@Component({ @Component({
selector: 'app-view-user', selector: 'app-view-user',
standalone: true, standalone: true,
@ -38,8 +43,6 @@ interface UserInfo {
}) })
export class ViewUserComponent implements OnInit, OnChanges, OnDestroy { export class ViewUserComponent implements OnInit, OnChanges, OnDestroy {
userUpdateIntervalHours = 4
isLoading = false; isLoading = false;
notFound = false; notFound = false;
userId: string | null = null; userId: string | null = null;
@ -70,29 +73,35 @@ export class ViewUserComponent implements OnInit, OnChanges, OnDestroy {
this.activatedRoute.params.subscribe(params => { this.activatedRoute.params.subscribe(params => {
this.userId = params['userId']; this.userId = params['userId'];
if (this.userId) { if (this.userId) {
this.getUserInfo().pipe( this.loadUser();
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();
}
);
} }
}); });
} }
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 { ngOnDestroy(): void {
this.liveUserSub?.unsubscribe(); this.liveUserSub?.unsubscribe();
} }
@ -136,7 +145,20 @@ export class ViewUserComponent implements OnInit, OnChanges, OnDestroy {
this.liveUserSub = this.rxStompService this.liveUserSub = this.rxStompService
.watch(`/topic/live-user/${this.userInfo?.user_details.user_id}`) .watch(`/topic/live-user/${this.userInfo?.user_details.user_id}`)
.subscribe((message: Message) => { .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;
}
}
}); });
} }