Basic work on replay upload

This commit is contained in:
nise.moe 2024-03-04 20:34:21 +01:00
parent a4edf5d0c2
commit 48cf50d448
15 changed files with 1210 additions and 57 deletions

BIN
nise-backend/replay1.osr Normal file

Binary file not shown.

View File

@ -22,6 +22,7 @@ import com.nisemoe.generated.tables.Scores
import com.nisemoe.generated.tables.ScoresJudgements import com.nisemoe.generated.tables.ScoresJudgements
import com.nisemoe.generated.tables.ScoresSimilarity import com.nisemoe.generated.tables.ScoresSimilarity
import com.nisemoe.generated.tables.UpdateUserQueue import com.nisemoe.generated.tables.UpdateUserQueue
import com.nisemoe.generated.tables.UserScores
import com.nisemoe.generated.tables.Users import com.nisemoe.generated.tables.Users
import kotlin.collections.List import kotlin.collections.List
@ -85,6 +86,11 @@ open class Public : SchemaImpl("public", DefaultCatalog.DEFAULT_CATALOG) {
*/ */
val UPDATE_USER_QUEUE: UpdateUserQueue get() = UpdateUserQueue.UPDATE_USER_QUEUE val UPDATE_USER_QUEUE: UpdateUserQueue get() = UpdateUserQueue.UPDATE_USER_QUEUE
/**
* The table <code>public.user_scores</code>.
*/
val USER_SCORES: UserScores get() = UserScores.USER_SCORES
/** /**
* The table <code>public.users</code>. * The table <code>public.users</code>.
*/ */
@ -114,6 +120,7 @@ open class Public : SchemaImpl("public", DefaultCatalog.DEFAULT_CATALOG) {
ScoresJudgements.SCORES_JUDGEMENTS, ScoresJudgements.SCORES_JUDGEMENTS,
ScoresSimilarity.SCORES_SIMILARITY, ScoresSimilarity.SCORES_SIMILARITY,
UpdateUserQueue.UPDATE_USER_QUEUE, UpdateUserQueue.UPDATE_USER_QUEUE,
UserScores.USER_SCORES,
Users.USERS Users.USERS
) )
} }

View File

