From 48cf50d4485412165de6a3319701915f0d47ce65 Mon Sep 17 00:00:00 2001 From: "nise.moe" Date: Mon, 4 Mar 2024 20:34:21 +0100 Subject: [PATCH] Basic work on replay upload --- nise-backend/replay1.osr | Bin 0 -> 19437 bytes .../kotlin/com/nisemoe/generated/Public.kt | 7 + .../nisemoe/generated/tables/UserScores.kt | 355 ++++++++++++++++++ .../tables/records/UserScoresRecord.kt | 272 ++++++++++++++ .../generated/tables/references/Tables.kt | 6 + .../nise/controller/UploadReplayController.kt | 122 ++++++ .../nisemoe/nise/database/BeatmapService.kt | 37 +- .../kotlin/com/nisemoe/nise/osu/OsuApi.kt | 19 + .../kotlin/com/nisemoe/nise/osu/OsuReplay.kt | 209 +++++++++++ .../V0.0.1.027__create_user_scores.sql | 56 +++ .../com/nisemoe/nise/osu/OsuReplayTest.kt | 23 ++ nise-frontend/src/app/home/home.component.css | 8 +- .../src/app/home/home.component.html | 117 +++--- nise-frontend/src/app/home/home.component.ts | 34 +- nise-frontend/src/assets/style.css | 2 +- 15 files changed, 1210 insertions(+), 57 deletions(-) create mode 100644 nise-backend/replay1.osr create mode 100644 nise-backend/src/main/kotlin/com/nisemoe/generated/tables/UserScores.kt create mode 100644 nise-backend/src/main/kotlin/com/nisemoe/generated/tables/records/UserScoresRecord.kt create mode 100644 nise-backend/src/main/kotlin/com/nisemoe/nise/controller/UploadReplayController.kt create mode 100644 nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuReplay.kt create mode 100644 nise-backend/src/main/resources/db/migration/V0.0.1.027__create_user_scores.sql create mode 100644 nise-backend/src/test/kotlin/com/nisemoe/nise/osu/OsuReplayTest.kt diff --git a/nise-backend/replay1.osr b/nise-backend/replay1.osr new file mode 100644 index 0000000000000000000000000000000000000000..e830ce4e7c93d8f459258a32f6030f9721c6314c GIT binary patch literal 19437 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 z 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
C6n>l4j&)v!CUL? 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.user_scores. + */ + val USER_SCORES: UserScores get() = UserScores.USER_SCORES + /** * The table public.users. */ @@ -114,6 +120,7 @@ open class Public : SchemaImpl("public", DefaultCatalog.DEFAULT_CATALOG) { ScoresJudgements.SCORES_JUDGEMENTS, ScoresSimilarity.SCORES_SIMILARITY, UpdateUserQueue.UPDATE_USER_QUEUE, + UserScores.USER_SCORES, Users.USERS ) } diff --git a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/UserScores.kt b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/UserScores.kt new file mode 100644 index 0000000..06a421f --- /dev/null +++ b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/UserScores.kt @@ -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?, + path: ForeignKey?, + aliased: Table?, + parameters: Array?>? +): TableImpl( + alias, + Public.PUBLIC, + child, + path, + aliased, + parameters, + DSL.comment(""), + TableOptions.table() +) { + companion object { + + /** + * The reference instance of public.user_scores + */ + val USER_SCORES: UserScores = UserScores() + } + + /** + * The class holding records for this type + */ + override fun getRecordType(): Class = UserScoresRecord::class.java + + /** + * The column public.user_scores.id. + */ + val ID: TableField = createField(DSL.name("id"), SQLDataType.UUID.nullable(false).defaultValue(DSL.field(DSL.raw("gen_random_uuid()"), SQLDataType.UUID)), this, "") + + /** + * The column public.user_scores.added_at. + */ + val ADDED_AT: TableField = createField(DSL.name("added_at"), SQLDataType.TIMESTAMPWITHTIMEZONE(6).defaultValue(DSL.field(DSL.raw("CURRENT_TIMESTAMP"), SQLDataType.TIMESTAMPWITHTIMEZONE)), this, "") + + /** + * The column public.user_scores.beatmap_id. + */ + val BEATMAP_ID: TableField = createField(DSL.name("beatmap_id"), SQLDataType.INTEGER, this, "") + + /** + * The column public.user_scores.beatmap_hash. + */ + val BEATMAP_HASH: TableField = createField(DSL.name("beatmap_hash"), SQLDataType.VARCHAR(32), this, "") + + /** + * The column public.user_scores.game_mode. + */ + val GAME_MODE: TableField = createField(DSL.name("game_mode"), SQLDataType.INTEGER, this, "") + + /** + * The column public.user_scores.game_version. + */ + val GAME_VERSION: TableField = createField(DSL.name("game_version"), SQLDataType.INTEGER, this, "") + + /** + * The column public.user_scores.player_name. + */ + val PLAYER_NAME: TableField = createField(DSL.name("player_name"), SQLDataType.VARCHAR(256), this, "") + + /** + * The column public.user_scores.replay_hash. + */ + val REPLAY_HASH: TableField = createField(DSL.name("replay_hash"), SQLDataType.VARCHAR(32), this, "") + + /** + * The column public.user_scores.count_300. + */ + val COUNT_300: TableField = createField(DSL.name("count_300"), SQLDataType.SMALLINT, this, "") + + /** + * The column public.user_scores.count_100. + */ + val COUNT_100: TableField = createField(DSL.name("count_100"), SQLDataType.SMALLINT, this, "") + + /** + * The column public.user_scores.count_50. + */ + val COUNT_50: TableField = createField(DSL.name("count_50"), SQLDataType.SMALLINT, this, "") + + /** + * The column public.user_scores.count_geki. + */ + val COUNT_GEKI: TableField = createField(DSL.name("count_geki"), SQLDataType.SMALLINT, this, "") + + /** + * The column public.user_scores.count_katu. + */ + val COUNT_KATU: TableField = createField(DSL.name("count_katu"), SQLDataType.SMALLINT, this, "") + + /** + * The column public.user_scores.count_miss. + */ + val COUNT_MISS: TableField = createField(DSL.name("count_miss"), SQLDataType.SMALLINT, this, "") + + /** + * The column public.user_scores.total_score. + */ + val TOTAL_SCORE: TableField = createField(DSL.name("total_score"), SQLDataType.INTEGER, this, "") + + /** + * The column public.user_scores.max_combo. + */ + val MAX_COMBO: TableField = createField(DSL.name("max_combo"), SQLDataType.SMALLINT, this, "") + + /** + * The column public.user_scores.perfect. + */ + val PERFECT: TableField = createField(DSL.name("perfect"), SQLDataType.BOOLEAN, this, "") + + /** + * The column public.user_scores.mods. + */ + val MODS: TableField = createField(DSL.name("mods"), SQLDataType.INTEGER, this, "") + + /** + * The column public.user_scores.life_bar_graph. + */ + val LIFE_BAR_GRAPH: TableField = createField(DSL.name("life_bar_graph"), SQLDataType.CLOB, this, "") + + /** + * The column public.user_scores.timestamp. + */ + val TIMESTAMP: TableField = createField(DSL.name("timestamp"), SQLDataType.BIGINT, this, "") + + /** + * The column public.user_scores.replay_length. + */ + val REPLAY_LENGTH: TableField = createField(DSL.name("replay_length"), SQLDataType.INTEGER, this, "") + + /** + * The column public.user_scores.replay_data. + */ + val REPLAY_DATA: TableField = createField(DSL.name("replay_data"), SQLDataType.CLOB, this, "") + + /** + * The column public.user_scores.online_score_id. + */ + val ONLINE_SCORE_ID: TableField = createField(DSL.name("online_score_id"), SQLDataType.BIGINT, this, "") + + /** + * The column public.user_scores.additional_mod_info. + */ + val ADDITIONAL_MOD_INFO: TableField = createField(DSL.name("additional_mod_info"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.ur. + */ + val UR: TableField = createField(DSL.name("ur"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.frametime. + */ + val FRAMETIME: TableField = createField(DSL.name("frametime"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.edge_hits. + */ + val EDGE_HITS: TableField = createField(DSL.name("edge_hits"), SQLDataType.INTEGER, this, "") + + /** + * The column public.user_scores.snaps. + */ + val SNAPS: TableField = createField(DSL.name("snaps"), SQLDataType.INTEGER, this, "") + + /** + * The column public.user_scores.adjusted_ur. + */ + val ADJUSTED_UR: TableField = createField(DSL.name("adjusted_ur"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.mean_error. + */ + val MEAN_ERROR: TableField = createField(DSL.name("mean_error"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.error_variance. + */ + val ERROR_VARIANCE: TableField = createField(DSL.name("error_variance"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.error_standard_deviation. + */ + val ERROR_STANDARD_DEVIATION: TableField = createField(DSL.name("error_standard_deviation"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.minimum_error. + */ + val MINIMUM_ERROR: TableField = createField(DSL.name("minimum_error"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.maximum_error. + */ + val MAXIMUM_ERROR: TableField = createField(DSL.name("maximum_error"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.error_range. + */ + val ERROR_RANGE: TableField = createField(DSL.name("error_range"), SQLDataType.DOUBLE, this, "") + + /** + * The column + * public.user_scores.error_coefficient_of_variation. + */ + val ERROR_COEFFICIENT_OF_VARIATION: TableField = createField(DSL.name("error_coefficient_of_variation"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.error_kurtosis. + */ + val ERROR_KURTOSIS: TableField = createField(DSL.name("error_kurtosis"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.error_skewness. + */ + val ERROR_SKEWNESS: TableField = createField(DSL.name("error_skewness"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.keypresses_times. + */ + val KEYPRESSES_TIMES: TableField?> = createField(DSL.name("keypresses_times"), SQLDataType.FLOAT.array(), this, "") + + /** + * The column public.user_scores.keypresses_median. + */ + val KEYPRESSES_MEDIAN: TableField = createField(DSL.name("keypresses_median"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.keypresses_standard_deviation. + */ + val KEYPRESSES_STANDARD_DEVIATION: TableField = createField(DSL.name("keypresses_standard_deviation"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.sliderend_release_times. + */ + val SLIDEREND_RELEASE_TIMES: TableField?> = createField(DSL.name("sliderend_release_times"), SQLDataType.FLOAT.array(), this, "") + + /** + * The column public.user_scores.sliderend_release_median. + */ + val SLIDEREND_RELEASE_MEDIAN: TableField = createField(DSL.name("sliderend_release_median"), SQLDataType.DOUBLE, this, "") + + /** + * The column + * public.user_scores.sliderend_release_standard_deviation. + */ + val SLIDEREND_RELEASE_STANDARD_DEVIATION: TableField = createField(DSL.name("sliderend_release_standard_deviation"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.keypresses_median_adjusted. + */ + val KEYPRESSES_MEDIAN_ADJUSTED: TableField = createField(DSL.name("keypresses_median_adjusted"), SQLDataType.DOUBLE, this, "") + + /** + * The column + * public.user_scores.keypresses_standard_deviation_adjusted. + */ + val KEYPRESSES_STANDARD_DEVIATION_ADJUSTED: TableField = createField(DSL.name("keypresses_standard_deviation_adjusted"), SQLDataType.DOUBLE, this, "") + + /** + * The column + * public.user_scores.sliderend_release_median_adjusted. + */ + val SLIDEREND_RELEASE_MEDIAN_ADJUSTED: TableField = createField(DSL.name("sliderend_release_median_adjusted"), SQLDataType.DOUBLE, this, "") + + /** + * The column + * public.user_scores.sliderend_release_standard_deviation_adjusted. + */ + val SLIDEREND_RELEASE_STANDARD_DEVIATION_ADJUSTED: TableField = createField(DSL.name("sliderend_release_standard_deviation_adjusted"), SQLDataType.DOUBLE, this, "") + + /** + * The column public.user_scores.judgements. + */ + val JUDGEMENTS: TableField = createField(DSL.name("judgements"), SQLDataType.BLOB, this, "") + + private constructor(alias: Name, aliased: Table?): this(alias, null, null, aliased, null) + private constructor(alias: Name, aliased: Table?, parameters: Array?>?): this(alias, null, null, aliased, parameters) + + /** + * Create an aliased public.user_scores table reference + */ + constructor(alias: String): this(DSL.name(alias)) + + /** + * Create an aliased public.user_scores table reference + */ + constructor(alias: Name): this(alias, null) + + /** + * Create a public.user_scores table reference + */ + constructor(): this(DSL.name("user_scores"), null) + + constructor(child: Table, key: ForeignKey): this(Internal.createPathAlias(child, key), child, key, USER_SCORES, null) + override fun getSchema(): Schema? = if (aliased()) null else Public.PUBLIC + override fun getChecks(): List> = 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) +} diff --git a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/records/UserScoresRecord.kt b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/records/UserScoresRecord.kt new file mode 100644 index 0000000..bcf327f --- /dev/null +++ b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/records/UserScoresRecord.kt @@ -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(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? + set(value): Unit = set(38, value) + get(): Array? = get(38) as Array? + + 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? + set(value): Unit = set(41, value) + get(): Array? = get(41) as Array? + + 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? = null, keypressesMedian: Double? = null, keypressesStandardDeviation: Double? = null, sliderendReleaseTimes: Array? = 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() + } +} diff --git a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/references/Tables.kt b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/references/Tables.kt index a521b55..f903481 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/references/Tables.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/generated/tables/references/Tables.kt @@ -12,6 +12,7 @@ import com.nisemoe.generated.tables.Scores import com.nisemoe.generated.tables.ScoresJudgements import com.nisemoe.generated.tables.ScoresSimilarity import com.nisemoe.generated.tables.UpdateUserQueue +import com.nisemoe.generated.tables.UserScores 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 +/** + * The table public.user_scores. + */ +val USER_SCORES: UserScores = UserScores.USER_SCORES + /** * The table public.users. */ diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/UploadReplayController.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/UploadReplayController.kt new file mode 100644 index 0000000..fc85221 --- /dev/null +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/controller/UploadReplayController.kt @@ -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 { + // 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())) + } +} \ No newline at end of file diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/database/BeatmapService.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/database/BeatmapService.kt index 19d6ea3..3b2c1ea 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/database/BeatmapService.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/database/BeatmapService.kt @@ -1,12 +1,47 @@ package com.nisemoe.nise.database +import com.nisemoe.generated.tables.references.BEATMAPS import com.nisemoe.generated.tables.references.SCORES +import com.nisemoe.nise.osu.OsuApi import org.jooq.DSLContext import org.jooq.impl.DSL.avg +import org.slf4j.LoggerFactory import org.springframework.stereotype.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? { val condition = SCORES.BEATMAP_ID.eq(beatmapId) diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuApi.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuApi.kt index 7896a3b..b845d1c 100644 --- a/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuApi.kt +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuApi.kt @@ -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. * Efficiently cycles through the API keys to avoid rate limiting. diff --git a/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuReplay.kt b/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuReplay.kt new file mode 100644 index 0000000..bcfeac6 --- /dev/null +++ b/nise-backend/src/main/kotlin/com/nisemoe/nise/osu/OsuReplay.kt @@ -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 + } + +} \ No newline at end of file diff --git a/nise-backend/src/main/resources/db/migration/V0.0.1.027__create_user_scores.sql b/nise-backend/src/main/resources/db/migration/V0.0.1.027__create_user_scores.sql new file mode 100644 index 0000000..c768b44 --- /dev/null +++ b/nise-backend/src/main/resources/db/migration/V0.0.1.027__create_user_scores.sql @@ -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) +); \ No newline at end of file diff --git a/nise-backend/src/test/kotlin/com/nisemoe/nise/osu/OsuReplayTest.kt b/nise-backend/src/test/kotlin/com/nisemoe/nise/osu/OsuReplayTest.kt new file mode 100644 index 0000000..8372c17 --- /dev/null +++ b/nise-backend/src/test/kotlin/com/nisemoe/nise/osu/OsuReplayTest.kt @@ -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) + } + +} \ No newline at end of file diff --git a/nise-frontend/src/app/home/home.component.css b/nise-frontend/src/app/home/home.component.css index 943d3a8..13c849e 100644 --- a/nise-frontend/src/app/home/home.component.css +++ b/nise-frontend/src/app/home/home.component.css @@ -4,11 +4,15 @@ 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%; margin-right: 10px; } -.main .term:nth-child(2) { +.main .subcontainer:nth-child(2) { width: 30%; } + +.subcontainer .term { + width: fit-content; +} diff --git a/nise-frontend/src/app/home/home.component.html b/nise-frontend/src/app/home/home.component.html index 8a65828..c6f1e47 100644 --- a/nise-frontend/src/app/home/home.component.html +++ b/nise-frontend/src/app/home/home.component.html @@ -4,64 +4,77 @@
-
-

# Welcome to [nise.moe]

-

wtf is this?

-

This application will automatically crawl [osu!std] top scores and search for stolen replays or obvious relax/timewarp scores.

-

It started collecting replays on 2024-01-12

-

This website is not affiliated with the osu! game nor ppy. It is an unrelated, unaffiliated, 3rd party project.

-

If you have any suggestions or want to report bugs, feel free to join the Discord server below.

- -

# do you use rss? (nerd)

-

you can keep up with newly detected scores with the rss feed, subscribe to it using your favorite reader.

-
- - -
- +
-
-
- -
- new scores [live] - new scores [disconnected] -
- -
- -

- nothing yet...
- new scores will appear here. -

-
- +
+ + + + +
+
+ +
+ new scores [live] + new scores [disconnected] +
+ +
+ +

+ nothing yet...
+ new scores will appear here. +

+
+ + + +
diff --git a/nise-frontend/src/app/home/home.component.ts b/nise-frontend/src/app/home/home.component.ts index 24fdecf..b16911f 100644 --- a/nise-frontend/src/app/home/home.component.ts +++ b/nise-frontend/src/app/home/home.component.ts @@ -6,7 +6,8 @@ import {RxStompService} from "../../corelib/stomp/stomp.service"; import {Message} from "@stomp/stompjs/esm6"; import {ReplayData} from "../replays"; 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 { total_beatmaps: number; @@ -16,6 +17,10 @@ interface Statistics { total_replay_similarity: number; } +interface AnalyzeReplayResponse { + id: string; +} + @Component({ selector: 'app-home', standalone: true, @@ -36,8 +41,12 @@ export class HomeComponent implements OnInit, OnDestroy { statistics: Statistics | null = null; wantsConnection: boolean = true; + loading = false; + constructor( private localCacheService: LocalCacheService, + private router: Router, + private httpClient: HttpClient, 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(`${environment.apiUrl}/analyze`, formData).subscribe({ + next: (response) => { + this.loading = false; + this.router.navigate(['/c/' + response.id ]); + }, + error: (error) => { + this.loading = false; + }, + }); + } + } diff --git a/nise-frontend/src/assets/style.css b/nise-frontend/src/assets/style.css index f7b6571..26d98c1 100644 --- a/nise-frontend/src/assets/style.css +++ b/nise-frontend/src/assets/style.css @@ -59,7 +59,7 @@ html { @media screen and (min-width: 768px) { .main { - width: 820px !important; + width: 850px !important; } .header {