feat(stations): скрывать украинские станции (ROKS, Kiss FM) для РФ
Radio ROKS и Kiss FM (TavR Media, хосты radioroks.ua / kissfm.ua) недоступны с российских IP без VPN. Теперь для пользователей из РФ они полностью скрыты — и сами станции (везде, где используется список), и их чипы-категории. Страна определяется по IP (api.country.is → ipapi.co; при VPN вернёт страну выходного узла, тогда станции доступны и НЕ скрываются), с фолбэком на страну SIM/сети/локали устройства, если IP-сервис недоступен (в РФ часто заблокирован). Код страны кэшируется (DataStore). Фильтр в GetStationsUseCase (combine со страной) + чипы в StationsViewModel. id 741 «Радио РОКС» (stream.roks.com) — российская, под правило не попадает. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
package com.radiola.data.repository
|
||||
|
||||
import android.content.Context
|
||||
import android.telephony.TelephonyManager
|
||||
import android.util.Log
|
||||
import com.radiola.domain.repository.RegionRepository
|
||||
import com.radiola.domain.repository.SettingsRepository
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Определяет страну пользователя по IP (для гео-фильтрации). Код кэшируется в
|
||||
* настройках, так что после первого успешного запроса работает мгновенно и
|
||||
* оффлайн. Источник — публичные гео-IP сервисы (без ключа).
|
||||
*/
|
||||
@Singleton
|
||||
class RegionRepositoryImpl @Inject constructor(
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
@ApplicationContext private val context: Context
|
||||
) : RegionRepository {
|
||||
|
||||
override fun countryCode(): Flow<String?> = settingsRepository.getCountryCode()
|
||||
|
||||
override suspend fun refresh() {
|
||||
withContext(Dispatchers.IO) {
|
||||
val code = fetchCountry()
|
||||
Log.d("RegionRepo", "country=$code")
|
||||
if (code != null) settingsRepository.setCountryCode(code)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchCountry(): String? {
|
||||
// IP — приоритет (учитывает VPN: при VPN страна = выходного узла, и тогда
|
||||
// украинские потоки доступны → не скрываем).
|
||||
ipCountry()?.let { return it }
|
||||
// Фолбэк, если IP-сервис недоступен (напр. заблокирован): страна SIM/сети/локали.
|
||||
return deviceCountry()
|
||||
}
|
||||
|
||||
private fun ipCountry(): String? {
|
||||
// 1) api.country.is → {"ip":"..","country":"RU"}
|
||||
request("https://api.country.is/")?.let { body ->
|
||||
Regex("\"country\"\\s*:\\s*\"([A-Za-z]{2})\"").find(body)?.groupValues?.get(1)?.let {
|
||||
return it.uppercase()
|
||||
}
|
||||
}
|
||||
// 2) ipapi.co/country/ → "RU"
|
||||
request("https://ipapi.co/country/")?.trim()?.let { body ->
|
||||
if (body.matches(Regex("[A-Za-z]{2}"))) return body.uppercase()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun deviceCountry(): String? = try {
|
||||
val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
|
||||
val net = tm?.networkCountryIso?.takeIf { it.isNotBlank() }
|
||||
val sim = tm?.simCountryIso?.takeIf { it.isNotBlank() }
|
||||
(net ?: sim ?: Locale.getDefault().country.takeIf { it.isNotBlank() })?.uppercase()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
private fun request(url: String): String? = try {
|
||||
okHttpClient.newCall(
|
||||
Request.Builder().url(url).header("User-Agent", "radiOLA").build()
|
||||
).execute().use { if (it.isSuccessful) it.body?.string() else null }
|
||||
} catch (e: Exception) {
|
||||
Log.w("RegionRepo", "geo fetch fail ($url): ${e.message}")
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ class SettingsRepositoryImpl @Inject constructor(
|
||||
private val EQUALIZER_PRESET = stringPreferencesKey("equalizer_preset")
|
||||
private val RECORDING_ENABLED = booleanPreferencesKey("recording_enabled")
|
||||
private val PREFERRED_BITRATE = intPreferencesKey("preferred_bitrate")
|
||||
private val COUNTRY_CODE = stringPreferencesKey("country_code")
|
||||
}
|
||||
|
||||
override fun getLastStationId(): Flow<Int?> = dataStore.data.map { it[LAST_STATION_ID] }
|
||||
@@ -53,4 +54,7 @@ class SettingsRepositoryImpl @Inject constructor(
|
||||
|
||||
override fun getPreferredBitrate(): Flow<Int> = dataStore.data.map { it[PREFERRED_BITRATE] ?: 0 }
|
||||
override suspend fun setPreferredBitrate(bitrate: Int) { dataStore.edit { it[PREFERRED_BITRATE] = bitrate } }
|
||||
|
||||
override fun getCountryCode(): Flow<String?> = dataStore.data.map { it[COUNTRY_CODE] }
|
||||
override suspend fun setCountryCode(code: String) { dataStore.edit { it[COUNTRY_CODE] = code } }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user