@ -0,0 +1,355 @@
/*
* This file is generated by jOOQ.
*/
package com.nisemoe.generated.tables
import com.nisemoe.generated.Public
import com.nisemoe.generated.tables.records.UserScoresRecord
import java.time.OffsetDateTime
import java.util.UUID
import kotlin.collections.List
import org.jooq.Check
import org.jooq.Field
import org.jooq.ForeignKey
import org.jooq.Name
import org.jooq.Record
import org.jooq.Schema
import org.jooq.Table
import org.jooq.TableField
import org.jooq.TableOptions
import org.jooq.impl.DSL
import org.jooq.impl.Internal
import org.jooq.impl.SQLDataType
import org.jooq.impl.TableImpl
/**
* This class is generated by jOOQ.
*/
@Suppress("UNCHECKED_CAST")
open class UserScores(
alias: Name,
child: Table<out Record>?,
path: ForeignKey<out Record, UserScoresRecord>?,
aliased: Table<UserScoresRecord>?,
parameters: Array<Field<*>?>?
): TableImpl<UserScoresRecord>(
alias,
Public.PUBLIC,
child,
path,
aliased,
parameters,
DSL.comment(""),
TableOptions.table()
) {
companion object {
/**
* The reference instance of <code>public.user_scores</code>
*/
val USER_SCORES: UserScores = UserScores()
}
/**
* The class holding records for this type
*/
override fun getRecordType(): Class<UserScoresRecord> = UserScoresRecord::class.java
/**
* The column <code>public.user_scores.id</code>.
*/
val ID: TableField<UserScoresRecord, UUID?> = createField(DSL.name("id"), SQLDataType.UUID.nullable(false).defaultValue(DSL.field(DSL.raw("gen_random_uuid()"), SQLDataType.UUID)), this, "")
/**
* The column <code>public.user_scores.added_at</code>.
*/
val ADDED_AT: TableField<UserScoresRecord, OffsetDateTime?> = createField(DSL.name("added_at"), SQLDataType.TIMESTAMPWITHTIMEZONE(6).defaultValue(DSL.field(DSL.raw("CURRENT_TIMESTAMP"), SQLDataType.TIMESTAMPWITHTIMEZONE)), this, "")
/**
* The column <code>public.user_scores.beatmap_id</code>.
*/
val BEATMAP_ID: TableField<UserScoresRecord, Int?> = createField(DSL.name("beatmap_id"), SQLDataType.INTEGER, this, "")
/**
* The column <code>public.user_scores.beatmap_hash</code>.
*/
val BEATMAP_HASH: TableField<UserScoresRecord, String?> = createField(DSL.name("beatmap_hash"), SQLDataType.VARCHAR(32), this, "")
/**
* The column <code>public.user_scores.game_mode</code>.
*/
val GAME_MODE: TableField<UserScoresRecord, Int?> = createField(DSL.name("game_mode"), SQLDataType.INTEGER, this, "")
/**
* The column <code>public.user_scores.game_version</code>.
*/
val GAME_VERSION: TableField<UserScoresRecord, Int?> = createField(DSL.name("game_version"), SQLDataType.INTEGER, this, "")
/**
* The column <code>public.user_scores.player_name</code>.
*/
val PLAYER_NAME: TableField<UserScoresRecord, String?> = createField(DSL.name("player_name"), SQLDataType.VARCHAR(256), this, "")
/**
* The column <code>public.user_scores.replay_hash</code>.
*/
val REPLAY_HASH: TableField<UserScoresRecord, String?> = createField(DSL.name("replay_hash"), SQLDataType.VARCHAR(32), this, "")
/**
* The column <code>public.user_scores.count_300</code>.
*/
val COUNT_300: TableField<UserScoresRecord, Short?> = createField(DSL.name("count_300"), SQLDataType.SMALLINT, this, "")
/**
* The column <code>public.user_scores.count_100</code>.
*/
val COUNT_100: TableField<UserScoresRecord, Short?> = createField(DSL.name("count_100"), SQLDataType.SMALLINT, this, "")
/**
* The column <code>public.user_scores.count_50</code>.
*/
val COUNT_50: TableField<UserScoresRecord, Short?> = createField(DSL.name("count_50"), SQLDataType.SMALLINT, this, "")
/**
* The column <code>public.user_scores.count_geki</code>.
*/
val COUNT_GEKI: TableField<UserScoresRecord, Short?> = createField(DSL.name("count_geki"), SQLDataType.SMALLINT, this, "")
/**
* The column <code>public.user_scores.count_katu</code>.
*/
val COUNT_KATU: TableField<UserScoresRecord, Short?> = createField(DSL.name("count_katu"), SQLDataType.SMALLINT, this, "")
/**
* The column <code>public.user_scores.count_miss</code>.
*/
val COUNT_MISS: TableField<UserScoresRecord, Short?> = createField(DSL.name("count_miss"), SQLDataType.SMALLINT, this, "")
/**
* The column <code>public.user_scores.total_score</code>.
*/
val TOTAL_SCORE: TableField<UserScoresRecord, Int?> = createField(DSL.name("total_score"), SQLDataType.INTEGER, this, "")
/**
* The column <code>public.user_scores.max_combo</code>.
*/
val MAX_COMBO: TableField<UserScoresRecord, Short?> = createField(DSL.name("max_combo"), SQLDataType.SMALLINT, this, "")
/**
* The column <code>public.user_scores.perfect</code>.
*/
val PERFECT: TableField<UserScoresRecord, Boolean?> = createField(DSL.name("perfect"), SQLDataType.BOOLEAN, this, "")
/**
* The column <code>public.user_scores.mods</code>.
*/
val MODS: TableField<UserScoresRecord, Int?> = createField(DSL.name("mods"), SQLDataType.INTEGER, this, "")
/**
* The column <code>public.user_scores.life_bar_graph</code>.
*/
val LIFE_BAR_GRAPH: TableField<UserScoresRecord, String?> = createField(DSL.name("life_bar_graph"), SQLDataType.CLOB, this, "")
/**
* The column <code>public.user_scores.timestamp</code>.
*/
val TIMESTAMP: TableField<UserScoresRecord, Long?> = createField(DSL.name("timestamp"), SQLDataType.BIGINT, this, "")
/**
* The column <code>public.user_scores.replay_length</code>.
*/
val REPLAY_LENGTH: TableField<UserScoresRecord, Int?> = createField(DSL.name("replay_length"), SQLDataType.INTEGER, this, "")
/**
* The column <code>public.user_scores.replay_data</code>.
*/
val REPLAY_DATA: TableField<UserScoresRecord, String?> = createField(DSL.name("replay_data"), SQLDataType.CLOB, this, "")
/**
* The column <code>public.user_scores.online_score_id</code>.
*/
val ONLINE_SCORE_ID: TableField<UserScoresRecord, Long?> = createField(DSL.name("online_score_id"), SQLDataType.BIGINT, this, "")
/**
* The column <code>public.user_scores.additional_mod_info</code>.
*/
val ADDITIONAL_MOD_INFO: TableField<UserScoresRecord, Double?> = createField(DSL.name("additional_mod_info"), SQLDataType.DOUBLE, this, "")
/**
* The column <code>public.user_scores.ur</code>.
*/
val UR: TableField<UserScoresRecord, Double?> = createField(DSL.name("ur"), SQLDataType.DOUBLE, this, "")
/**
* The column <code>public.user_scores.frametime</code>.
*/
val FRAMETIME: TableField<UserScoresRecord, Double?> = createField(DSL.name("frametime"), SQLDataType.DOUBLE, this, "")
/**
* The column <code>public.user_scores.edge_hits</code>.
*/
val EDGE_HITS: TableField<UserScoresRecord, Int?> = createField(DSL.name("edge_hits"), SQLDataType.INTEGER, this, "")
/**
* The column <code>public.user_scores.snaps</code>.
*/
val SNAPS: TableField<UserScoresRecord, Int?> = createField(DSL.name("snaps"), SQLDataType.INTEGER, this, "")
/**
* The column <code>public.user_scores.adjusted_ur</code>.
*/
val ADJUSTED_UR: TableField<UserScoresRecord, Double?> = createField(DSL.name("adjusted_ur"), SQLDataType.DOUBLE, this, "")
/**
* The column <code>public.user_scores.mean_error</code>.
*/
val MEAN_ERROR: TableField<UserScoresRecord, Double?> = createField(DSL.name("mean_error"), SQLDataType.DOUBLE, this, "")
/**
* The column <code>public.user_scores.error_variance</code>.
*/
val ERROR_VARIANCE: TableField<UserScoresRecord, Double?> = createField(DSL.name("error_variance"), SQLDataType.DOUBLE, this, "")
/**
* The column <code>public.user_scores.error_standard_deviation</code>.
*/
val ERROR_STANDARD_DEVIATION: TableField<UserScoresRecord, Double?> = createField(DSL.name("error_standard_deviation"), SQLDataType.DOUBLE, this, "")
/**
* The column <code>public.user_scores.minimum_error</code>.
*/
val MINIMUM_ERROR: TableField<UserScoresRecord, Double?> = createField(DSL.name("minimum_error"), SQLDataType.DOUBLE, this, "")
/**
* The column <code>public.user_scores.maximum_error</code>.
*/
val MAXIMUM_ERROR: TableField<UserScoresRecord, Double?> = createField(DSL.name("maximum_error"), SQLDataType.DOUBLE, this, "")
/**
* The column <code>public.user_scores.error_range</code>.
*/
val ERROR_RANGE: TableField<UserScoresRecord, Double?> = createField(DSL.name("error_range"), SQLDataType.DOUBLE, this, "")
/**
* The column
* <code>public.user_scores.error_coefficient_of_variation</code>.
*/
val ERROR_COEFFICIENT_OF_VARIATION: TableField<UserScoresRecord, Double?> = createField(DSL.name("error_coefficient_of_variation"), SQLDataType.DOUBLE, this, "")
/**
* The column <code>public.user_scores.error_kurtosis</code>.
*/
val ERROR_KURTOSIS: TableField<UserScoresRecord, Double?> = createField(DSL.name("error_kurtosis"), SQLDataType.DOUBLE, this, "")
/**
* The column <code>public.user_scores.error_skewness</code>.
*/
val ERROR_SKEWNESS: TableField<UserScoresRecord, Double?> = createField(DSL.name("error_skewness"), SQLDataType.DOUBLE, this, "")
/**
* The column <code>public.user_scores.keypresses_times</code>.
*/
val KEYPRESSES_TIMES: TableField<UserScoresRecord, Array<Double?>?> = createField(DSL.name("keypresses_times"), SQLDataType.FLOAT.array(), this, "")
/**
* The column <code>public.user_scores.keypresses_median</code>.
*/
val KEYPRESSES_MEDIAN: TableField<UserScoresRecord, Double?> = createField(DSL.name("keypresses_median"), SQLDataType.DOUBLE, this, "")
/**
* The column <code>public.user_scores.keypresses_standard_deviation</code>.
*/
val KEYPRESSES_STANDARD_DEVIATION: TableField<UserScoresRecord, Double?> = createField(DSL.name("keypresses_standard_deviation"), SQLDataType.DOUBLE, this, "")
/**
* The column <code>public.user_scores.sliderend_release_times</code>.
*/
val SLIDEREND_RELEASE_TIMES: TableField<UserScoresRecord, Array<Double?>?> = createField(DSL.name("sliderend_release_times"), SQLDataType.FLOAT.array(), this, "")
/**
* The column <code>public.user_scores.sliderend_release_median</code>.
*/
val SLIDEREND_RELEASE_MEDIAN: TableField<UserScoresRecord, Double?> = createField(DSL.name("sliderend_release_median"), SQLDataType.DOUBLE, this, "")
/**
* The column
* <code>public.user_scores.sliderend_release_standard_deviation</code>.
*/
val SLIDEREND_RELEASE_STANDARD_DEVIATION: TableField<UserScoresRecord, Double?> = createField(DSL.name("sliderend_release_standard_deviation"), SQLDataType.DOUBLE, this, "")
/**
* The column <code>public.user_scores.keypresses_median_adjusted</code>.
*/
val KEYPRESSES_MEDIAN_ADJUSTED: TableField<UserScoresRecord, Double?> = createField(DSL.name("keypresses_median_adjusted"), SQLDataType.DOUBLE, this, "")
/**
* The column
* <code>public.user_scores.keypresses_standard_deviation_adjusted</code>.
*/
val KEYPRESSES_STANDARD_DEVIATION_ADJUSTED: TableField<UserScoresRecord, Double?> = createField(DSL.name("keypresses_standard_deviation_adjusted"), SQLDataType.DOUBLE, this, "")
/**
* The column
* <code>public.user_scores.sliderend_release_median_adjusted</code>.
*/
val SLIDEREND_RELEASE_MEDIAN_ADJUSTED: TableField<UserScoresRecord, Double?> = createField(DSL.name("sliderend_release_median_adjusted"), SQLDataType.DOUBLE, this, "")
/**
* The column
* <code>public.user_scores.sliderend_release_standard_deviation_adjusted</code>.
*/
val SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED: TableField<UserScoresRecord, Double?> = createField(DSL.name("sliderend_release_standard_deviation_adjusted"), SQLDataType.DOUBLE, this, "")
/**
* The column <code>public.user_scores.judgements</code>.
*/
val JUDGEMENTS: TableField<UserScoresRecord, ByteArray?> = createField(DSL.name("judgements"), SQLDataType.BLOB, this, "")
private constructor(alias: Name, aliased: Table<UserScoresRecord>?): this(alias, null, null, aliased, null)
private constructor(alias: Name, aliased: Table<UserScoresRecord>?, parameters: Array<Field<*>?>?): this(alias, null, null, aliased, parameters)
/**
* Create an aliased <code>public.user_scores</code> table reference
*/
constructor(alias: String): this(DSL.name(alias))
/**
* Create an aliased <code>public.user_scores</code> table reference
*/
constructor(alias: Name): this(alias, null)
/**
* Create a <code>public.user_scores</code> table reference
*/
constructor(): this(DSL.name("user_scores"), null)
constructor(child: Table<out Record>, key: ForeignKey<out Record, UserScoresRecord>): this(Internal.createPathAlias(child, key), child, key, USER_SCORES, null)
override fun getSchema(): Schema? = if (aliased()) null else Public.PUBLIC
override fun getChecks(): List<Check<UserScoresRecord>> = listOf(
Internal.createCheck(this, DSL.name("life_bar_graph_check"), "((octet_length(life_bar_graph) <= 524288))", true),
Internal.createCheck(this, DSL.name("replay_data_check"), "((octet_length(replay_data) <= 524288))", true)
)
override fun `as`(alias: String): UserScores = UserScores(DSL.name(alias), this)
override fun `as`(alias: Name): UserScores = UserScores(alias, this)
override fun `as`(alias: Table<*>): UserScores = UserScores(alias.getQualifiedName(), this)
/**
* Rename this table
*/
override fun rename(name: String): UserScores = UserScores(DSL.name(name), null)
/**
* Rename this table
*/
override fun rename(name: Name): UserScores = UserScores(name, null)
/**
* Rename this table
*/
override fun rename(name: Table<*>): UserScores = UserScores(name.getQualifiedName(), null)
}

