Compare commits

..

No commits in common. "ef1031fcf334da4d99d4d45d575f4f10df96aaca" and "792b2032554fcb8119f56f2f8b8c155f3daf9492" have entirely different histories.

57 changed files with 76 additions and 647 deletions

View File

@ -26,12 +26,8 @@ val IDX_REPLAY_IDS_PAIRS: Index = Internal.createIndex(DSL.name("idx_replay_ids_
val IDX_SCORES_BEATMAP_ID: Index = Internal.createIndex(DSL.name("idx_scores_beatmap_id"), Scores.SCORES, arrayOf(Scores.SCORES.BEATMAP_ID), false)
val IDX_SCORES_BEATMAP_ID_REPLAY_ID: Index = Internal.createIndex(DSL.name("idx_scores_beatmap_id_replay_id"), Scores.SCORES, arrayOf(Scores.SCORES.BEATMAP_ID, Scores.SCORES.REPLAY_ID), false)
val IDX_SCORES_BEATMAP_ID_REPLAY_ID_UR: Index = Internal.createIndex(DSL.name("idx_scores_beatmap_id_replay_id_ur"), Scores.SCORES, arrayOf(Scores.SCORES.BEATMAP_ID, Scores.SCORES.REPLAY_ID, Scores.SCORES.UR), false)
val IDX_SCORES_IS_BANNED: Index = Internal.createIndex(DSL.name("idx_scores_is_banned"), Scores.SCORES, arrayOf(Scores.SCORES.IS_BANNED), false)
val IDX_SCORES_JUDGEMENTS_SCORE_ID: Index = Internal.createIndex(DSL.name("idx_scores_judgements_score_id"), ScoresJudgements.SCORES_JUDGEMENTS, arrayOf(ScoresJudgements.SCORES_JUDGEMENTS.SCORE_ID), false)
val IDX_SCORES_KEYPRESSES_STANDARD_DEVIATION_ADJUSTED: Index = Internal.createIndex(DSL.name("idx_scores_keypresses_standard_deviation_adjusted"), Scores.SCORES, arrayOf(Scores.SCORES.KEYPRESSES_STANDARD_DEVIATION_ADJUSTED), false)
val IDX_SCORES_PP: Index = Internal.createIndex(DSL.name("idx_scores_pp"), Scores.SCORES, arrayOf(Scores.SCORES.PP), false)
val IDX_SCORES_REPLAY_ID: Index = Internal.createIndex(DSL.name("idx_scores_replay_id"), Scores.SCORES, arrayOf(Scores.SCORES.REPLAY_ID), false)
val IDX_SCORES_SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED: Index = Internal.createIndex(DSL.name("idx_scores_sliderend_release_standard_deviation_adjusted"), Scores.SCORES, arrayOf(Scores.SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED), false)
val IDX_SCORES_UR: Index = Internal.createIndex(DSL.name("idx_scores_ur"), Scores.SCORES, arrayOf(Scores.SCORES.UR), false)
val IDX_SCORES_USER_ID: Index = Internal.createIndex(DSL.name("idx_scores_user_id"), Scores.SCORES, arrayOf(Scores.SCORES.USER_ID), false)
val IDX_USERS_IS_BANNED_FALSE: Index = Internal.createIndex(DSL.name("idx_users_is_banned_false"), Users.USERS, arrayOf(Users.USERS.IS_BANNED), false)

View File

@ -8,11 +8,7 @@ import com.nisemoe.generated.Public
import com.nisemoe.generated.indexes.IDX_SCORES_BEATMAP_ID
import com.nisemoe.generated.indexes.IDX_SCORES_BEATMAP_ID_REPLAY_ID
import com.nisemoe.generated.indexes.IDX_SCORES_BEATMAP_ID_REPLAY_ID_UR
import com.nisemoe.generated.indexes.IDX_SCORES_IS_BANNED
import com.nisemoe.generated.indexes.IDX_SCORES_KEYPRESSES_STANDARD_DEVIATION_ADJUSTED
import com.nisemoe.generated.indexes.IDX_SCORES_PP
import com.nisemoe.generated.indexes.IDX_SCORES_REPLAY_ID
import com.nisemoe.generated.indexes.IDX_SCORES_SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED
import com.nisemoe.generated.indexes.IDX_SCORES_UR
import com.nisemoe.generated.indexes.IDX_SCORES_USER_ID
import com.nisemoe.generated.keys.REPLAY_ID_UNIQUE
@ -321,11 +317,6 @@ open class Scores(
*/
val JUDGEMENTS: TableField<ScoresRecord, ByteArray?> = createField(DSL.name("judgements"), SQLDataType.BLOB, this, "")
/**
* The column <code>public.scores.leaderboard_rank</code>.
*/
val LEADERBOARD_RANK: TableField<ScoresRecord, Long?> = createField(DSL.name("leaderboard_rank"), SQLDataType.BIGINT, this, "")
private constructor(alias: Name, aliased: Table<ScoresRecord>?): this(alias, null, null, null, aliased, null, null)
private constructor(alias: Name, aliased: Table<ScoresRecord>?, parameters: Array<Field<*>?>?): this(alias, null, null, null, aliased, parameters, null)
private constructor(alias: Name, aliased: Table<ScoresRecord>?, where: Condition?): this(alias, null, null, null, aliased, null, where)
@ -358,7 +349,7 @@ open class Scores(
override fun `as`(alias: Table<*>): ScoresPath = ScoresPath(alias.qualifiedName, this)
}
override fun getSchema(): Schema? = if (aliased()) null else Public.PUBLIC
override fun getIndexes(): List<Index> = listOf(IDX_SCORES_BEATMAP_ID, IDX_SCORES_BEATMAP_ID_REPLAY_ID, IDX_SCORES_BEATMAP_ID_REPLAY_ID_UR, IDX_SCORES_IS_BANNED, IDX_SCORES_KEYPRESSES_STANDARD_DEVIATION_ADJUSTED, IDX_SCORES_PP, IDX_SCORES_REPLAY_ID, IDX_SCORES_SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED, IDX_SCORES_UR, IDX_SCORES_USER_ID)
override fun getIndexes(): List<Index> = listOf(IDX_SCORES_BEATMAP_ID, IDX_SCORES_BEATMAP_ID_REPLAY_ID, IDX_SCORES_BEATMAP_ID_REPLAY_ID_UR, IDX_SCORES_REPLAY_ID, IDX_SCORES_UR, IDX_SCORES_USER_ID)
override fun getPrimaryKey(): UniqueKey<ScoresRecord> = SCORES_PKEY
override fun getUniqueKeys(): List<UniqueKey<ScoresRecord>> = listOf(REPLAY_ID_UNIQUE)

View File

@ -205,10 +205,6 @@ open class ScoresRecord private constructor() : UpdatableRecordImpl<ScoresRecord
set(value): Unit = set(45, value)
get(): ByteArray? = get(45) as ByteArray?
open var leaderboardRank: Long?
set(value): Unit = set(46, value)
get(): Long? = get(46) as Long?
// -------------------------------------------------------------------------
// Primary key information
// -------------------------------------------------------------------------
@ -218,7 +214,7 @@ open class ScoresRecord private constructor() : UpdatableRecordImpl<ScoresRecord
/**
* Create a detached, initialised ScoresRecord
*/
constructor(id: Int? = null, beatmapId: Int? = null, count_100: Int? = null, count_300: Int? = null, count_50: Int? = null, countMiss: Int? = null, date: LocalDateTime? = null, maxCombo: Int? = null, mods: Int? = null, perfect: Boolean? = null, pp: Double? = null, rank: String? = null, replayAvailable: Boolean? = null, replayId: Long? = null, score: Long? = null, userId: Long? = null, replay: ByteArray? = null, ur: Double? = null, frametime: Double? = null, edgeHits: Int? = null, snaps: Int? = null, isBanned: Boolean? = null, adjustedUr: Double? = null, meanError: Double? = null, errorVariance: Double? = null, errorStandardDeviation: Double? = null, minimumError: Double? = null, maximumError: Double? = null, errorRange: Double? = null, errorCoefficientOfVariation: Double? = null, errorKurtosis: Double? = null, errorSkewness: Double? = null, sentDiscordNotification: Boolean? = null, addedAt: OffsetDateTime? = null, version: Int? = null, keypressesTimes: Array<Double?>? = null, keypressesMedian: Double? = null, keypressesStandardDeviation: Double? = null, sliderendReleaseTimes: Array<Double?>? = null, sliderendReleaseMedian: Double? = null, sliderendReleaseStandardDeviation: Double? = null, keypressesMedianAdjusted: Double? = null, keypressesStandardDeviationAdjusted: Double? = null, sliderendReleaseMedianAdjusted: Double? = null, sliderendReleaseStandardDeviationAdjusted: Double? = null, judgements: ByteArray? = null, leaderboardRank: Long? = null): this() {
constructor(id: Int? = null, beatmapId: Int? = null, count_100: Int? = null, count_300: Int? = null, count_50: Int? = null, countMiss: Int? = null, date: LocalDateTime? = null, maxCombo: Int? = null, mods: Int? = null, perfect: Boolean? = null, pp: Double? = null, rank: String? = null, replayAvailable: Boolean? = null, replayId: Long? = null, score: Long? = null, userId: Long? = null, replay: ByteArray? = null, ur: Double? = null, frametime: Double? = null, edgeHits: Int? = null, snaps: Int? = null, isBanned: Boolean? = null, adjustedUr: Double? = null, meanError: Double? = null, errorVariance: Double? = null, errorStandardDeviation: Double? = null, minimumError: Double? = null, maximumError: Double? = null, errorRange: Double? = null, errorCoefficientOfVariation: Double? = null, errorKurtosis: Double? = null, errorSkewness: Double? = null, sentDiscordNotification: Boolean? = null, addedAt: OffsetDateTime? = null, version: Int? = null, keypressesTimes: Array<Double?>? = null, keypressesMedian: Double? = null, keypressesStandardDeviation: Double? = null, sliderendReleaseTimes: Array<Double?>? = null, sliderendReleaseMedian: Double? = null, sliderendReleaseStandardDeviation: Double? = null, keypressesMedianAdjusted: Double? = null, keypressesStandardDeviationAdjusted: Double? = null, sliderendReleaseMedianAdjusted: Double? = null, sliderendReleaseStandardDeviationAdjusted: Double? = null, judgements: ByteArray? = null): this() {
this.id = id
this.beatmapId = beatmapId
this.count_100 = count_100
@ -265,7 +261,6 @@ open class ScoresRecord private constructor() : UpdatableRecordImpl<ScoresRecord
this.sliderendReleaseMedianAdjusted = sliderendReleaseMedianAdjusted
this.sliderendReleaseStandardDeviationAdjusted = sliderendReleaseStandardDeviationAdjusted
this.judgements = judgements
this.leaderboardRank = leaderboardRank
resetChangedOnNotNull()
}
}

View File

@ -56,8 +56,7 @@ data class SuspiciousScoreEntry(
val beatmap_star_rating: Double,
val pp: Double,
val frametime: Double,
val ur: Double,
val leaderboard_rank: Long?,
val ur: Double
)
data class SimilarReplayEntry(
@ -65,8 +64,6 @@ data class SimilarReplayEntry(
val replay_id_2: Long,
val user_id_1: Long,
val user_id_2: Long,
val user_banned_1: Boolean,
val user_banned_2: Boolean,
val username_1: String,
val username_2: String,
val beatmap_beatmapset_id: Long,
@ -74,8 +71,6 @@ data class SimilarReplayEntry(
val replay_date_2: String,
val replay_pp_1: Double,
val replay_pp_2: Double,
val replay_leaderboard_rank_1: Long?,
val replay_leaderboard_rank_2: Long?,
val beatmap_id: Long,
val beatmap_title: String,
val beatmap_star_rating: Double,
@ -180,7 +175,6 @@ data class ReplayData(
val pp: Double?,
val perfect: Boolean,
val max_combo: Int,
val leaderboard_rank: Long?,
val count_300: Int,
val count_100: Int,
@ -204,31 +198,6 @@ data class ReplayData(
}
// Contains everything needed to reconstruct a replay (.osr) file.
// We currently do not store some of these - these have been marked.
data class EncodedReplayData(
val gameMode: Byte,
val gameVersion: Int,
val beatmapHash: String,
val username: String,
val replayHash: String, // We do not store - maybe we can reconstruct?
val count300: Short,
val count100: Short,
val count50: Short,
val countGeki: Short, // We do not store - probably should
val countKatu: Short, // We do not store - probably should
val countMisses: Short,
val totalScore: Int,
val greatestCombo: Short,
val perfect: Byte,
val mods: Int,
val lifeBar: String, // We do not store, and maybe shouldn't?
val timeStamp: Long,
val replayLength: ByteArray, // We do not store - could be calculated?
val scoreId: Long,
val additionalInformation: Double, // We do not store - probably not needed
)
data class DistributionEntry(
val countMiss: Double,
val count300: Double,

View File

@ -76,7 +76,7 @@ class BanlistController(
.where(USERS.IS_BANNED.eq(true))
.orderBy(USERS.APPROX_BAN_DATE.desc())
.limit(MAX_BANLIST_ENTRIES_PER_PAGE)
.offset((request.page - 1) * MAX_BANLIST_ENTRIES_PER_PAGE)
.offset((request.page - 1) * 10)
.fetch()
.map {
BanlistEntry(

View File

@ -8,15 +8,15 @@ data class HealthResponse(
val healthy: Boolean,
)
val healthResponse = HealthResponse(
healthy = true,
)
@RestController
class HealthController {
companion object {
val HEALTH = HealthResponse(
healthy = true,
)
}
@GetMapping("/health")
fun healthCheck(): ResponseEntity<HealthResponse> = ResponseEntity.ok(HEALTH)
fun healthCheck(): ResponseEntity<HealthResponse> {
return ResponseEntity.ok(healthResponse)
}
}

View File

@ -8,14 +8,12 @@ data class VersionResponse(
val version: String,
)
val versionResponse = VersionResponse(
version = "v20250213",
)
@RestController
class VersionController {
companion object {
val VERSION = VersionResponse(
version = "v20250702",
)
}
@GetMapping("/version")
fun getVersion(): ResponseEntity<VersionResponse> = ResponseEntity.ok(VERSION)
fun getVersion(): ResponseEntity<VersionResponse> = ResponseEntity.ok(versionResponse)
}

View File

@ -10,7 +10,6 @@ import com.nisemoe.nise.osu.OsuApi
import com.nisemoe.nise.service.AuthService
import com.nisemoe.nise.service.CompressReplay
import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream
import org.jetbrains.kotlinx.dataframe.impl.asList
import org.jooq.Condition
import org.jooq.DSLContext
import org.jooq.Record
@ -40,32 +39,6 @@ class ScoreService(
val osuUserAlias1 = USERS.`as`("osu_user_alias1")
val osuUserAlias2 = USERS.`as`("osu_user_alias2")
val SKIPPED_SIMILARITY_MAPS = arrayOf(
2635266, // - Death is just the beginning cs10
2528044, // - Genkaku Catastrophe cs10
2528067, // - Genkaku Catastrophe cs8
2665747, // - expand cs9
3063865, // - Kagayaku Hari no Kobitozoku ~ Little Princess 700 note stream
4641077, // - granat cs10
3616081, // - shop cs10
3533781, //- uwa cs10
3535358, // - uwa cs8
3208341, // - mizuumi cs8
4871320, // - youre a winner ninerik incident
3237399, // - dialtone cs10
4383507, // - another diff from the 700 note stream set
1606883, // - tabi no tochu cs7
4991602, // new bad map
4997662, // new bad map
1839017,
4467121,
4783487,
3772967,
3571032,
4443518,
5125702,
)
}
fun getCharts(db: Record): List<ReplayDataChart> {
@ -192,8 +165,7 @@ class ScoreService(
SCORES.ERROR_KURTOSIS,
SCORES.ERROR_SKEWNESS,
SCORES.SLIDEREND_RELEASE_TIMES,
SCORES.KEYPRESSES_TIMES,
SCORES.LEADERBOARD_RANK,
SCORES.KEYPRESSES_TIMES
)
.from(SCORES)
.join(USERS).on(SCORES.USER_ID.eq(USERS.USER_ID))
@ -256,8 +228,7 @@ class ScoreService(
error_kurtosis = result.get(SCORES.ERROR_KURTOSIS, Double::class.java),
error_skewness = result.get(SCORES.ERROR_SKEWNESS, Double::class.java),
charts = charts,
similar_scores = this.getSimilarScores(replayId),
leaderboard_rank = result.get(SCORES.LEADERBOARD_RANK, Long::class.java)
similar_scores = this.getSimilarScores(replayId)
)
this.loadComparableReplayData(replayData)
return replayData
@ -280,8 +251,7 @@ class ScoreService(
BEATMAPS.STAR_RATING,
SCORES.PP,
SCORES.FRAMETIME,
SCORES.UR,
SCORES.LEADERBOARD_RANK,
SCORES.UR
)
.from(SCORES)
.join(USERS).on(SCORES.USER_ID.eq(USERS.USER_ID))
@ -303,8 +273,7 @@ class ScoreService(
beatmap_star_rating = it.get(BEATMAPS.STAR_RATING, Double::class.java),
pp = it.get(SCORES.PP, Double::class.java),
frametime = it.get(SCORES.FRAMETIME, Double::class.java),
ur = it.get(SCORES.UR, Double::class.java),
leaderboard_rank = it.get(SCORES.LEADERBOARD_RANK, Long::class.java),
ur = it.get(SCORES.UR, Double::class.java)
)
}
@ -349,17 +318,13 @@ class ScoreService(
osuScoreAlias1.REPLAY_ID,
osuScoreAlias1.USER_ID,
osuUserAlias1.USERNAME,
osuUserAlias1.IS_BANNED,
osuScoreAlias1.DATE,
osuScoreAlias1.PP,
osuScoreAlias1.LEADERBOARD_RANK,
osuScoreAlias2.REPLAY_ID,
osuScoreAlias2.USER_ID,
osuUserAlias2.USERNAME,
osuUserAlias2.IS_BANNED,
osuScoreAlias2.DATE,
osuScoreAlias2.PP,
osuScoreAlias2.LEADERBOARD_RANK,
BEATMAPS.BEATMAP_ID,
BEATMAPS.TITLE,
BEATMAPS.STAR_RATING,
@ -379,10 +344,6 @@ class ScoreService(
and(osuScoreAlias2.IS_BANNED.eq(false))
}
}
// Globally skip maps that are known to have false positives (eg. high CS)
.and(SCORES_SIMILARITY.BEATMAP_ID.notIn(*SKIPPED_SIMILARITY_MAPS))
// Skip maps that have high CS values (smaller circles mean the replays will be naturally similar)
.and(BEATMAPS.CS.lt(8.0))
.and(condition)
.orderBy(osuScoreAlias2.DATE.desc(), SCORES_SIMILARITY.SIMILARITY.asc())
.fetch()
@ -402,8 +363,6 @@ class ScoreService(
fun getSimilarReplays(condition: Condition = DSL.noCondition()): List<SimilarReplayEntry> {
val replays = getSimilarReplaysRecords(condition)
return mapSimilarReplays(replays)
// Filter scores where the imports have been out of order and the stolen replay's user has been banned
.filter { !it.user_banned_2 }
}
private fun mapSimilarReplays(replays: List<Record>) = replays.map {
@ -414,9 +373,6 @@ class ScoreService(
var userId1 = it.get(osuScoreAlias1.USER_ID, Long::class.java)
var userId2 = it.get(osuScoreAlias2.USER_ID, Long::class.java)
var userBanned1 = it.get(osuUserAlias1.IS_BANNED, Boolean::class.java)
var userBanned2 = it.get(osuUserAlias2.IS_BANNED, Boolean::class.java)
var username1 = it.get(osuUserAlias1.USERNAME, String::class.java)
var username2 = it.get(osuUserAlias2.USERNAME, String::class.java)
@ -426,9 +382,6 @@ class ScoreService(
var replayPp1 = it.get(osuScoreAlias1.PP, Double::class.java)
var replayPp2 = it.get(osuScoreAlias2.PP, Double::class.java)
var replayLeaderboardRank1 = it.get(osuScoreAlias1.LEADERBOARD_RANK, Long::class.java)
var replayLeaderboardRank2 = it.get(osuScoreAlias2.LEADERBOARD_RANK, Long::class.java)
// Swap logic if replayDate1 is after replayDate2
if (replayDate1.isAfter(replayDate2)) {
val tempReplayId = replayId1
@ -439,10 +392,6 @@ class ScoreService(
userId1 = userId2
userId2 = tempUserId
val tempUserBanned = userBanned1
userBanned1 = userBanned2
userBanned2 = tempUserBanned
val tempUsername = username1
username1 = username2
username2 = tempUsername
@ -454,10 +403,6 @@ class ScoreService(
val tempReplayPp = replayPp1
replayPp1 = replayPp2
replayPp2 = tempReplayPp
val tempLeaderboardRank = replayLeaderboardRank1
replayLeaderboardRank1 = replayLeaderboardRank2
replayLeaderboardRank2 = tempLeaderboardRank
}
SimilarReplayEntry(
@ -465,8 +410,6 @@ class ScoreService(
replay_id_2 = replayId2,
user_id_1 = userId1,
user_id_2 = userId2,
user_banned_1 = userBanned1,
user_banned_2 = userBanned2,
username_1 = username1,
username_2 = username2,
beatmap_beatmapset_id = it.get(BEATMAPS.BEATMAPSET_ID, Long::class.java),
@ -474,12 +417,10 @@ class ScoreService(
replay_date_2 = Format.formatLocalDateTime(replayDate2),
replay_pp_1 = replayPp1,
replay_pp_2 = replayPp2,
replay_leaderboard_rank_1 = replayLeaderboardRank1,
replay_leaderboard_rank_2 = replayLeaderboardRank2,
beatmap_id = it.get(BEATMAPS.BEATMAP_ID, Long::class.java),
beatmap_title = it.get(BEATMAPS.TITLE, String::class.java),
beatmap_star_rating = it.get(BEATMAPS.STAR_RATING, Double::class.java),
similarity = it.get(SCORES_SIMILARITY.SIMILARITY, Double::class.java),
similarity = it.get(SCORES_SIMILARITY.SIMILARITY, Double::class.java)
)
}.distinctBy {
val (smallerId, largerId) = listOf(it.replay_id_1, it.replay_id_2).sorted()

View File

@ -65,7 +65,7 @@ class UserScoreService(
USER_SCORES.ERROR_SKEWNESS,
USER_SCORES.SLIDEREND_RELEASE_TIMES,
USER_SCORES.KEYPRESSES_TIMES,
USER_SCORES.JUDGEMENTS,
USER_SCORES.JUDGEMENTS
)
.from(USER_SCORES)
.join(BEATMAPS).on(USER_SCORES.BEATMAP_ID.eq(BEATMAPS.BEATMAP_ID))
@ -127,8 +127,7 @@ class UserScoreService(
date = null,
pp = null,
rank = null,
user_id = null,
leaderboard_rank = null,
user_id = null
)
this.scoreService.loadComparableReplayData(replayData)
return replayData

View File

@ -61,8 +61,7 @@ class CircleguardService {
val sliderend_release_standard_deviation: Double?,
val sliderend_release_standard_deviation_adjusted: Double?,
val judgements: List<Judgement>,
val hit_count: Int?,
val judgements: List<Judgement>
)
fun postProcessReplay(replayResponse: ReplayResponse, mods: Int = 0) {

View File

@ -18,7 +18,7 @@ import java.util.*
@Service
class MetabaseService : InitializingBean {
@Value("\${METABASE_API_KEY:nil}")
@Value("\${METABASE_API_KEY}")
private lateinit var metabaseApiKey: String
@Value("\${METABASE_URL:https://neko.nise.moe}")

View File

@ -9,7 +9,6 @@ import org.slf4j.LoggerFactory
import org.springframework.beans.factory.InitializingBean
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import java.io.IOException
import java.net.URI
import java.net.URLEncoder
import java.net.http.HttpClient
@ -242,22 +241,6 @@ class OsuApi(
}
}
fun getUserBeatmapScore(userId: Long, beatmapId: Int, scoreId: Long? = null): OsuApiModels.UserScore? {
val response = doRequest("https://osu.ppy.sh/api/v2/beatmaps/$beatmapId/scores/users/$userId", emptyMap())
if (response == null) {
this.logger.info("Error getting score on beatmap $beatmapId for user $userId")
return null
}
val score = when (response.statusCode()) {
200 -> serializer.decodeFromString<OsuApiModels.UserScore>(response.body())
else -> null
}
return if (scoreId == null || score?.score?.id == scoreId) score else null
}
fun searchBeatmapsets(cursor: OsuApiModels.BeatmapsetSearchResultCursor?): OsuApiModels.BeatmapsetSearchResult? {
val queryParams = mutableMapOf(
"s" to "ranked", // Status [only ranked]
@ -363,12 +346,7 @@ class OsuApi(
val waitTimes = listOf(15L, 30L, 60L)
for (waitTime in waitTimes) {
val response = try {
httpClient.send(request, HttpResponse.BodyHandlers.ofString())
} catch (ex: IOException) {
// Some transport level exception might be thrown, continue with the retry backoff and see if it fixes itself
continue
}
val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString())
this.logger.debug("Request: {}", request.uri())
this.logger.debug("Result: {}", response.statusCode())

View File

@ -140,12 +140,6 @@ class OsuApiModels {
val scores: List<Score>
)
@Serializable
data class UserScore(
val score: Score,
val position: Long,
)
@Serializable
enum class Grade {
@SerialName("XH")

View File

@ -1,283 +0,0 @@
package com.nisemoe.nise.replays
import java.io.ByteArrayOutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
import kotlin.math.abs
import kotlin.math.round
// JVM implementation of https://github.com/circleguard/wtc-lzma-compressor/tree/master
class WTC {
companion object {
private const val CURRENT_VERSION_HEADER: Short = 1
private val VERSION_HEADER_BYTE_ARRAY = byteArrayOf((CURRENT_VERSION_HEADER.toInt() and 0xFF).toByte(), ((CURRENT_VERSION_HEADER.toInt() shr 8) and 0xFF).toByte())
fun compress(stream: String): ByteArray {
val lists = separate(stream)
val xs = unsortedDiffPackShortsToBytes(lists.x)
val ys = unsortedDiffPackShortsToBytes(lists.y)
val ws = packIntsToBytes(lists.w)
val zs = lists.z
fun packBytes(arr: ByteArray): ByteArray {
val length = arr.size
val buffer = ByteBuffer.allocate(4 + length).order(ByteOrder.LITTLE_ENDIAN)
buffer.putInt(length)
for (byte in arr) {
buffer.put(byte)
}
return buffer.array()
}
val byteStream = ByteArrayOutputStream()
byteStream.writeBytes(VERSION_HEADER_BYTE_ARRAY)
byteStream.writeBytes(packBytes(xs))
byteStream.writeBytes(packBytes(ys))
byteStream.writeBytes(packBytes(zs))
byteStream.writeBytes(packBytes(ws))
return byteStream.toByteArray()
}
fun decompress(data: ByteArray, hasVersionHeader: Boolean = true): String {
val buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN)
fun unpackBytes(): ByteArray {
val size = buffer.getInt()
val bytes = ByteArray(size)
buffer.get(bytes, 0, size)
return bytes
}
if (hasVersionHeader) {
buffer.getShort() // Version - may be used in the future
}
val xs = unpackBytes()
val ys = unpackBytes()
val zs = unpackBytes()
val ws = unpackBytes()
val xxs = unsortedDiffUnpackBytesToShorts(xs)
val yys = unsortedDiffUnpackBytesToShorts(ys)
val wws = unpackBytesToInts(ws)
return combine(FrameLists(
x = xxs,
y = yys,
z = zs,
w = wws,
))
}
data class FrameLists(
val x: ShortArray,
val y: ShortArray,
val z: ByteArray,
val w: IntArray,
)
private fun unsortedDiffPackShortsToBytes(shorts: ShortArray): ByteArray {
val start = shorts.first()
val diff = arrayDiff(shorts)
val packed = mutableListOf<Byte>()
fun pack(word: Short) {
if (abs(word.toInt()) <= Byte.MAX_VALUE) {
packed.add(word.toByte())
}
else {
packed.add(Byte.MIN_VALUE)
packed.add((word.toInt() and 0xFF).toByte())
packed.add((word.toInt() shr 8).toByte())
}
}
pack(start)
for (word in diff) {
pack(word)
}
return packed.toByteArray()
}
private fun unsortedDiffUnpackBytesToShorts(int8s: ByteArray): ShortArray {
val decoded = mutableListOf<Short>()
var i = 0
while (i < int8s.size) {
val byte = int8s[i]
if (byte == Byte.MIN_VALUE) {
i++
var word = int8s[i].toInt() and 0xFF
i++
word += int8s[i].toInt() shl 8
decoded.add(word.toShort())
}
else {
decoded.add(byte.toShort())
}
i++
}
return cumSum(decoded.toShortArray())
}
private fun packIntsToBytes(int32s: IntArray): ByteArray {
val packed = mutableListOf<Byte>()
for (dw in int32s) {
var dword = dw
if (abs(dword) <= Byte.MAX_VALUE) {
packed.add(dword.toByte())
}
else {
packed.add(Byte.MIN_VALUE)
packed.add((dword and 0xFF).toByte())
dword = dword shr 8
packed.add((dword and 0xFF).toByte())
dword = dword shr 8
packed.add((dword and 0xFF).toByte())
dword = dword shr 8
packed.add(dword.toByte())
}
}
return packed.toByteArray()
}
private fun unpackBytesToInts(int8s: ByteArray): IntArray {
val unpacked = mutableListOf<Int>()
var i = 0
while (i < int8s.size) {
val byte = int8s[i]
if (byte == Byte.MIN_VALUE) {
i++
var dword = int8s[i].toInt() and 0xFF
i++
dword += (int8s[i].toInt() shl 8) and 0xFF00
i++
dword += (int8s[i].toInt() shl 16) and 0xFF0000
i++
dword += int8s[i].toInt() shl 24
unpacked.add(dword)
}
else {
unpacked.add(byte.toInt())
}
i++
}
return unpacked.toIntArray()
}
private fun separate(stream: String): FrameLists {
val frames = stream.split(',')
val frameCount = frames.size
val ws = IntArray(frameCount)
val xs = ShortArray(frameCount)
val ys = ShortArray(frameCount)
val zs = ByteArray(frameCount)
for ((i, frame) in frames.withIndex()) {
if (frame.isEmpty()) {
continue
}
val splitFrame = frame.split('|')
val w = splitFrame[0].toInt()
val x = splitFrame[1].toFloat()
val y = splitFrame[2].toFloat()
val z = splitFrame[3].toInt()
val zz = z and 0xFF
var xx = round(x * 16).toInt()
var yy = round(y * 16).toInt()
if (xx <= -0x8000) xx = -0x8000
else if (xx >= 0x7FFF) xx = 0x7FFF
if (yy <= -0x8000) yy = -0x8000
else if (yy >= 0x7FFF) yy = 0x7FFF
ws[i] = w
xs[i] = xx.toShort()
ys[i] = yy.toShort()
zs[i] = zz.toByte()
}
return FrameLists(
x = xs,
y = ys,
z = zs,
w = ws,
)
}
private fun combine(lists: FrameLists): String {
val xArr = lists.x.map { it.toFloat() / 16 }
val yArr = lists.y.map { it.toFloat() / 16 }
val frames = arrayOfNulls<String>(xArr.size)
for (i in xArr.indices) {
val x = xArr[i]
val y = yArr[i]
val z = lists.z[i]
val w = lists.w[i]
frames[i] = "$w|$x|$y|$z"
}
return frames.joinToString(",")
}
private fun arrayDiff(arr: ShortArray): ShortArray {
if (arr.isEmpty()) {
return emptyArray<Short>().toShortArray()
}
val diffed = ShortArray(arr.size - 1)
for (index in 1..<arr.size) {
diffed[index - 1] = (arr[index] - arr[index - 1]).toShort()
}
return diffed
}
private fun cumSum(arr: ShortArray): ShortArray {
if (arr.isEmpty()) {
return emptyArray<Short>().toShortArray()
}
val cumArr = ShortArray(arr.size)
cumArr[0] = arr.first()
for (index in 1..<arr.size) {
cumArr[index] = (arr[index] + cumArr[index - 1]).toShort()
}
return cumArr
}
}
}

View File

@ -28,9 +28,9 @@ class RssService(
items.subList(0, items.size.coerceAtMost(50))
val channel = Channel(
title = "nise.stedos.dev's feed and sneed",
link = "https://nise.stedos.dev/rss",
description = "Feed of *sus* scores for osu!std - /nise.stedos.dev/",
title = "nise.moe's feed and sneed",
link = "https://nise.moe/rss",
description = "Feed of *sus* scores for osu!std - /nise.moe/",
lastBuildDate = Date().toInstant().atZone(ZoneOffset.UTC).format(DateTimeFormatter.RFC_1123_DATE_TIME),
item = items.map {
Item(
@ -42,7 +42,7 @@ class RssService(
)
},
atomLink = AtomLink(href = "https://nise.stedos.dev/rss.xml")
atomLink = AtomLink(href = "https://nise.moe/rss.xml")
)
return RssFeed(channel = channel)
@ -74,8 +74,8 @@ class RssService(
if(score1.addedAt != null) {
val item = RssFeedController.IntermeriaryFeedItem(
title = "Possible stolen replay",
guid = "https://nise.stedos.dev/p/${score1.replayId}/${score2.replayId}",
link = "https://nise.stedos.dev/p/${score1.replayId}/${score2.replayId}",
guid = "https://nise.moe/p/${score1.replayId}/${score2.replayId}",
link = "https://nise.moe/p/${score1.replayId}/${score2.replayId}",
description = "Similarity: ${result[SCORES_SIMILARITY.SIMILARITY]}%\n" +
"Replay by ${user1.username} on ${beatmap.artist} - ${beatmap.title} [${beatmap.version}] (${beatmap.starRating} stars)\n" +
"Replay by ${user2.username} on ${beatmap.artist} - ${beatmap.title} [${beatmap.version}] (${beatmap.starRating} stars)",
@ -113,8 +113,8 @@ class RssService(
if(score.addedAt != null) {
val item = RssFeedController.IntermeriaryFeedItem(
title = "Suspicious score by ${user.username}",
guid = "https://nise.stedos.dev/s/${score.replayId}",
link = "https://nise.stedos.dev/s/${score.replayId}",
guid = "https://nise.moe/s/${score.replayId}",
link = "https://nise.moe/s/${score.replayId}",
description = "New score by ${user.username} on ${beatmap.artist} - ${beatmap.title} [${beatmap.version}] (${beatmap.starRating} stars)",
pubDate = score.addedAt!!
)

View File

@ -13,6 +13,7 @@ import org.slf4j.LoggerFactory
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
import org.springframework.util.StopWatch
import kotlin.math.roundToInt
@Service
class GlobalCache(
@ -33,7 +34,7 @@ class GlobalCache(
fun updateCaches() {
val stopwatch = StopWatch()
stopwatch.start()
logger.info("Updating the cache...")
logger.info("Updating the cache!")
runBlocking {
val rssFeedDeferred = async { rssService.generateFeed() }
@ -49,8 +50,6 @@ class GlobalCache(
stopwatch.stop()
logger.info("Cache updated in {} seconds", String.format("%.2f", stopwatch.totalTimeSeconds))
logger.info("[CACHE]: Similar replays count: {}", similarReplays?.size)
logger.info("[CACHE]: Suspicious scores count: {}", suspiciousScores?.size)
}
}

View File

@ -293,7 +293,7 @@ class ImportScores(
dslContext.update(BEATMAPS)
.set(BEATMAPS.BEATMAP_HASH, topScore.beatmap.checksum)
.set(BEATMAPS.STAR_RATING, topScore.beatmap.difficulty_rating)
.set(BEATMAPS.VERSION, topScore.beatmap.version)
.set(BEATMAPS.VERSION, beatmap.version)
.set(BEATMAPS.ARTIST, topScore.beatmapset!!.artist)
.set(BEATMAPS.SOURCE, topScore.beatmapset.source)
.set(BEATMAPS.TITLE, topScore.beatmapset.title)
@ -727,8 +727,6 @@ class ImportScores(
return
}
val topScore = osuApi.getUserBeatmapScore(score.user_id, beatmapId, score.best_id)
dslContext.insertInto(SCORES)
.set(SCORES.BEATMAP_ID, beatmapId)
.set(SCORES.COUNT_300, score.statistics.count_300)
@ -747,7 +745,6 @@ class ImportScores(
.set(SCORES.REPLAY_ID, score.best_id)
.set(SCORES.USER_ID, score.user_id)
.set(SCORES.VERSION, CURRENT_VERSION)
.set(SCORES.LEADERBOARD_RANK, topScore?.position)
.execute()
this.statistics.scoresAddedToDatabase++
@ -801,12 +798,6 @@ class ImportScores(
return
}
// If the score has a low amount of hits the UR calculation will be inaccurate, skip these plays
if (processedReplay.hit_count == null || processedReplay.hit_count < 10) {
this.logger.warn("Processed play has less than 10 hits, skipping score ${score.id}")
return
}
val compressedReplay = CompressReplay.compressReplay(scoreReplay.content.toByteArray())
val scoreId = dslContext.update(SCORES)

View File

@ -78,7 +78,6 @@ class ScoreSearchController(
val perfect: Boolean?,
val pp: Double?,
val rank: String?,
val leaderboard_rank: Long?,
val replay_id: Long?,
val score: Long?,
val ur: Double?,

View File

@ -51,7 +51,6 @@ class ScoreSearchSchemaController(
InternalSchemaField("perfect", "Perfect", Category.score, Type.boolean, false, "if score is a full combo", databaseField = SCORES.PERFECT),
InternalSchemaField("pp", "Score PP", Category.score, Type.number, true, "performance points for score", databaseField = SCORES.PP),
InternalSchemaField("rank", "Rank", Category.score, Type.grade, false, "score grade", databaseField = SCORES.RANK),
InternalSchemaField("leaderboard_rank", "Leaderboard Rank", Category.score, Type.number, false, "leaderboard position of the play at import", databaseField = SCORES.LEADERBOARD_RANK),
InternalSchemaField("replay_id", "Replay ID", Category.score, Type.number, false, "identifier for replay", databaseField = SCORES.REPLAY_ID),
InternalSchemaField("score", "Score", Category.score, Type.number, false, "score value", databaseField = SCORES.SCORE),
InternalSchemaField("ur", "UR", Category.metrics, Type.number, false, "unstable rate", databaseField = SCORES.UR),

View File

@ -83,7 +83,6 @@ class ScoreSearchService(
SCORES.ERROR_COEFFICIENT_OF_VARIATION,
SCORES.ERROR_KURTOSIS,
SCORES.ERROR_SKEWNESS,
SCORES.LEADERBOARD_RANK,
// Beatmaps fields
BEATMAPS.ARTIST,
@ -187,7 +186,6 @@ class ScoreSearchService(
perfect = it.get(SCORES.PERFECT),
pp = it.get(SCORES.PP)?.roundToInt()?.toDouble(),
rank = it.get(SCORES.RANK),
leaderboard_rank = it.get(SCORES.LEADERBOARD_RANK),
replay_id = it.get(SCORES.REPLAY_ID),
score = it.get(SCORES.SCORE),
ur = it.get(SCORES.UR),

View File

@ -1 +0,0 @@
ALTER TABLE public.scores ADD COLUMN leaderboard_rank BIGINT;

View File

@ -1,43 +0,0 @@
package com.nisemoe.nise.osu
import com.nisemoe.nise.replays.WTC
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import org.junit.jupiter.api.Assertions
import org.testcontainers.shaded.org.bouncycastle.util.Arrays
import java.nio.file.Paths
import kotlin.test.assertEquals
class WtcTest {
private val resourcesPath = Paths.get("src", "test", "resources", "wtc")
/**
* Compares the bytes produced from the `wtcCompress()` function with bytes produced from the Python implementation of WTC.
*/
@ParameterizedTest
@ValueSource(strings = ["replay_1", "replay_2", "replay_3", "replay_4"])
fun wtcCompressReturnsCorrectBytes(replayName: String) {
val expected = resourcesPath.resolve("${replayName}_compressed.dat").toFile().readBytes()
val replayEvents = resourcesPath.resolve("${replayName}_events.txt").toFile().readText()
val wtcCompressed = WTC.compress(replayEvents)
// We include a version header at the start of the compressed byte array - create a new array excluding these
// so we can compare just the raw data with the Python WTC implementation's output.
// (remove the first two bytes since the version header is a Short)
val wtcCompressedWithoutVersionHeader = Arrays.copyOfRange(wtcCompressed, 2, wtcCompressed.size)
Assertions.assertArrayEquals(expected, wtcCompressedWithoutVersionHeader)
}
@ParameterizedTest
@ValueSource(strings = ["replay_1", "replay_2", "replay_3", "replay_4"])
fun wtcDecompressReturnsCorrectReplayFrames(replayName: String) {
val expected = resourcesPath.resolve("${replayName}_decompressed.txt").toFile().readText()
val compressedReplay = resourcesPath.resolve("${replayName}_compressed.dat").toFile().readBytes()
val wtcDecompressed = WTC.decompress(compressedReplay, false)
assertEquals(expected, wtcDecompressed)
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -102,7 +102,6 @@ class ReplayResponse:
sliderend_release_standard_deviation_adjusted: float
judgements: List[Hit]
hit_count: int
def to_dict(self):
d = asdict(self)
@ -218,8 +217,7 @@ async def process_replay(request: Request):
sliderend_release_standard_deviation=np.std(se, ddof=1),
sliderend_release_standard_deviation_adjusted=np.std(my_filter_outliers(se), ddof=1),
judgements=judgements,
hit_count=len(hits)
judgements=judgements
)
return json(ur_response.to_dict())

View File

@ -34,5 +34,5 @@
<router-outlet></router-outlet>
<div class="text-center version">
v20250622
v20250213
</div>

View File

@ -97,10 +97,9 @@ export interface ReplayData {
snaps: number;
hits: number;
pp: number;
pp: number,
perfect: boolean;
max_combo: number;
leaderboard_rank?: number;
max_combo: number,
mean_error?: number,
error_variance?: number,
@ -157,7 +156,6 @@ export interface SuspiciousScore {
pp: number;
frametime: number;
ur: number;
leaderboard_rank: number;
}
export interface SimilarReplay {
@ -170,8 +168,6 @@ export interface SimilarReplay {
replay_date_2: string;
replay_pp_1: number;
replay_pp_2: number;
replay_leaderboard_rank_1: number;
replay_leaderboard_rank_2: number;
beatmap_id: number;
beatmap_title: string;
beatmap_star_rating: number;

View File

@ -129,7 +129,7 @@
<td *ngFor="let column of fields" [hidden]="!column.active" class="text-center" style="line-height: 32px">
<ng-container *ngIf="getValue(entry, column.name) !== null; else nullDisplay">
<ng-container *ngIf="column.type == 'number'">
{{ isFieldId(column.name) ? getValue(entry, column.name) : (getValue(entry, column.name) | number) }}
{{ getValue(entry, column.name) | number }}
</ng-container>
<ng-container *ngIf="column.type == 'flag'">
<span class="flag" [title]="getValue(entry, column.name)">{{ countryCodeToFlag(getValue(entry, column.name)) }}</span>

View File

@ -352,8 +352,6 @@ export class SearchComponent implements OnInit {
}
}
isFieldId = (field: string): boolean => field === 'id' || field.includes("_id");
protected readonly countryCodeToFlag = countryCodeToFlag;
protected readonly Math = Math;
protected readonly formatDuration = formatDuration;

View File

@ -32,7 +32,7 @@ export class TextReportService {
report += `\n\n${this.getStealingReport(similarReplay)}\n`;
}
report += `\n\nGenerated on ${site} - [${userDetails.username} profile](${environment.webUrl}/u/${userDetails.user_id})`;
report += `\n\nGenerated on ${site} - [${userDetails.username} on ${site}](${environment.webUrl}/u/${userDetails.user_id})`;
return report;
}

View File

@ -8,7 +8,7 @@
<ng-container *ngIf="this.isError">
<div class="main term">
<div class="text-center">
An error occurred. Maybe try again in a bit?
An error occured. Maybe try again in a bit?
</div>
</div>
</ng-container>
@ -29,9 +29,9 @@
</a>
</div>
<div class="text-center mt-2">
<a class="btn" [href]="'https://nise.stedos.dev/replay-viewer/' + this.pair.replays[0].replay_id + '/' + this.pair.replays[1].replay_id" target="_blank">Open in Replay Viewer</a>
</div>
<!-- <div class="text-center mt-2">
<a class="btn" [href]="'https://replay.nise.moe/' + this.pair.replays[0].replay_id + '/' + this.pair.replays[1].replay_id" target="_blank">Open in Replay Viewer</a>
</div> -->
<div class="some-page-wrapper text-center">
<div class="row">

View File

@ -8,7 +8,7 @@
<ng-container *ngIf="this.error">
<div class="main term">
<div class="text-center">
An error occurred. Maybe try again in a bit?
An error occured. Maybe try again in a bit?
</div>
</div>
</ng-container>
@ -53,9 +53,9 @@
Open in CircleGuard
</a>
<a style="flex: 1" class="text-center" [href]="'https://nise.stedos.dev/replay-viewer/' + this.replayData.replay_id" target="_blank" [class.disabled]="!hasReplay()">
<!-- <a style="flex: 1" class="text-center" [href]="'https://replay.nise.moe/' + this.replayData.replay_id" target="_blank" [class.disabled]="!hasReplay()">
Open in Replay Viewer
</a>
</a> -->
</div>
@ -136,10 +136,6 @@
<span class="stat-label">PP</span>
<span class="stat-value">{{ this.replayData.pp | number: '1.0-0' }}</span>
</div>
<div class="stat" *ngIf="this.replayData.leaderboard_rank">
<span class="stat-label">Rank</span>
<span class="stat-value">{{ this.replayData.leaderboard_rank }}</span>
</div>
</div>
<div class="stats-container">
<div class="stat">

View File

@ -51,20 +51,6 @@
<input class="form-control" type="text" id="searchBeatmap" [(ngModel)]="this.filterManager.filters.searchBeatmap" (input)="filterScores()"
[readOnly]="this.isUrlFilters" [disabled]="this.isUrlFilters">
</p>
<!-- Min Rank -->
<p>
<label for="minRank" class="form-label">Min Rank (of stolen)</label>
<input class="form-control" type="number" id="minRank" [(ngModel)]="this.filterManager.filters.minRank" (input)="filterScores()"
[readOnly]="this.isUrlFilters" [disabled]="this.isUrlFilters">
</p>
<!-- Max Rank -->
<p>
<label for="maxRank" class="form-label">Max Rank (of stolen)</label>
<input class="form-control" type="number" id="maxRank" [(ngModel)]="this.filterManager.filters.maxRank" (input)="filterScores()"
[readOnly]="this.isUrlFilters" [disabled]="this.isUrlFilters">
</p>
</fieldset>
<div *ngIf="getTotalPages() > 1" style="padding: 20px">

View File

@ -19,9 +19,6 @@ export interface FilterStolenReplays {
minSimilarity?: number;
maxSimilarity?: number;
minRank?: number;
maxRank?: number;
}
@Component({
@ -136,12 +133,7 @@ export class ViewSimilarReplaysComponent implements OnInit {
const similarityMatch = (filters.minSimilarity !== undefined ? score.similarity >= filters.minSimilarity : true) &&
(filters.maxSimilarity !== undefined ? score.similarity <= filters.maxSimilarity : true);
const scoreHasRank = score.replay_leaderboard_rank_2 > 0;
const rankMatch = (filters.minRank == null || (score.replay_leaderboard_rank_2 <= filters.minRank && scoreHasRank)) &&
(filters.maxRank == null || score.replay_leaderboard_rank_2 >= filters.maxRank && scoreHasRank);
return usernameMatch && beatmapMatch && ppMatch && similarityMatch && rankMatch;
return usernameMatch && beatmapMatch && ppMatch && similarityMatch;
});
this.filterManager.persistToLocalStorage();

View File

@ -24,7 +24,7 @@
<input class="form-control" type="number" id="maxPP" [(ngModel)]="this.filterManager.filters.maxPP" (input)="filterScores()"
[readOnly]="this.isUrlFilters" [disabled]="this.isUrlFilters">
</p>
`
<!-- Min cvUR -->
<p>
<label for="minUR" class="form-label">Min cvUR</label>
@ -52,20 +52,6 @@
<input class="form-control" type="text" id="searchBeatmap" [(ngModel)]="this.filterManager.filters.searchBeatmap" (input)="filterScores()"
[readOnly]="this.isUrlFilters" [disabled]="this.isUrlFilters">
</p>
<!-- Min Rank -->
<p>
<label for="minRank" class="form-label">Min Rank</label>
<input class="form-control" type="number" id="minRank" [(ngModel)]="this.filterManager.filters.minRank" (input)="filterScores()"
[readOnly]="this.isUrlFilters" [disabled]="this.isUrlFilters">
</p>
<!-- Max Rank -->
<p>
<label for="maxRank" class="form-label">Max Rank</label>
<input class="form-control" type="number" id="maxRank" [(ngModel)]="this.filterManager.filters.maxRank" (input)="filterScores()"
[readOnly]="this.isUrlFilters" [disabled]="this.isUrlFilters">
</p>
</fieldset>
<div *ngIf="getTotalPages() > 1" style="padding: 20px">

View File

@ -19,9 +19,6 @@ export interface SuspiciousScoresFilter {
searchUsername?: string;
searchBeatmap?: string;
minRank?: number;
maxRank?: number;
}
@Component({
@ -155,12 +152,7 @@ export class ViewSuspiciousScoresComponent implements OnInit, OnDestroy {
const urMatch = (filters.minUR == null || score.ur >= filters.minUR) &&
(filters.maxUR == null || score.ur <= filters.maxUR);
const scoreHasRank = score.leaderboard_rank > 0;
const rankMatch = (filters.minRank == null || (score.leaderboard_rank <= filters.minRank && scoreHasRank)) &&
(filters.maxRank == null || score.leaderboard_rank >= filters.maxRank && scoreHasRank);
return usernameMatch && beatmapMatch && ppMatch && urMatch && rankMatch;
return usernameMatch && beatmapMatch && ppMatch && urMatch;
});
// Presumably persists the current state of filters for future sessions

View File

@ -298,6 +298,7 @@ fieldset button:not(:last-child) {
fieldset p label {
display: block;
margin-right: 100px !important;
}
.badge.mod {

View File

@ -18,24 +18,12 @@ export class DownloadFilesService {
}
downloadCSV(input: Object[], columns: string[], fileName: string = 'data') {
let csvData = columns.join(',') + '\n';
const header = columns.join(',') + '\n';
for (const row of input) {
let rowData: string[] = [];
for (const column of columns) {
let value = (row as Record<string, any>)[column];
if (typeof value === 'string') {
value = value.replaceAll(',', ';');
} else if (Array.isArray(value)) {
value = value.join(';');
}
rowData.push(value);
}
csvData += rowData.join(',') + '\n';
}
let csvData = input.map(row =>
input.map(row => Object.values(row).join(',')).join('\n')
).join('\n');
csvData = header + csvData;
const blob = new Blob([csvData], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);

View File

@ -63,7 +63,6 @@ services:
POSTGRES_DB: ${DB_NAME}
# redis
REDIS_DB: 4
REDIS_HOST: "redis"
# Discord
WEBHOOK_URL: ${WEBHOOK_URL}
SCORES_WEBHOOK_URL: ${SCORES_WEBHOOK_URL}
@ -101,10 +100,6 @@ services:
container_name: nise-frontend
restart: always
nise-replay-viewer:
image: code.stedos.dev/stedos/nise-replay-viewer:latest
container_name: nise-replay-viewer
restart: always
volumes:
postgres-data:

View File

@ -35,12 +35,6 @@ http {
proxy_set_header Connection "upgrade";
}
location /replay-viewer/ {
proxy_pass http://nise-replay-viewer/;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}

View File

@ -1,4 +1,4 @@
FROM nginx:1.27.0-alpine
FROM openresty/openresty:focal
RUN rm -rf /usr/share/nginx/html/*

View File

@ -4,17 +4,17 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>/replay/ - nise.stedos.dev</title>
<link rel="icon" type="image/x-icon" href="https:/nise.stedos.dev/assets/favicon.ico">
<title>/replay/ - nise.moe</title>
<link rel="icon" type="image/x-icon" href="https://nise.moe/assets/favicon.ico">
<!-- Embed data -->
<meta property="og:title" content="/nise.stedos.dev/ - osu!cheaters finder">
<meta property="og:title" content="/nise.moe/ - osu!cheaters finder">
<meta property="og:description" content="crawls osu!std replays and tries to find naughty boys.">
<meta property="og:url" content="https://nise.stedos.dev">
<meta property="og:image" content="https://nise.stedos.dev/assets/banner.png">
<meta property="og:url" content="https://nise.moe">
<meta property="og:image" content="https://nise.moe/assets/banner.png">
<meta property="og:type" content="website">
<meta name="theme-color" content="#151515">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image:src" content="https://nise.stedos.dev/assets/banner.png">
<meta name="twitter:image:src" content="https://nise.moe/assets/banner.png">
</head>
<body>

View File

@ -18,12 +18,9 @@ export function App() {
}
useEffect(() => {
// We might be beind a reverse proxy - remove the proxied URL
const path = location.pathname.replace("/replay-viewer", "");
// This pattern matches one or more digits followed by an optional slash and any characters (non-greedy)
const pathRegex = /^\/(\d+)(?:\/(\d+))?/;
const match = path.match(pathRegex);
const match = location.pathname.match(pathRegex);
if (match) {
// match[1] will contain the first ID, match[2] (if present) will contain the second ID

View File

@ -8,7 +8,7 @@ export function Navbar() {
<Menubar className="rounded-none border-x-0 border-t-0 flex justify-between px-4">
<div className="flex gap-2">
<div className="flex items-center gap-2">
<img src={"https://nise.stedos.dev/assets/keisatsu-chan.png"} width={48}/>
<img src={"https://nise.moe/assets/keisatsu-chan.png"} width={48}/>
<h3 className="scroll-m-20 text-lg font-semibold tracking-tight">
/replay/
</h3>
@ -24,14 +24,14 @@ export function Navbar() {
{OsuRenderer.beatmap && (
<>
{OsuRenderer.replay2 == null && (
<a href={"https://nise.stedos.dev/s/" + OsuRenderer.replay.info.id} target="_blank"
<a href={"https://nise.moe/s/" + OsuRenderer.replay.info.id} target="_blank"
className="flex items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent hover:bg-accent">
View on nise.stedos.dev
View on nise.moe
</a>)}
{OsuRenderer.replay2 && (
<a href={"https://nise.stedos.dev/p/" + OsuRenderer.replay.info.id + "/" + OsuRenderer.replay2.info.id} target="_blank"
<a href={"https://nise.moe/p/" + OsuRenderer.replay.info.id + "/" + OsuRenderer.replay2.info.id} target="_blank"
className="flex items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent hover:bg-accent">
View on nise.stedos.dev
View on nise.moe
</a>)}
</>
)}

View File

@ -39,7 +39,7 @@ export class Drawer {
static async loadDefaultImages() {
const imageLoadPromises = Object.keys(Drawer.images).map(imageName =>
loadImageAsync(`/replay-viewer/${imageName}.png`).then(
loadImageAsync(`/${imageName}.png`).then(
image => {
Drawer.images[imageName as keyof typeof Drawer.images] = image;
},

View File

@ -231,7 +231,7 @@ export class OsuRenderer {
static getApiUrl(): string {
return document.location.hostname === "localhost"
? `http://localhost:8080`
: `https://nise.stedos.dev/api`;
: `https://nise.moe/api`;
}
static async loadReplayPairFromUrl(replayId1: number, replayId2: number) {

View File

@ -9,5 +9,4 @@ export default defineConfig({
"@": path.resolve(__dirname, "./src"),
},
},
base: "/replay-viewer/",
});