This commit is contained in:
2026-09-02 23:47:00 +03:00
parent b4d0b0c8e9
commit a8830bcd91
77 changed files with 2998 additions and 0 deletions
@@ -0,0 +1,74 @@
package agzam.vyatsu.data.repository
import agzam.vyatsu.data.local.GroupDao
import agzam.vyatsu.data.local.GroupEntity
import agzam.vyatsu.data.remote.GroupsParser
import agzam.vyatsu.data.remote.VyatsuHttpClient
import agzam.vyatsu.data.remote.VyatsuUrls
import agzam.vyatsu.domain.model.Group
import agzam.vyatsu.domain.model.Season
import agzam.vyatsu.domain.repository.GroupRepository
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
class GroupRepositoryImpl(
private val groupDao: GroupDao,
) : GroupRepository {
override fun observeGroups(): Flow<List<Group>> =
groupDao.observeAll().map { list -> list.map { it.toDomain() } }
override suspend fun getGroups(): List<Group> =
groupDao.observeAll().first().map { it.toDomain() }
override suspend fun getFaculties(): List<String> = groupDao.getFaculties()
override suspend fun refreshIfStale(): Boolean {
val last = groupDao.maxSyncedAt()
if (last != null && now() - last < STALE_WINDOW_MS) {
return true
}
refresh()
return true
}
override suspend fun forceRefresh(): Boolean {
refresh()
return true
}
private suspend fun refresh() {
val html = VyatsuHttpClient.getText(VyatsuUrls.GROUPS_PAGE)
val groups = GroupsParser.parseGroups(html)
val ranges = GroupsParser.parseDateRanges(html)
val entities = groups.map { group ->
val range = ranges[group.id]
val autumn = range?.get(Season.AUTUMN)?.lastOrNull()
val spring = range?.get(Season.SPRING)?.lastOrNull()
GroupEntity(
id = group.id,
name = group.name,
faculty = group.faculty,
autumnFirst = autumn?.first ?: "",
autumnSecond = autumn?.second ?: "",
springFirst = spring?.first ?: "",
springSecond = spring?.second ?: "",
lastSyncedAt = now()
)
}
groupDao.insertAll(entities)
}
private fun now(): Long = System.currentTimeMillis()
private fun GroupEntity.toDomain(): Group = Group(
id = id,
name = name,
faculty = faculty
)
companion object {
private const val STALE_WINDOW_MS = 12 * 60 * 60 * 1000L
}
}
@@ -0,0 +1,12 @@
package agzam.vyatsu.data.repository
import agzam.vyatsu.data.local.PreferencesDao
import agzam.vyatsu.domain.repository.PreferencesRepository
class PreferencesRepositoryImpl(
private val dao: PreferencesDao
) : PreferencesRepository {
override suspend fun selectedGroupId(): String? = dao.selectedGroupId()
override suspend fun setSelectedGroup(groupId: String) = dao.setSelectedGroup(groupId)
override suspend fun clearSelectedGroup() = dao.clearSelectedGroup()
}
@@ -0,0 +1,105 @@
package agzam.vyatsu.data.repository
import agzam.vyatsu.data.local.ScheduleCodec
import agzam.vyatsu.data.local.ScheduleDao
import agzam.vyatsu.data.local.ScheduleEntity
import agzam.vyatsu.data.local.GroupDao
import agzam.vyatsu.data.local.GroupEntity
import agzam.vyatsu.data.remote.pdf.PdfScheduleParser
import agzam.vyatsu.data.remote.VyatsuHttpClient
import agzam.vyatsu.data.remote.VyatsuUrls
import agzam.vyatsu.domain.ScheduleDates
import agzam.vyatsu.domain.model.DateRange
import agzam.vyatsu.domain.model.Schedule
import agzam.vyatsu.domain.model.Season
import agzam.vyatsu.domain.repository.ScheduleRepository
import android.util.Log
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
class ScheduleRepositoryImpl(
private val scheduleDao: ScheduleDao,
private val groupDao: GroupDao,
private val codec: ScheduleCodec
) : ScheduleRepository {
override fun observeSchedule(groupId: String): Flow<Schedule?> =
scheduleDao.observe(groupId).map { it?.toDomain() }
override suspend fun getSchedule(groupId: String): Schedule? =
scheduleDao.get(groupId)?.toDomain()
override suspend fun sync(groupId: String): Schedule? {
Log.d(TAG, "sync(groupId=$groupId) start")
val group = groupDao.getById(groupId)
if (group == null) {
Log.w(TAG, "sync: group $groupId not found in DB")
return null
}
val season = ScheduleDates.currentSeason()
val range = dateRangeFor(group, season)
if (range == null) {
Log.w(TAG, "sync: no date range for season $season (group ${group.id}, " +
"autumn=[${group.autumnFirst}, ${group.autumnSecond}], " +
"spring=[${group.springFirst}, ${group.springSecond}])")
return null
}
Log.d(TAG, "sync: season=$season range=[${range.first}, ${range.second}]")
return try {
val pdfUrl = VyatsuUrls.groupPdfUrl(groupId, season, range)
Log.d(TAG, "sync: downloading $pdfUrl")
val bytes = VyatsuHttpClient.getBytes(pdfUrl)
Log.d(TAG, "sync: downloaded ${bytes.size} bytes")
val days = PdfScheduleParser.parse(bytes)
Log.d(TAG, "sync: parsed ${days.size} days")
val entity = ScheduleEntity(
groupId = groupId,
season = season.name,
dateRangeFirst = range.first,
dateRangeSecond = range.second,
weeksJson = codec.toJson(days),
lastSyncedAt = now()
)
scheduleDao.upsert(entity)
Log.d(TAG, "sync: upserted schedule for $groupId")
entity.toDomain()
} catch (e: Throwable) {
Log.e(TAG, "sync: failed for $groupId (${e::class.simpleName}: ${e.message})", e)
null
}
}
override suspend fun isStale(groupId: String): Boolean {
val entity = scheduleDao.get(groupId) ?: return true
return now() - entity.lastSyncedAt > STALE_WINDOW_MS
}
override suspend fun clear(groupId: String) = scheduleDao.delete(groupId)
private fun dateRangeFor(group: GroupEntity, season: Season): DateRange? {
return when (season) {
Season.AUTUMN -> if (group.autumnFirst.isNotEmpty() && group.autumnSecond.isNotEmpty()) {
DateRange(group.autumnFirst, group.autumnSecond)
} else null
Season.SPRING -> if (group.springFirst.isNotEmpty() && group.springSecond.isNotEmpty()) {
DateRange(group.springFirst, group.springSecond)
} else null
}
}
private fun now(): Long = System.currentTimeMillis()
private fun ScheduleEntity.toDomain(): Schedule = Schedule(
groupId = groupId,
season = Season.valueOf(season),
days = codec.fromJson(weeksJson)
)
companion object {
private const val STALE_WINDOW_MS = 12 * 60 * 60 * 1000L
private const val TAG = "ScheduleRepo"
}
}