View File

@ -0,0 +1,272 @@
/*
* This file is generated by jOOQ.
*/
package com.nisemoe.generated.tables.records
import com.nisemoe.generated.tables.UserScores
import java.time.OffsetDateTime
import java.util.UUID
import org.jooq.impl.TableRecordImpl
/**
* This class is generated by jOOQ.
*/
@Suppress("UNCHECKED_CAST")
open class UserScoresRecord private constructor() : TableRecordImpl<UserScoresRecord>(UserScores.USER_SCORES) {
open var id: UUID?
set(value): Unit = set(0, value)
get(): UUID? = get(0) as UUID?
open var addedAt: OffsetDateTime?
set(value): Unit = set(1, value)
get(): OffsetDateTime? = get(1) as OffsetDateTime?
open var beatmapId: Int?
set(value): Unit = set(2, value)
get(): Int? = get(2) as Int?
open var beatmapHash: String?
set(value): Unit = set(3, value)
get(): String? = get(3) as String?
open var gameMode: Int?
set(value): Unit = set(4, value)
get(): Int? = get(4) as Int?
open var gameVersion: Int?
set(value): Unit = set(5, value)
get(): Int? = get(5) as Int?
open var playerName: String?
set(value): Unit = set(6, value)
get(): String? = get(6) as String?
open var replayHash: String?
set(value): Unit = set(7, value)
get(): String? = get(7) as String?
open var count_300: Short?
set(value): Unit = set(8, value)
get(): Short? = get(8) as Short?
open var count_100: Short?
set(value): Unit = set(9, value)
get(): Short? = get(9) as Short?
open var count_50: Short?
set(value): Unit = set(10, value)
get(): Short? = get(10) as Short?
open var countGeki: Short?
set(value): Unit = set(11, value)
get(): Short? = get(11) as Short?
open var countKatu: Short?
set(value): Unit = set(12, value)
get(): Short? = get(12) as Short?
open var countMiss: Short?
set(value): Unit = set(13, value)
get(): Short? = get(13) as Short?
open var totalScore: Int?
set(value): Unit = set(14, value)
get(): Int? = get(14) as Int?
open var maxCombo: Short?
set(value): Unit = set(15, value)
get(): Short? = get(15) as Short?
open var perfect: Boolean?
set(value): Unit = set(16, value)
get(): Boolean? = get(16) as Boolean?
open var mods: Int?
set(value): Unit = set(17, value)
get(): Int? = get(17) as Int?
open var lifeBarGraph: String?
set(value): Unit = set(18, value)
get(): String? = get(18) as String?
open var timestamp: Long?
set(value): Unit = set(19, value)
get(): Long? = get(19) as Long?
open var replayLength: Int?
set(value): Unit = set(20, value)
get(): Int? = get(20) as Int?
open var replayData: String?
set(value): Unit = set(21, value)
get(): String? = get(21) as String?
open var onlineScoreId: Long?
set(value): Unit = set(22, value)
get(): Long? = get(22) as Long?
open var additionalModInfo: Double?
set(value): Unit = set(23, value)
get(): Double? = get(23) as Double?
open var ur: Double?
set(value): Unit = set(24, value)
get(): Double? = get(24) as Double?
open var frametime: Double?
set(value): Unit = set(25, value)
get(): Double? = get(25) as Double?
open var edgeHits: Int?
set(value): Unit = set(26, value)
get(): Int? = get(26) as Int?
open var snaps: Int?
set(value): Unit = set(27, value)
get(): Int? = get(27) as Int?
open var adjustedUr: Double?
set(value): Unit = set(28, value)
get(): Double? = get(28) as Double?
open var meanError: Double?
set(value): Unit = set(29, value)
get(): Double? = get(29) as Double?
open var errorVariance: Double?
set(value): Unit = set(30, value)
get(): Double? = get(30) as Double?
open var errorStandardDeviation: Double?
set(value): Unit = set(31, value)
get(): Double? = get(31) as Double?
open var minimumError: Double?
set(value): Unit = set(32, value)
get(): Double? = get(32) as Double?
open var maximumError: Double?
set(value): Unit = set(33, value)
get(): Double? = get(33) as Double?
open var errorRange: Double?
set(value): Unit = set(34, value)
get(): Double? = get(34) as Double?
open var errorCoefficientOfVariation: Double?
set(value): Unit = set(35, value)
get(): Double? = get(35) as Double?
open var errorKurtosis: Double?
set(value): Unit = set(36, value)
get(): Double? = get(36) as Double?
open var errorSkewness: Double?
set(value): Unit = set(37, value)
get(): Double? = get(37) as Double?
open var keypressesTimes: Array<Double?>?
set(value): Unit = set(38, value)
get(): Array<Double?>? = get(38) as Array<Double?>?
open var keypressesMedian: Double?
set(value): Unit = set(39, value)
get(): Double? = get(39) as Double?
open var keypressesStandardDeviation: Double?
set(value): Unit = set(40, value)
get(): Double? = get(40) as Double?
open var sliderendReleaseTimes: Array<Double?>?
set(value): Unit = set(41, value)
get(): Array<Double?>? = get(41) as Array<Double?>?
open var sliderendReleaseMedian: Double?
set(value): Unit = set(42, value)
get(): Double? = get(42) as Double?
open var sliderendReleaseStandardDeviation: Double?
set(value): Unit = set(43, value)
get(): Double? = get(43) as Double?
open var keypressesMedianAdjusted: Double?
set(value): Unit = set(44, value)
get(): Double? = get(44) as Double?
open var keypressesStandardDeviationAdjusted: Double?
set(value): Unit = set(45, value)
get(): Double? = get(45) as Double?
open var sliderendReleaseMedianAdjusted: Double?
set(value): Unit = set(46, value)
get(): Double? = get(46) as Double?
open var sliderendReleaseStandardDeviationAdjusted: Double?
set(value): Unit = set(47, value)
get(): Double? = get(47) as Double?
open var judgements: ByteArray?
set(value): Unit = set(48, value)
get(): ByteArray? = get(48) as ByteArray?
/**
* Create a detached, initialised UserScoresRecord
*/
constructor(id: UUID? = null, addedAt: OffsetDateTime? = null, beatmapId: Int? = null, beatmapHash: String? = null, gameMode: Int? = null, gameVersion: Int? = null, playerName: String? = null, replayHash: String? = null, count_300: Short? = null, count_100: Short? = null, count_50: Short? = null, countGeki: Short? = null, countKatu: Short? = null, countMiss: Short? = null, totalScore: Int? = null, maxCombo: Short? = null, perfect: Boolean? = null, mods: Int? = null, lifeBarGraph: String? = null, timestamp: Long? = null, replayLength: Int? = null, replayData: String? = null, onlineScoreId: Long? = null, additionalModInfo: Double? = null, ur: Double? = null, frametime: Double? = null, edgeHits: Int? = null, snaps: Int? = 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, 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.addedAt = addedAt
this.beatmapId = beatmapId
this.beatmapHash = beatmapHash
this.gameMode = gameMode
this.gameVersion = gameVersion
this.playerName = playerName
this.replayHash = replayHash
this.count_300 = count_300
this.count_100 = count_100
this.count_50 = count_50
this.countGeki = countGeki
this.countKatu = countKatu
this.countMiss = countMiss
this.totalScore = totalScore
this.maxCombo = maxCombo
this.perfect = perfect
this.mods = mods
this.lifeBarGraph = lifeBarGraph
this.timestamp = timestamp
this.replayLength = replayLength
this.replayData = replayData
this.onlineScoreId = onlineScoreId
this.additionalModInfo = additionalModInfo
this.ur = ur
this.frametime = frametime
this.edgeHits = edgeHits
this.snaps = snaps
this.adjustedUr = adjustedUr
this.meanError = meanError
this.errorVariance = errorVariance
this.errorStandardDeviation = errorStandardDeviation
this.minimumError = minimumError
this.maximumError = maximumError
this.errorRange = errorRange
this.errorCoefficientOfVariation = errorCoefficientOfVariation
this.errorKurtosis = errorKurtosis
this.errorSkewness = errorSkewness
this.keypressesTimes = keypressesTimes
this.keypressesMedian = keypressesMedian
this.keypressesStandardDeviation = keypressesStandardDeviation
this.sliderendReleaseTimes = sliderendReleaseTimes
this.sliderendReleaseMedian = sliderendReleaseMedian
this.sliderendReleaseStandardDeviation = sliderendReleaseStandardDeviation
this.keypressesMedianAdjusted = keypressesMedianAdjusted
this.keypressesStandardDeviationAdjusted = keypressesStandardDeviationAdjusted
this.sliderendReleaseMedianAdjusted = sliderendReleaseMedianAdjusted
this.sliderendReleaseStandardDeviationAdjusted = sliderendReleaseStandardDeviationAdjusted
this.judgements = judgements
resetChangedOnNotNull()
}
}

