mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-16 23:12:12 +00:00
feat: update contributions api
This commit is contained in:
parent
84a4771f67
commit
99d821e7db
3 changed files with 42 additions and 175 deletions
|
|
@ -97,6 +97,7 @@ abstract class GenerateRuntimeConfigsTask : DefaultTask() {
|
|||
|package com.nuvio.app.features.settings
|
||||
|
|
||||
|object CommunityConfig {
|
||||
| const val CONTRIBUTIONS_URL = "${props.getProperty("CONTRIBUTIONS_URL", "")}"
|
||||
| const val DONATIONS_BASE_URL = "${props.getProperty("DONATIONS_BASE_URL", "")}"
|
||||
| const val DONATIONS_DONATE_URL = "${props.getProperty("DONATIONS_DONATE_URL", "")}"
|
||||
|}
|
||||
|
|
|
|||
|
|
@ -54,10 +54,7 @@ import com.nuvio.app.core.ui.NuvioScreen
|
|||
import com.nuvio.app.core.ui.NuvioScreenHeader
|
||||
import com.nuvio.app.core.ui.NuvioSurfaceCard
|
||||
import com.nuvio.app.features.addons.httpRequestRaw
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
|
@ -83,12 +80,16 @@ private data class CommunityUiState(
|
|||
)
|
||||
|
||||
@Serializable
|
||||
private data class GitHubContributorDto(
|
||||
val login: String? = null,
|
||||
@SerialName("avatar_url") val avatarUrl: String? = null,
|
||||
@SerialName("html_url") val htmlUrl: String? = null,
|
||||
val contributions: Int? = null,
|
||||
val type: String? = null,
|
||||
private data class ContributionsResponseDto(
|
||||
val contributors: List<ContributionDto> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class ContributionDto(
|
||||
val name: String? = null,
|
||||
val avatar: String? = null,
|
||||
val profile: String? = null,
|
||||
val total: Int? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
|
|
@ -108,9 +109,6 @@ internal data class CommunityContributor(
|
|||
val avatarUrl: String?,
|
||||
val profileUrl: String?,
|
||||
val totalContributions: Int,
|
||||
val mobileContributions: Int,
|
||||
val tvContributions: Int,
|
||||
val webContributions: Int,
|
||||
)
|
||||
|
||||
internal data class SupporterDonation(
|
||||
|
|
@ -122,39 +120,31 @@ internal data class SupporterDonation(
|
|||
)
|
||||
|
||||
private object SupportersContributorsRepository {
|
||||
private const val gitHubOwner = "nuviomedia"
|
||||
private const val mobileRepository = "nuviomobile"
|
||||
private const val tvRepository = "nuviotv"
|
||||
private const val webRepository = "nuvioweb"
|
||||
private const val gitHubApiBase = "https://api.github.com"
|
||||
|
||||
private val json = Json { ignoreUnknownKeys = true; isLenient = true }
|
||||
|
||||
suspend fun getContributors(): Result<List<CommunityContributor>> = runCatching {
|
||||
coroutineScope {
|
||||
val mobileDeferred = async { fetchRepoContributors(mobileRepository) }
|
||||
val tvDeferred = async { fetchRepoContributors(tvRepository) }
|
||||
val webDeferred = async { fetchRepoContributors(webRepository) }
|
||||
|
||||
val mobileResult = mobileDeferred.await()
|
||||
val tvResult = tvDeferred.await()
|
||||
val webResult = webDeferred.await()
|
||||
|
||||
if (mobileResult.isFailure && tvResult.isFailure && webResult.isFailure) {
|
||||
throw (
|
||||
mobileResult.exceptionOrNull()
|
||||
?: tvResult.exceptionOrNull()
|
||||
?: webResult.exceptionOrNull()
|
||||
?: IllegalStateException(getString(Res.string.community_error_unable_load_contributors))
|
||||
)
|
||||
}
|
||||
|
||||
mergeContributors(
|
||||
mobileContributors = mobileResult.getOrDefault(emptyList()),
|
||||
tvContributors = tvResult.getOrDefault(emptyList()),
|
||||
webContributors = webResult.getOrDefault(emptyList()),
|
||||
)
|
||||
val contributionsUrl = CommunityConfig.CONTRIBUTIONS_URL.trim()
|
||||
check(contributionsUrl.isNotBlank()) {
|
||||
getString(Res.string.community_error_unable_load_contributors)
|
||||
}
|
||||
|
||||
val response = httpRequestRaw(
|
||||
method = "GET",
|
||||
url = contributionsUrl,
|
||||
headers = emptyMap(),
|
||||
body = "",
|
||||
)
|
||||
if (response.status !in 200..299) {
|
||||
error(getString(Res.string.community_error_contributors_request_failed))
|
||||
}
|
||||
|
||||
json.decodeFromString<ContributionsResponseDto>(response.body)
|
||||
.contributors
|
||||
.mapNotNull(::normalizeContributor)
|
||||
.sortedWith(
|
||||
compareByDescending<CommunityContributor> { it.totalContributions }
|
||||
.thenBy { it.login.lowercase() },
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun getSupporters(limit: Int = 200): Result<List<SupporterDonation>> = runCatching {
|
||||
|
|
@ -194,127 +184,19 @@ private object SupportersContributorsRepository {
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchRepoContributors(repo: String): Result<List<GitHubContributorDto>> = runCatching {
|
||||
val contributors = mutableListOf<GitHubContributorDto>()
|
||||
var nextUrl: String? = "$gitHubApiBase/repos/$gitHubOwner/$repo/contributors?per_page=100"
|
||||
|
||||
while (nextUrl != null) {
|
||||
val response = httpRequestRaw(
|
||||
method = "GET",
|
||||
url = nextUrl,
|
||||
headers = mapOf(
|
||||
"Accept" to "application/vnd.github+json",
|
||||
"User-Agent" to "NuvioMobile",
|
||||
),
|
||||
body = "",
|
||||
)
|
||||
if (response.status !in 200..299) {
|
||||
error(getString(Res.string.community_error_contributors_request_failed))
|
||||
}
|
||||
|
||||
contributors += json.decodeFromString<List<GitHubContributorDto>>(response.body)
|
||||
nextUrl = response.headers.entries
|
||||
.firstOrNull { it.key.equals("link", ignoreCase = true) }
|
||||
?.value
|
||||
?.let(::parseNextLink)
|
||||
}
|
||||
|
||||
contributors
|
||||
}
|
||||
|
||||
private fun mergeContributors(
|
||||
mobileContributors: List<GitHubContributorDto>,
|
||||
tvContributors: List<GitHubContributorDto>,
|
||||
webContributors: List<GitHubContributorDto>,
|
||||
): List<CommunityContributor> {
|
||||
val contributorsByLogin = linkedMapOf<String, MutableCommunityContributor>()
|
||||
|
||||
mobileContributors.forEach { dto ->
|
||||
normalizeContributor(dto)?.let { contributor ->
|
||||
val entry = contributorsByLogin.getOrPut(contributor.login.lowercase()) {
|
||||
MutableCommunityContributor(
|
||||
login = contributor.login,
|
||||
avatarUrl = contributor.avatarUrl,
|
||||
profileUrl = contributor.htmlUrl,
|
||||
)
|
||||
}
|
||||
entry.avatarUrl = entry.avatarUrl ?: contributor.avatarUrl
|
||||
entry.profileUrl = entry.profileUrl ?: contributor.htmlUrl
|
||||
entry.mobileContributions += contributor.contributions
|
||||
}
|
||||
}
|
||||
|
||||
tvContributors.forEach { dto ->
|
||||
normalizeContributor(dto)?.let { contributor ->
|
||||
val entry = contributorsByLogin.getOrPut(contributor.login.lowercase()) {
|
||||
MutableCommunityContributor(
|
||||
login = contributor.login,
|
||||
avatarUrl = contributor.avatarUrl,
|
||||
profileUrl = contributor.htmlUrl,
|
||||
)
|
||||
}
|
||||
entry.avatarUrl = entry.avatarUrl ?: contributor.avatarUrl
|
||||
entry.profileUrl = entry.profileUrl ?: contributor.htmlUrl
|
||||
entry.tvContributions += contributor.contributions
|
||||
}
|
||||
}
|
||||
|
||||
webContributors.forEach { dto ->
|
||||
normalizeContributor(dto)?.let { contributor ->
|
||||
val entry = contributorsByLogin.getOrPut(contributor.login.lowercase()) {
|
||||
MutableCommunityContributor(
|
||||
login = contributor.login,
|
||||
avatarUrl = contributor.avatarUrl,
|
||||
profileUrl = contributor.htmlUrl,
|
||||
)
|
||||
}
|
||||
entry.avatarUrl = entry.avatarUrl ?: contributor.avatarUrl
|
||||
entry.profileUrl = entry.profileUrl ?: contributor.htmlUrl
|
||||
entry.webContributions += contributor.contributions
|
||||
}
|
||||
}
|
||||
|
||||
return contributorsByLogin.values
|
||||
.map { contributor ->
|
||||
CommunityContributor(
|
||||
login = contributor.login,
|
||||
avatarUrl = contributor.avatarUrl,
|
||||
profileUrl = contributor.profileUrl,
|
||||
totalContributions = contributor.mobileContributions + contributor.tvContributions + contributor.webContributions,
|
||||
mobileContributions = contributor.mobileContributions,
|
||||
tvContributions = contributor.tvContributions,
|
||||
webContributions = contributor.webContributions,
|
||||
)
|
||||
}
|
||||
.sortedWith(
|
||||
compareByDescending<CommunityContributor> { it.totalContributions }
|
||||
.thenBy { it.login.lowercase() },
|
||||
)
|
||||
}
|
||||
|
||||
private fun normalizeContributor(dto: GitHubContributorDto): NormalizedContributor? {
|
||||
val login = dto.login?.trim().orEmpty()
|
||||
val contributions = dto.contributions ?: 0
|
||||
val type = dto.type?.trim()
|
||||
private fun normalizeContributor(dto: ContributionDto): CommunityContributor? {
|
||||
val login = dto.name?.trim().orEmpty()
|
||||
val contributions = dto.total ?: 0
|
||||
if (login.isBlank() || contributions <= 0) return null
|
||||
if (type != null && !type.equals("User", ignoreCase = true)) return null
|
||||
|
||||
return NormalizedContributor(
|
||||
return CommunityContributor(
|
||||
login = login,
|
||||
avatarUrl = dto.avatarUrl?.trim()?.takeIf { it.isNotBlank() },
|
||||
htmlUrl = dto.htmlUrl?.trim()?.takeIf { it.isNotBlank() },
|
||||
contributions = contributions,
|
||||
avatarUrl = dto.avatar?.trim()?.takeIf { it.isNotBlank() },
|
||||
profileUrl = dto.profile?.trim()?.takeIf { it.isNotBlank() },
|
||||
totalContributions = contributions,
|
||||
)
|
||||
}
|
||||
|
||||
private fun parseNextLink(linkHeader: String): String? =
|
||||
linkHeader.split(',')
|
||||
.map(String::trim)
|
||||
.firstOrNull { it.contains("rel=\"next\"") }
|
||||
?.substringAfter('<')
|
||||
?.substringBefore('>')
|
||||
?.takeIf { it.isNotBlank() }
|
||||
|
||||
private fun supporterSortTimestamp(rawDate: String): Long {
|
||||
val datePart = rawDate.substringBefore('T')
|
||||
val parts = datePart.split('-')
|
||||
|
|
@ -324,22 +206,6 @@ private object SupportersContributorsRepository {
|
|||
val day = parts[2].toLongOrNull() ?: return Long.MIN_VALUE
|
||||
return year * 10_000L + month * 100L + day
|
||||
}
|
||||
|
||||
private data class NormalizedContributor(
|
||||
val login: String,
|
||||
val avatarUrl: String?,
|
||||
val htmlUrl: String?,
|
||||
val contributions: Int,
|
||||
)
|
||||
|
||||
private data class MutableCommunityContributor(
|
||||
val login: String,
|
||||
var avatarUrl: String?,
|
||||
var profileUrl: String?,
|
||||
var mobileContributions: Int = 0,
|
||||
var tvContributions: Int = 0,
|
||||
var webContributions: Int = 0,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -656,7 +522,7 @@ private fun ContributorsCard(
|
|||
) {
|
||||
items(
|
||||
items = contributors,
|
||||
key = { contributor -> contributor.login.lowercase() },
|
||||
key = { contributor -> "${contributor.login.lowercase()}-${contributor.profileUrl.orEmpty()}" },
|
||||
) { contributor ->
|
||||
ContributorRow(
|
||||
contributor = contributor,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
CURRENT_PROJECT_VERSION=38
|
||||
MARKETING_VERSION=0.1.7
|
||||
CURRENT_PROJECT_VERSION=40
|
||||
MARKETING_VERSION=0.1.9
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue