Aggiunge replay YouTube temporaneo, pull registrazioni dai CPX e snapshot ingest in admin.

Così overflow Hetzner e VOD YouTube restano in archivio dopo lo spegnimento del nodo, e la colonna ingest non si svuota.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-28 13:01:41 +02:00
co-authored by Cursor
parent 35dfa923e3
commit 05ef56c56d
54 changed files with 1843 additions and 201 deletions
@@ -43,7 +43,11 @@ class E2ECoverUploadTest {
openMatchWizardViaQuickStart()
waitForText(s(R.string.wizard_step_title_match), timeoutMs = 45_000)
scrollUntilVisible(s(R.string.wizard_match_cover_title))
scrollUntilVisible(
s(R.string.wizard_match_cover_title),
"Copertina in pausa",
"Pause cover",
)
tapClickableText(s(R.string.wizard_match_cover_change))
pickFirstPhotoFromPicker()
@@ -185,12 +189,12 @@ class E2ECoverUploadTest {
}
}
private fun scrollUntilVisible(text: String) {
repeat(8) {
if (device.hasObject(By.text(text))) return
private fun scrollUntilVisible(vararg texts: String) {
repeat(20) {
if (texts.any { device.hasObject(By.text(it)) }) return
scrollDown(1)
}
waitForText(text, timeoutMs = 10_000)
waitForAnyText(*texts, timeoutMs = 10_000)
}
private fun grantRuntimePermissions() {
@@ -27,7 +27,10 @@ data class ChangePasswordRequest(
data class MessageResponse(val message: String)
data class ApiErrorResponse(val error: String? = null)
data class ApiErrorResponse(
val error: String? = null,
@Json(name = "error_code") val errorCode: String? = null,
)
data class LoginResponse(
val user: UserDto,
@@ -2,31 +2,63 @@ package com.matchlivetv.match_live_tv.data.repository
import android.content.Context
import com.matchlivetv.match_live_tv.core.DeviceTelemetry
import com.matchlivetv.match_live_tv.data.api.ApiErrorResponse
import com.matchlivetv.match_live_tv.data.api.CreateSessionRequest
import com.matchlivetv.match_live_tv.data.api.MatchLiveApi
import com.matchlivetv.match_live_tv.data.api.MinQualityRequest
import com.matchlivetv.match_live_tv.data.api.NetworkTestRequest
import com.matchlivetv.match_live_tv.data.api.NetworkTestResponse
import com.matchlivetv.match_live_tv.domain.StreamSession
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import kotlinx.coroutines.delay
import retrofit2.HttpException
class SessionRepository(
private val api: MatchLiveApi,
private val appContext: Context,
) {
private val errorAdapter by lazy {
Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
.adapter(ApiErrorResponse::class.java)
}
suspend fun createSession(
matchId: String,
platform: String = "matchlivetv",
privacyStatus: String = "public",
youtubeChannel: String? = null,
): StreamSession = api.createSession(
matchId,
CreateSessionRequest(
platform = platform,
privacyStatus = privacyStatus,
youtubeChannel = youtubeChannel,
client = DeviceTelemetry.clientInfo(appContext),
),
).toDomain()
scalingRetries: Int = 8,
scalingRetryDelayMs: Long = 15_000L,
): StreamSession {
var attempt = 0
while (true) {
try {
return api.createSession(
matchId,
CreateSessionRequest(
platform = platform,
privacyStatus = privacyStatus,
youtubeChannel = youtubeChannel,
client = DeviceTelemetry.clientInfo(appContext),
),
).toDomain()
} catch (e: HttpException) {
val code = parseErrorCode(e)
if (code != "stream_capacity_scaling" || attempt >= scalingRetries) throw e
attempt += 1
delay(scalingRetryDelayMs)
}
}
}
private fun parseErrorCode(error: HttpException): String? {
val body = error.response()?.errorBody()?.string().orEmpty()
if (body.isBlank()) return null
return runCatching { errorAdapter.fromJson(body)?.errorCode }.getOrNull()
}
suspend fun fetchSession(sessionId: String): StreamSession =
api.session(sessionId).toDomain()