View File

@ -12,6 +12,7 @@ import com.nisemoe.generated.tables.Scores
import com.nisemoe.generated.tables.ScoresJudgements import com.nisemoe.generated.tables.ScoresJudgements
import com.nisemoe.generated.tables.ScoresSimilarity import com.nisemoe.generated.tables.ScoresSimilarity
import com.nisemoe.generated.tables.UpdateUserQueue import com.nisemoe.generated.tables.UpdateUserQueue
import com.nisemoe.generated.tables.UserScores
import com.nisemoe.generated.tables.Users import com.nisemoe.generated.tables.Users
@ -56,6 +57,11 @@ val SCORES_SIMILARITY: ScoresSimilarity = ScoresSimilarity.SCORES_SIMILARITY
*/ */
val UPDATE_USER_QUEUE: UpdateUserQueue = UpdateUserQueue.UPDATE_USER_QUEUE val UPDATE_USER_QUEUE: UpdateUserQueue = UpdateUserQueue.UPDATE_USER_QUEUE
/**
* The table <code>public.user_scores</code>.
*/
val USER_SCORES: UserScores = UserScores.USER_SCORES
/** /**
* The table <code>public.users</code>. * The table <code>public.users</code>.
*/ */

View File

@ -0,0 +1,122 @@
package com.nisemoe.nise.controller
import com.nisemoe.generated.tables.references.SCORES
import com.nisemoe.generated.tables.references.USER_SCORES
import com.nisemoe.nise.database.BeatmapService
import com.nisemoe.nise.integrations.CircleguardService
import com.nisemoe.nise.osu.OsuApi
import com.nisemoe.nise.osu.OsuReplay
import com.nisemoe.nise.service.CompressJudgements
import org.jooq.DSLContext
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
import java.util.UUID
@RestController
class UploadReplayController(
private val dslContext: DSLContext,
private val beatmapService: BeatmapService,
private val compressJudgements: CompressJudgements,
private val circleguardService: CircleguardService,
private val osuApi: OsuApi
) {
private val maxFileSize: Long = 4 * 1024 * 1024 // ~4MB
data class AnalyzeReplayResult(
val id: String
)
@PostMapping("analyze")
fun analyzeReplay(@RequestParam("replay") replayFile: MultipartFile): ResponseEntity<AnalyzeReplayResult> {
// Basic pre-flights checks
if (replayFile.size > maxFileSize) {
return ResponseEntity.badRequest().build()
}
// Create an OsuReplay instance and decode the file
val replay = OsuReplay(replayFile.bytes)
if(replay.gameMode != 0 || replay.beatmapHash.isNullOrBlank()) {
return ResponseEntity.badRequest().build()
}
// Fetch the beatmap id
val beatmapId = this.osuApi.getBeatmapIdFromHash(replay.beatmapHash!!)
?: return ResponseEntity.badRequest().build()
// TODO: Add beatmap to database if it doesn't exist
val beatmapFile = this.beatmapService.getBeatmapFile(beatmapId)
?: return ResponseEntity.badRequest().build()
// Analyze the replay
val analysis = this.circleguardService.processReplay(
replayData = replay.replayData!!,
beatmapData = beatmapFile,
mods = replay.modsUsed
).get()
// TODO: Compare with all other replays in the same beatmap
val newUserReplayId = dslContext.insertInto(USER_SCORES)
.set(USER_SCORES.BEATMAP_ID, beatmapId)
.set(USER_SCORES.BEATMAP_HASH, replay.beatmapHash)
.set(USER_SCORES.GAME_MODE, replay.gameMode)
.set(USER_SCORES.GAME_VERSION, replay.gameVersion)
.set(USER_SCORES.PLAYER_NAME, replay.playerName)
.set(USER_SCORES.REPLAY_HASH, replay.replayHash)
.set(USER_SCORES.COUNT_300, replay.numberOf300s)
.set(USER_SCORES.COUNT_100, replay.numberOf100s)
.set(USER_SCORES.COUNT_50, replay.numberOf50s)
.set(USER_SCORES.COUNT_GEKI, replay.numberOfGekis)
.set(USER_SCORES.COUNT_KATU, replay.numberOfKatus)
.set(USER_SCORES.COUNT_MISS, replay.numberOfMisses)
.set(USER_SCORES.TOTAL_SCORE, replay.totalScore)
.set(USER_SCORES.MAX_COMBO, replay.greatestCombo)
.set(USER_SCORES.PERFECT, replay.perfectCombo)
.set(USER_SCORES.MODS, replay.modsUsed)
.set(USER_SCORES.LIFE_BAR_GRAPH, replay.lifeBarGraph)
.set(USER_SCORES.TIMESTAMP, replay.timestamp)
.set(USER_SCORES.REPLAY_LENGTH, replay.replayLength)
.set(USER_SCORES.REPLAY_DATA, replay.replayData)
.set(USER_SCORES.ONLINE_SCORE_ID, replay.onlineScoreID)
.set(USER_SCORES.ADDITIONAL_MOD_INFO, replay.additionalModInfo)
.set(USER_SCORES.UR, analysis.ur)
.set(USER_SCORES.FRAMETIME, analysis.frametime)
.set(USER_SCORES.EDGE_HITS, analysis.edge_hits)
.set(USER_SCORES.SNAPS, analysis.snaps)
.set(USER_SCORES.ADJUSTED_UR, analysis.adjusted_ur)
.set(USER_SCORES.MEAN_ERROR, analysis.mean_error)
.set(USER_SCORES.ERROR_VARIANCE, analysis.error_variance)
.set(USER_SCORES.ERROR_STANDARD_DEVIATION, analysis.error_standard_deviation)
.set(USER_SCORES.MINIMUM_ERROR, analysis.minimum_error)
.set(USER_SCORES.MAXIMUM_ERROR, analysis.maximum_error)
.set(USER_SCORES.ERROR_RANGE, analysis.error_range)
.set(USER_SCORES.ERROR_COEFFICIENT_OF_VARIATION, analysis.error_coefficient_of_variation)
.set(USER_SCORES.ERROR_KURTOSIS, analysis.error_kurtosis)
.set(USER_SCORES.ERROR_SKEWNESS, analysis.error_skewness)
.set(USER_SCORES.KEYPRESSES_TIMES, analysis.keypresses_times?.toTypedArray())
.set(USER_SCORES.KEYPRESSES_MEDIAN, analysis.keypresses_median)
.set(USER_SCORES.KEYPRESSES_STANDARD_DEVIATION, analysis.keypresses_standard_deviation)
.set(USER_SCORES.SLIDEREND_RELEASE_TIMES, analysis.sliderend_release_times?.toTypedArray())
.set(USER_SCORES.SLIDEREND_RELEASE_MEDIAN, analysis.sliderend_release_median)
.set(USER_SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION, analysis.sliderend_release_standard_deviation)
.set(USER_SCORES.KEYPRESSES_MEDIAN_ADJUSTED, analysis.keypresses_median_adjusted)
.set(USER_SCORES.KEYPRESSES_STANDARD_DEVIATION_ADJUSTED, analysis.keypresses_standard_deviation_adjusted)
.set(USER_SCORES.SLIDEREND_RELEASE_MEDIAN_ADJUSTED, analysis.sliderend_release_median_adjusted)
.set(USER_SCORES.SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED, analysis.sliderend_release_standard_deviation_adjusted)
.set(SCORES.JUDGEMENTS, compressJudgements.serialize(analysis.judgements))
.returning(USER_SCORES.ID)
.fetchOne()
if(newUserReplayId == null) {
return ResponseEntity.internalServerError().build()
}
return ResponseEntity.ok(AnalyzeReplayResult(newUserReplayId.get(USER_SCORES.ID).toString()))
}
}

