### Start BeatLeader Server (Kotlin) Source: https://context7.com/beatmaps-io/beatsaver-ranking/llms.txt Initializes and starts the BeatLeader service. It sets up logging, database connections, and an embedded Netty server. The server listens on a configurable port and host, defaulting to 3032 and 127.0.0.1 respectively, and integrates with RabbitMQ for messaging. ```kotlin package io.beatmaps.beatleader import io.beatmaps.common.amqp.setupAMQP import io.beatmaps.common.db.setupDB import io.beatmaps.common.setupLogging import io.ktor.server.application.Application import io.ktor.server.application.install import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty import pl.jutupe.ktor_rabbitmq.RabbitMQ // Configure via environment variables // LISTEN_PORT_BL: Port for BeatLeader service (default: 3032) // LISTEN_HOST_BL or LISTEN_HOST: Host to bind (default: 127.0.0.1) fun main() { setupLogging() setupDB() embeddedServer( Netty, port = System.getenv("LISTEN_PORT_BL")?.toIntOrNull() ?: 3032, host = System.getenv("LISTEN_HOST_BL") ?: System.getenv("LISTEN_HOST") ?: "127.0.0.1", module = Application::beatleader ).start(wait = true) } fun Application.beatleader() { val mq = install(RabbitMQ) { setupAMQP(false) } startScraper(mq) } ``` -------------------------------- ### Start ScoreSaber Ktor Server - Kotlin Source: https://context7.com/beatmaps-io/beatsaver-ranking/llms.txt Initializes the ScoreSaber Ktor server, setting up logging, database connections, and RabbitMQ. It then starts concurrent background scraping tasks for ranked and qualified maps. Configuration is primarily done via environment variables. ```Kotlin package io.beatmaps.scoresaber import io.beatmaps.common.amqp.setupAMQP import io.beatmaps.common.db.setupDB import io.beatmaps.common.setupLogging import io.ktor.server.application.Application import io.ktor.server.application.install import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty import pl.jutupe.ktor_rabbitmq.RabbitMQ // Configure via environment variables // LISTEN_PORT_SS: Port for ScoreSaber service (default: 3031) // LISTEN_HOST_SS or LISTEN_HOST: Host to bind (default: 127.0.0.1) fun main() { setupLogging() setupDB() embeddedServer( Netty, port = System.getenv("LISTEN_PORT_SS")?.toIntOrNull() ?: 3031, host = System.getenv("LISTEN_HOST_SS") ?: System.getenv("LISTEN_HOST") ?: "127.0.0.1", module = Application::scoresaber ).start(wait = true) } fun Application.scoresaber() { val mq = install(RabbitMQ) { setupAMQP(false) } startScraper(mq) } ``` -------------------------------- ### Background Scraping Tasks Initialization in Kotlin Source: https://context7.com/beatmaps-io/beatsaver-ranking/llms.txt Starts two continuous background tasks that run hourly to synchronize map data from external APIs. One task scrapes qualified maps, and the other scrapes ranked maps. It includes initial delays and error handling for each task, running on a fixed thread pool. Dependencies include kotlinx.coroutines and java.util.concurrent. ```kotlin import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.launch import java.util.concurrent.Executors import java.time.Duration val es = Executors.newFixedThreadPool(8) fun startScraper(mq: RabbitMQInstance) { // Task 1: Scrape qualified maps hourly GlobalScope.launch(es.asCoroutineDispatcher()) { val qualifiedHashes = transaction { Versions.join(Beatmap, JoinType.INNER, Beatmap.id, Versions.mapId) .select(Versions.hash) .where { Beatmap.qualified eq true } .map { it[Versions.hash] }.toHashSet() } delay(Duration.ofMinutes(4)) // Initial delay while (true) { try { scrapeQualified(qualifiedHashes) } catch (e: Exception) { logger.severe(e.message) } delay(Duration.ofHours(1)) } } // Task 2: Scrape ranked maps hourly GlobalScope.launch(es.asCoroutineDispatcher()) { delay(Duration.ofMinutes(5)) // Initial delay while (true) { try { updateRanked(mq = mq) } catch (e: Exception) { logger.severe(e.message) } delay(Duration.ofHours(1)) } } } ``` -------------------------------- ### Update Ranked Maps from ScoreSaber API (Kotlin) Source: https://context7.com/beatmaps-io/beatsaver-ranking/llms.txt Fetches newly ranked maps from ScoreSaber, updates database entries for difficulties and beatmaps with new star ratings and ranking timestamps, and publishes update events to RabbitMQ. It depends on database access, ScoreSaber API interaction, and RabbitMQ publishing capabilities. Inputs include the RabbitMQ instance, and outputs are database updates and message queue events. ```kotlin import io.beatmaps.common.db.NowExpression import io.beatmaps.common.dbo.Beatmap import io.beatmaps.common.dbo.Difficulty import io.beatmaps.common.dbo.Versions import io.beatmaps.common.jsonClient import io.beatmaps.scoresaber.dto.ScoreSaberList import io.ktor.client.call.body import io.ktor.client.request.get import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update import pl.jutupe.ktor_rabbitmq.publish suspend fun updateRanked(mq: RabbitMQInstance) { // Get most recent ranked timestamp from database val mostRecentRanked = transaction { Difficulty.selectAll() .orderBy(Difficulty.rankedAt, SortOrder.DESC_NULLS_LAST) .limit(1).singleOrNull()?.let { it[Difficulty.rankedAt] } } // Scrape all ranked maps newer than most recent val rankedMaps = scrapeRanked( page = 1, mostRecentRanked = mostRecentRanked, filter = "ranked", unique = false, dateSelector = { it.rankedDate }, boolSelector = { it.ranked } ) val groupedByHash = rankedMaps.groupBy { it.songHash } // Update difficulty ratings and timestamps transaction { rankedMaps.forEachIndexed { idx, diff -> Difficulty.join(Versions, JoinType.INNER, Versions.id, Difficulty.versionId) .update({ Versions.hash eq diff.songHash.lowercase() and (Difficulty.characteristic eq diff.characteristic) and (Difficulty.difficulty eq diff.diff) }) { it[Difficulty.stars] = if (diff.stars > 0) diff.stars.toBigDecimal() else null it[Difficulty.rankedAt] = coalesce( LiteralOp(Difficulty.rankedAt.columnType, diff.rankedDate?.toJavaInstant()), Difficulty.rankedAt, NowExpression(Difficulty.rankedAt) ) } } // Update beatmap ranking status groupedByHash.forEach { entry -> Beatmap.join(Versions, JoinType.INNER, Versions.mapId, Beatmap.id) .update({ Versions.hash eq entry.key.lowercase() }) { it[Beatmap.ranked] = true it[Beatmap.rankedAt] = coalesce( LiteralOp(Beatmap.rankedAt.columnType, entry.value.mapNotNull { e -> e.rankedDate }.minOrNull()?.toJavaInstant()), Beatmap.rankedAt, NowExpression(Beatmap.rankedAt) ) } } // Get updated beatmap IDs Beatmap.joinVersions() .select(Beatmap.id) .where { Versions.hash inList groupedByHash.keys } .map { it[Beatmap.id].value } }.forEach { mapId -> // Publish update event to RabbitMQ mq.publish("beatmaps", "maps.$mapId.updated.ranked", null, mapId) } } ``` -------------------------------- ### BeatLeader API Pagination with Timestamp Filtering in Kotlin Source: https://context7.com/beatmaps-io/beatsaver-ranking/llms.txt Fetches ranked leaderboards from the BeatLeader API using timestamp-based filtering and recursive pagination. It handles API requests, filters results by status and timestamp, and includes a delay between paginated calls to respect API rate limits. Dependencies include kotlinx.datetime and java.time. ```kotlin import io.beatmaps.beatleader.dto.BeatLeaderList import kotlinx.datetime.Instant import java.time.Duration const val qualifiedStatus = 2 suspend fun scrapeRanked( mostRecentRanked: java.time.Instant?, filter: String, dateSelector: (BeatLeaderLeaderboard) -> Instant?, boolSelector: (BeatLeaderLeaderboard) -> Boolean, page: Int = 1, pageSize: Int = 20 ): List { // BeatLeader API supports timestamp filtering directly val fromTimestamp = mostRecentRanked?.epochSecond ?: 0 val json = jsonClient.get( "https://api.beatleader.com/leaderboards?" + "type=$filter&sortBy=timestamp&order=asc&count=$pageSize&page=$page" + "&date_from=$fromTimestamp&date_range=$filter" ) { timeout { socketTimeoutMillis = 30000 requestTimeoutMillis = 60000 } }.body() // Filter by status and timestamp val filteredLeaderboards = json.data.filter { val date = dateSelector(it)?.toJavaInstant() boolSelector(it) && (mostRecentRanked == null || date == null || date > mostRecentRanked) } // Continue pagination if results found return if (filteredLeaderboards.isNotEmpty()) { delay(Duration.ofMillis(20L)) filteredLeaderboards.plus( scrapeRanked(mostRecentRanked, filter, dateSelector, boolSelector, page + 1, pageSize) ) } else { emptyList() } } ``` -------------------------------- ### Scrape BeatLeader Ranked Maps (Kotlin) Source: https://context7.com/beatmaps-io/beatsaver-ranking/llms.txt Fetches ranked maps from the BeatLeader API, filters them by timestamp, and updates the database with BeatLeader-specific ranking information. It handles grouping by hash, updating difficulty columns (stars, rankedAt, qualifiedAt), and marking beatmaps as ranked. ```kotlin import io.beatmaps.beatleader.dto.BeatLeaderList import io.beatmaps.beatleader.dto.BeatLeaderLeaderboard const val rankedStatus = 3 suspend fun updateRanked(mq: RabbitMQInstance) { // Get most recent BeatLeader ranked timestamp val mostRecentRanked = transaction { Difficulty.selectAll() .orderBy(Difficulty.blRankedAt, SortOrder.DESC_NULLS_LAST) .limit(1).singleOrNull()?.let { it[Difficulty.blRankedAt] } } // Scrape with timestamp filtering val rankedLeaderboards = scrapeRanked( mostRecentRanked = mostRecentRanked, filter = "ranked", dateSelector = { it.difficulty.rankedTime }, boolSelector = { it.difficulty.status == rankedStatus } ) val groupedByHash = rankedLeaderboards.groupBy { it.song.hash } transaction { rankedLeaderboards.forEachIndexed { idx, leaderboard -> val characteristic = leaderboard.characteristic ?: return@forEachIndexed // Update BeatLeader-specific difficulty columns Difficulty.join(Versions, JoinType.INNER, Versions.id, Difficulty.versionId) .update({ Versions.hash eq leaderboard.song.hash.lowercase() and (Difficulty.characteristic eq characteristic) and (Difficulty.difficulty eq leaderboard.diff) }) { it[Difficulty.blStars] = if (leaderboard.difficulty.stars > 0) leaderboard.difficulty.stars.toBigDecimal() else null it[Difficulty.blRankedAt] = coalesce( LiteralOp(Difficulty.blRankedAt.columnType, leaderboard.difficulty.rankedTime?.toJavaInstant()), Difficulty.blRankedAt, NowExpression(Difficulty.blRankedAt) ) it[Difficulty.blQualifiedAt] = coalesce( LiteralOp(Difficulty.blQualifiedAt.columnType, leaderboard.difficulty.qualifiedTime?.toJavaInstant()), Difficulty.blQualifiedAt, NowExpression(Difficulty.blQualifiedAt) ) } } // Update BeatLeader beatmap ranking groupedByHash.forEach { entry -> Beatmap.join(Versions, JoinType.INNER, Versions.mapId, Beatmap.id) .update({ Versions.hash eq entry.key.lowercase() }) { it[Beatmap.blRanked] = true it[Beatmap.blRankedAt] = coalesce( LiteralOp(Beatmap.blRankedAt.columnType, entry.value.mapNotNull { e -> e.difficulty.rankedTime }.minOrNull()?.toJavaInstant()), Beatmap.blRankedAt, NowExpression(Beatmap.blRankedAt) ) } } Beatmap.joinVersions() .select(Beatmap.id) .where { Versions.hash inList groupedByHash.keys } .map { it[Beatmap.id].value } }.forEach { mapId -> mq.publish("beatmaps", "maps.$mapId.updated.ranked", null, mapId) } } ``` -------------------------------- ### Recursive API Pagination for ScoreSaber (Kotlin) Source: https://context7.com/beatmaps-io/beatsaver-ranking/llms.txt The `scrapeRanked` function recursively fetches paginated results from the ScoreSaber API, continuing until all new or updated maps are retrieved. It includes logic to prevent excessive calls if maps are ranked too recently and handles API response parsing. Dependencies include `ktor-client` and `kotlinx-datetime`. ```kotlin import io.beatmaps.scoresaber.dto.ScoreSaberList import io.ktor.client.plugins.timeout import kotlinx.coroutines.time.delay import kotlinx.datetime.Clock import kotlin.time.Duration.Companion.hours import java.time.Duration import java.time.Instant as JInstant suspend fun scrapeRanked( page: Int, mostRecentRanked: JInstant?, filter: String, unique: Boolean, dateSelector: (ScoreSaberSong) -> Instant?, boolSelector: (ScoreSaberSong) -> Boolean ): List { // Fetch page from ScoreSaber API val json = jsonClient.get( "https://scoresaber.com/api/leaderboards?$filter=true&category=1&unique=$unique&page=$page" ) { timeout { socketTimeoutMillis = 30000 requestTimeoutMillis = 60000 } }.body() // Check if first page was ranked too recently (within 4 hours) val tooSoon = page == 1 && json.leaderboards.firstOrNull()?.rankedDate?.let { Clock.System.now() - it < 4.hours } == true // Filter maps newer than most recent in database val filteredMaps = json.leaderboards.filter { val date = dateSelector(it)?.toJavaInstant() boolSelector(it) && (mostRecentRanked == null || date == null || date > mostRecentRanked) } // Recursively fetch next page if results found and not too recent return if (!tooSoon && filteredMaps.isNotEmpty()) { delay(Duration.ofMillis(20L)) filteredMaps.plus(scrapeRanked(page + 1, mostRecentRanked, filter, unique, dateSelector, boolSelector)) } else { emptyList() } } ``` -------------------------------- ### Scrape Qualified Maps from ScoreSaber (Kotlin) Source: https://context7.com/beatmaps-io/beatsaver-ranking/llms.txt The `scrapeQualified` function identifies and updates the database with newly qualified and unqualified maps by comparing current qualified maps against cached data. It interacts with the ScoreSaber API and the application's database, requiring `exposed` and `kotlinx-datetime` dependencies. ```kotlin import org.jetbrains.exposed.sql.alias suspend fun scrapeQualified(qualifiedHashes: HashSet) { // Fetch all currently qualified maps from API val qualified = scrapeRanked( page = 1, mostRecentRanked = null, filter = "qualified", unique = false, dateSelector = { it.qualifiedDate }, boolSelector = { it.qualified } ) // Group by hash and expand to include all versions val groupedByHash = qualified.groupBy { it.songHash.lowercase() } val qualifiedMaps = transaction { val otherVersions = Versions.alias("v2") Versions.join(otherVersions, JoinType.INNER, otherVersions[Versions.mapId], Versions.mapId) .select(otherVersions[Versions.hash]) .where { Versions.hash inList groupedByHash.keys } .map { it[otherVersions[Versions.hash]] } }.toHashSet() // Calculate differences val toRemove = qualifiedHashes.minus(qualifiedMaps) val toAdd = qualifiedMaps.minus(qualifiedHashes) // Update database transaction { // Mark maps as no longer qualified Beatmap.join(Versions, JoinType.INNER, Versions.mapId, Beatmap.id) .update({ Versions.hash inList toRemove }) { it[Beatmap.qualified] = false } // Mark new qualified maps Beatmap.join(Versions, JoinType.INNER, Versions.mapId, Beatmap.id) .update({ Versions.hash inList toAdd }) { it[Beatmap.qualified] = true it[Beatmap.qualifiedAt] = coalesce( Beatmap.qualifiedAt, NowExpression(Beatmap.qualifiedAt) ) } // Update difficulty-level qualified timestamps toAdd.forEach { hash -> groupedByHash[hash]?.forEach { diff -> Difficulty.join(Versions, JoinType.INNER, Versions.id, Difficulty.versionId) .update({ Versions.hash eq diff.songHash.lowercase() and (Difficulty.characteristic eq diff.characteristic) and (Difficulty.difficulty eq diff.diff) }) { it[Difficulty.qualifiedAt] = coalesce( LiteralOp(Difficulty.qualifiedAt.columnType, diff.qualifiedDate?.toJavaInstant()), Difficulty.qualifiedAt, NowExpression(Difficulty.qualifiedAt) ) } } } } // Update cache qualifiedHashes.clear() qualifiedHashes.addAll(qualifiedMaps) } ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.