View File

@ -1,12 +1,47 @@
package com.nisemoe.nise.database package com.nisemoe.nise.database
import com.nisemoe.generated.tables.references.BEATMAPS
import com.nisemoe.generated.tables.references.SCORES import com.nisemoe.generated.tables.references.SCORES
import com.nisemoe.nise.osu.OsuApi
import org.jooq.DSLContext import org.jooq.DSLContext
import org.jooq.impl.DSL.avg import org.jooq.impl.DSL.avg
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@Service @Service
class BeatmapService(private val dslContext: DSLContext) { class BeatmapService(
private val dslContext: DSLContext,
private val osuApi: OsuApi
) {
private val logger = LoggerFactory.getLogger(javaClass)
fun getBeatmapFile(beatmapId: Int): String? {
// Fetch the beatmap file from database
var beatmapFile = dslContext.select(BEATMAPS.BEATMAP_FILE)
.from(BEATMAPS)
.where(BEATMAPS.BEATMAP_ID.eq(beatmapId))
.fetchOneInto(String::class.java)
if(!beatmapFile.isNullOrBlank()) {
return beatmapFile
}
this.logger.warn("Failed to fetch beatmap file for beatmap_id = $beatmapId from database")
beatmapFile = this.osuApi.getBeatmapFile(beatmapId = beatmapId)
if(beatmapFile == null) {
this.logger.error("Failed to fetch beatmap file for beatmap_id = $beatmapId from osu!api")
return null
} else {
dslContext.update(BEATMAPS)
.set(BEATMAPS.BEATMAP_FILE, beatmapFile)
.where(BEATMAPS.BEATMAP_ID.eq(beatmapId))
.execute()
return beatmapFile
}
}
fun getAverageUR(beatmapId: Int, excludeReplayId: Long): Double? { fun getAverageUR(beatmapId: Int, excludeReplayId: Long): Double? {
val condition = SCORES.BEATMAP_ID.eq(beatmapId) val condition = SCORES.BEATMAP_ID.eq(beatmapId)

View File

@ -118,6 +118,25 @@ class OsuApi(
} }
} }
fun getBeatmapIdFromHash(beatmapHash: String): Int? {
val queryParams = mapOf(
"checksum" to beatmapHash
)
val response = doRequest("https://osu.ppy.sh/api/v2/beatmaps/lookup?", queryParams)
if(response == null) {
this.logger.info("Error loading beatmap")
return null
}
return if (response.statusCode() == 200) {
val beatmap = serializer.decodeFromString(OsuApiModels.BeatmapCompact.serializer(), response.body())
beatmap.id ?: null
} else {
null
}
}
/** /**
* Retrieves the replay data for a given score ID from the osu!api. * Retrieves the replay data for a given score ID from the osu!api.
* Efficiently cycles through the API keys to avoid rate limiting. * Efficiently cycles through the API keys to avoid rate limiting.

View File

@ -0,0 +1,209 @@
package com.nisemoe.nise.osu
import org.apache.commons.compress.compressors.lzma.LZMACompressorInputStream
import org.apache.commons.compress.compressors.lzma.LZMACompressorOutputStream
import java.io.ByteArrayOutputStream
import java.io.DataInputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.charset.StandardCharsets
import java.util.*
class OsuReplay(fileContent: ByteArray) {
companion object {
// ~4mb
private val EXPECTED_FILE_SIZE = 0 .. 4194304
// ~512kb
private const val MAX_STRING_LENGTH = 512000
private val EXPECTED_STRING_LENGTH = 0 .. MAX_STRING_LENGTH
private val EXPECTED_INT_RANGE = Int.MIN_VALUE..Int.MAX_VALUE
private val EXPECTED_LONG_RANGE = Long.MIN_VALUE..Long.MAX_VALUE
private val EXPECTED_DOUBLE_RANGE = Double.MIN_VALUE..Double.MAX_VALUE
}
private val dis = DataInputStream(fileContent.inputStream())
var gameMode: Int = 0
var gameVersion: Int = 0
var beatmapHash: String? = null
var playerName: String? = null
var replayHash: String? = null
var numberOf300s: Short = 0
var numberOf100s: Short = 0
var numberOf50s: Short = 0
var numberOfGekis: Short = 0
var numberOfKatus: Short = 0
var numberOfMisses: Short = 0
var totalScore: Int = 0
var greatestCombo: Short = 0
var perfectCombo: Boolean = false
var modsUsed: Int = 0
var lifeBarGraph: String? = null
var timestamp: Long = 0
var replayLength: Int = 0
var replayData: String? = null
var onlineScoreID: Long = 0
var additionalModInfo: Double = 0.0
init {
if (fileContent.size !in EXPECTED_FILE_SIZE) {
throw SecurityException("File size out of expected bounds")
}
decode()
}
private fun decode() {
try {
gameMode = dis.readByte().toInt()
if(gameMode != 0) {
throw SecurityException("Invalid game mode")
}
gameVersion = readIntLittleEndian()
beatmapHash = dis.readCompressedReplayData()
playerName = dis.readCompressedReplayData()
replayHash = dis.readCompressedReplayData()
numberOf300s = readShortLittleEndian()
numberOf100s = readShortLittleEndian()
numberOf50s = readShortLittleEndian()
numberOfGekis = readShortLittleEndian()
numberOfKatus = readShortLittleEndian()
numberOfMisses = readShortLittleEndian()
totalScore = readIntLittleEndian()
greatestCombo = readShortLittleEndian()
perfectCombo = dis.readByte() != 0.toByte()
modsUsed = readIntLittleEndian()
lifeBarGraph = dis.readCompressedReplayData()
timestamp = readLongLittleEndian()
replayLength = readIntLittleEndian()
replayData = dis.readCompressedReplayData(replayLength)
onlineScoreID = readLongLittleEndian()
if ((modsUsed and (1 shl 24)) != 0) {
additionalModInfo = readDoubleLittleEndian()
}
} catch (e: Exception) {
println("Failed to decode .osr file content: ${e.message}")
}
}
private fun DataInputStream.readCompressedReplayData(): String? {
return when (readByte()) {
0x0b.toByte() -> {
val length = readULEB128()
if (length !in EXPECTED_STRING_LENGTH) {
throw SecurityException("String length out of expected bounds")
}
ByteArray(length.toInt()).also { readFully(it) }.toString(StandardCharsets.UTF_8)
}
else -> null
}
}
private fun DataInputStream.readCompressedReplayData(length: Int): String {
// Read the compressed data
val compressedData = ByteArray(length)
readFully(compressedData)
// Decompress the data
val decompressedStream = LZMACompressorInputStream(compressedData.inputStream())
val decompressedData = decompressedStream.readBytes()
decompressedStream.close()
// Compress the decompressed data
val compressedOutputStream = ByteArrayOutputStream()
val lzmaCompressorOutputStream = LZMACompressorOutputStream(compressedOutputStream)
lzmaCompressorOutputStream.write(decompressedData)
lzmaCompressorOutputStream.close()
// Now encode the re-compressed data to Base64
return Base64.getEncoder().encodeToString(compressedOutputStream.toByteArray())
}
private fun DataInputStream.readULEB128(): Long {
var result = 0L
var shift = 0
var size = 0
do {
if (size == 10) { // Prevent reading more than 10 bytes, the maximum needed for a 64-bit number
throw SecurityException("Invalid LEB128 sequence.")
}
val byte = readByte()
size++
// Check for overflow: If we're on the last byte (10th), it should not have more than 1 bit before the continuation bit
if (size == 10 && byte.toInt() and 0x7F > 1) {
throw SecurityException("LEB128 sequence overflow.")
}
val value = (byte.toInt() and 0x7F)
if (shift >= 63 && value > 0) { // prevent shifting into oblivion
throw SecurityException("LEB128 sequence overflow.")
}
result = result or (value.toLong() shl shift)
shift += 7
} while (byte.toInt() and 0x80 > 0)
return result
}
private fun readShortLittleEndian(): Short {
if (dis.available() < Short.SIZE_BYTES) {
throw SecurityException("Insufficient data available to read short")
}
val bytes = ByteArray(Short.SIZE_BYTES)
dis.readFully(bytes)
return ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).short
}
private fun readIntLittleEndian(): Int {
if (dis.available() < Int.SIZE_BYTES) {
throw SecurityException("Insufficient data available to read int")
}
val bytes = ByteArray(Int.SIZE_BYTES)
dis.readFully(bytes)
val value = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).int
if (value !in EXPECTED_INT_RANGE) {
throw SecurityException("Decoded integer value out of expected bounds")
}
return value
}
private fun readLongLittleEndian(): Long {
if (dis.available() < Long.SIZE_BYTES) {
throw SecurityException("Insufficient data available to read long")
}
val bytes = ByteArray(Long.SIZE_BYTES)
dis.readFully(bytes)
val value = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).long
if (value !in EXPECTED_LONG_RANGE) {
throw SecurityException("Decoded long value out of expected bounds")
}
return value
}
private fun readDoubleLittleEndian(): Double {
if (dis.available() < Double.SIZE_BYTES) {
throw SecurityException("Insufficient data available to read double")
}
val bytes = ByteArray(Double.SIZE_BYTES)
dis.readFully(bytes)
val value = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).double
if (value !in EXPECTED_DOUBLE_RANGE) {
throw SecurityException("Decoded double value out of expected bounds")
}
return value
}
}

View File

@ -0,0 +1,56 @@
CREATE TABLE public.user_scores
(
id uuid not null default gen_random_uuid(),
added_at timestamptz DEFAULT CURRENT_TIMESTAMP NULL,
beatmap_id int4 NULL,
beatmap_hash varchar(32) NULL,
game_mode int4 NULL,
game_version int4 NULL,
player_name varchar(256) NULL,
replay_hash varchar(32) NULL,
count_300 int2 NULL,
count_100 int2 NULL,
count_50 int2 NULL,
count_geki int2 NULL,
count_katu int2 NULL,
count_miss int2 NULL,
total_score int4 NULL,
max_combo int2 NULL,
perfect bool NULL,
mods int4 NULL,
life_bar_graph text NULL,
timestamp int8 NULL,
replay_length int4 NULL,
replay_data text NULL,
online_score_id int8 NULL,
additional_mod_info float8 NULL,
ur float8 NULL,
frametime float8 NULL,
edge_hits int4 NULL,
snaps int4 NULL,
adjusted_ur float8 NULL,
mean_error float8 NULL,
error_variance float8 NULL,
error_standard_deviation float8 NULL,
minimum_error float8 NULL,
maximum_error float8 NULL,
error_range float8 NULL,
error_coefficient_of_variation float8 NULL,
error_kurtosis float8 NULL,
error_skewness float8 NULL,
keypresses_times float8[] NULL,
keypresses_median float8 NULL,
keypresses_standard_deviation float8 NULL,
sliderend_release_times float8[] NULL,
sliderend_release_median float8 NULL,
sliderend_release_standard_deviation float8 NULL,
keypresses_median_adjusted float8 NULL,
keypresses_standard_deviation_adjusted float8 NULL,
sliderend_release_median_adjusted float8 NULL,
sliderend_release_standard_deviation_adjusted float8 NULL,
judgements bytea NULL,
CONSTRAINT life_bar_graph_check CHECK (OCTET_LENGTH(life_bar_graph) <= 524288),
CONSTRAINT replay_data_check CHECK (OCTET_LENGTH(replay_data) <= 524288)
);

View File

@ -0,0 +1,23 @@
package com.nisemoe.nise.osu
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import java.io.FileInputStream
class OsuReplayTest {
@Test
fun testDecode() {
// Read the .osr file into a ByteArray
val filePath = "replay1.osr"
val fileByteArray = FileInputStream(filePath).readBytes()
// Create an OsuReplay instance and decode the file
val replay = OsuReplay(fileByteArray)
// Assert the decoded properties
assertEquals(0, replay.gameMode)
assertEquals("jup1terrosu", replay.playerName)
}
}

View File

@ -4,11 +4,15 @@
box-sizing: border-box; /* Includes padding and border in the element's total width and height */ box-sizing: border-box; /* Includes padding and border in the element's total width and height */
} }
.main .term:nth-child(1) { .main .subcontainer:nth-child(1) {
width: 70%; width: 70%;
margin-right: 10px; margin-right: 10px;
} }
.main .term:nth-child(2) { .main .subcontainer:nth-child(2) {
width: 30%; width: 30%;
} }
.subcontainer .term {
width: fit-content;
}

View File

@ -4,64 +4,77 @@
<div class="main container"> <div class="main container">
<div class="term"> <div class="subcontainer">
<h1># Welcome to [nise.moe]</h1> <div class="term">
<h3>wtf is this?</h3> <h1># Welcome to [nise.moe]</h1>
<p>This application will automatically crawl [osu!std] top scores and search for stolen replays or obvious relax/timewarp scores.</p> <h3>wtf is this?</h3>
<p>It started collecting replays on <i>2024-01-12</i></p> <p>This application will automatically crawl [osu!std] top scores and search for stolen replays or obvious relax/timewarp scores.</p>
<p>This website is not affiliated with the osu! game nor ppy. It is an unrelated, unaffiliated, 3rd party project.</p> <p>It started collecting replays on <i>2024-01-12</i></p>
<p>If you have any suggestions or want to report bugs, feel free to join the Discord server below.</p> <p>This website is not affiliated with the osu! game nor ppy. It is an unrelated, unaffiliated, 3rd party project.</p>
<div class="text-center mt-4"> <p>If you have any suggestions or want to report bugs, feel free to join the Discord server below.</p>
<a href="https://discord.gg/wn4gWpA36w" target="_blank" class="btn">Join the Discord!</a> <div class="text-center mt-4">
</div> <a href="https://discord.gg/wn4gWpA36w" target="_blank" class="btn">Join the Discord!</a>
<h3 class="mt-4"># do you use rss? (nerd)</h3> </div>
<p>you can keep up with newly detected scores with the rss feed, subscribe to it using your favorite reader.</p> <h3 class="mt-4"># do you use rss? (nerd)</h3>
<div class="text-center"> <p>you can keep up with newly detected scores with the rss feed, subscribe to it using your favorite reader.</p>
<a href="https://nise.moe/api/rss.xml" target="_blank"> <div class="text-center">
<img title="rss-chan!" src="/assets/rss.png" width="64" style="filter: grayscale(40%) sepia(10%) brightness(90%);"> <a href="https://nise.moe/api/rss.xml" target="_blank">
<br> <img title="rss-chan!" src="/assets/rss.png" width="64" style="filter: grayscale(40%) sepia(10%) brightness(90%);">
<span style="padding: 2px; border: 1px dotted #b3b8c3;"> <br>
<span style="padding: 2px; border: 1px dotted #b3b8c3;">
Get the feed Get the feed
</span> </span>
</a> </a>
</div>
</div> </div>
</div> </div>
<div class="term"> <div class="subcontainer">
<div class="text-center" style="font-weight: bold; font-size: 14px; padding-bottom: 10px">
<img src="/assets/new.png" width="48" style=" filter: grayscale(30%) sepia(20%) brightness(90%);">
<br>
<ng-container *ngIf="this.wantsConnection">new <span style="margin-right: 4px" class="text-has-info " title="this feed includes any score - this does not mean they're cheating.">scores</span> <span style="color: rgba(36,255,114,0.65); filter: grayscale(40%)">[live]</span></ng-container>
<ng-container *ngIf="!this.wantsConnection">new <span style="margin-right: 4px" class="text-has-info " title="this feed includes any score - this does not mean they're cheating.">scores</span> <span style="color: rgba(234,64,29,0.92); filter: grayscale(40%)">[disconnected]</span></ng-container>
<br>
<button (click)="this.toggleConnection()" style="margin-top: 4px;">
<ng-container *ngIf="this.wantsConnection">Disconnect</ng-container>
<ng-container *ngIf="!this.wantsConnection">Connect</ng-container>
</button>
</div>
<ng-container *ngIf="this.liveScores.length <= 0">
<p class="text-center text-muted">
nothing yet...<br>
new scores will appear here.
</p>
</ng-container>
<div style="overflow-y: scroll; max-height: 565px">
<ng-container *ngFor="let replay of this.liveScores">
<div style="margin-bottom: 6px">
<a [routerLink]="['/s/' + replay.replay_id]">
<div style="border: 1px dotted rgba(204,204,204,0.4); padding: 4px; margin-right: 16px; font-size: 13px">
<ul style="list-style: none; padding-left: 10px;">
<li>User: {{ replay.username }}</li>
<li>Date: {{ replay.date }}</li>
<li>PP: {{ replay.pp | number: '1.0-0' }}</li>
<li>cvUR: {{ replay.ur | number: '1.2-2' }}</li>
</ul>
</div>
</a>
</div>
</ng-container>
</div>
<input type="file" id="fileUpload" style="display: none" (change)="this.uploadReplay($event)"
accept=".osr">
<label for="fileUpload" class="btn pointer text-center" style="border: 2px solid #CCCCCC; display: block; margin-bottom: 20px; padding: 10px; color: white">
Upload Replay!
</label>
<div class="term">
<div class="text-center" style="font-weight: bold; font-size: 14px; padding-bottom: 10px">
<img src="/assets/new.png" width="48" style=" filter: grayscale(30%) sepia(20%) brightness(90%);">
<br>
<ng-container *ngIf="this.wantsConnection">new <span style="margin-right: 4px" class="text-has-info " title="this feed includes any score - this does not mean they're cheating.">scores</span> <span style="color: rgba(36,255,114,0.65); filter: grayscale(40%)">[live]</span></ng-container>
<ng-container *ngIf="!this.wantsConnection">new <span style="margin-right: 4px" class="text-has-info " title="this feed includes any score - this does not mean they're cheating.">scores</span> <span style="color: rgba(234,64,29,0.92); filter: grayscale(40%)">[disconnected]</span></ng-container>
<br>
<button (click)="this.toggleConnection()" style="margin-top: 4px;">
<ng-container *ngIf="this.wantsConnection">Disconnect</ng-container>
<ng-container *ngIf="!this.wantsConnection">Connect</ng-container>
</button>
</div>
<ng-container *ngIf="this.liveScores.length <= 0">
<p class="text-center text-muted">
nothing yet...<br>
new scores will appear here.
</p>
</ng-container>
<div style="overflow-y: scroll; max-height: 565px">
<ng-container *ngFor="let replay of this.liveScores">
<div style="margin-bottom: 6px">
<a [routerLink]="['/s/' + replay.replay_id]">
<div style="border: 1px dotted rgba(204,204,204,0.4); padding: 4px; margin-right: 16px; font-size: 13px">
<ul style="list-style: none; padding-left: 10px;">
<li>User: {{ replay.username }}</li>
<li>Date: {{ replay.date }}</li>
<li>PP: {{ replay.pp | number: '1.0-0' }}</li>
<li>cvUR: {{ replay.ur | number: '1.2-2' }}</li>
</ul>
</div>
</a>
</div>
</ng-container>
</div>
</div>
</div> </div>

View File

@ -6,7 +6,8 @@ import {RxStompService} from "../../corelib/stomp/stomp.service";
import {Message} from "@stomp/stompjs/esm6"; import {Message} from "@stomp/stompjs/esm6";
import {ReplayData} from "../replays"; import {ReplayData} from "../replays";
import {DecimalPipe, NgForOf, NgIf} from "@angular/common"; import {DecimalPipe, NgForOf, NgIf} from "@angular/common";
import {RouterLink} from "@angular/router"; import {Router, RouterLink} from "@angular/router";
import {HttpClient} from "@angular/common/http";
interface Statistics { interface Statistics {
total_beatmaps: number; total_beatmaps: number;
@ -16,6 +17,10 @@ interface Statistics {
total_replay_similarity: number; total_replay_similarity: number;
} }
interface AnalyzeReplayResponse {
id: string;
}
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
standalone: true, standalone: true,
@ -36,8 +41,12 @@ export class HomeComponent implements OnInit, OnDestroy {
statistics: Statistics | null = null; statistics: Statistics | null = null;
wantsConnection: boolean = true; wantsConnection: boolean = true;
loading = false;
constructor( constructor(
private localCacheService: LocalCacheService, private localCacheService: LocalCacheService,
private router: Router,
private httpClient: HttpClient,
private rxStompService: RxStompService, private rxStompService: RxStompService,
) { } ) { }
@ -92,4 +101,27 @@ export class HomeComponent implements OnInit, OnDestroy {
); );
} }
uploadReplay(event: any) {
if (event.target.files.length <= 0) {
return;
}
this.loading = true;
const file: File = event.target.files[0];
const formData = new FormData();
formData.append('replay', file);
this.httpClient.post<AnalyzeReplayResponse>(`${environment.apiUrl}/analyze`, formData).subscribe({
next: (response) => {
this.loading = false;
this.router.navigate(['/c/' + response.id ]);
},
error: (error) => {
this.loading = false;
},
});
}
} }

View File

@ -59,7 +59,7 @@ html {
@media screen and (min-width: 768px) { @media screen and (min-width: 768px) {
.main { .main {
width: 820px !important; width: 850px !important;
} }
.header { .header {