103 lines
5.9 KiB
Markdown
103 lines
5.9 KiB
Markdown
# radiOLA — Project Memory
|
||
|
||
## Overview
|
||
Android online radio app combining **Radio Record** (Russian dance radio network) with **Radio Browser API** (`radio-browser.info`) for genre filtering across thousands of internet radio stations.
|
||
|
||
**Stack:** Kotlin 2.0, Compose BOM 2024.06, Material3, Hilt 2.51.1, Media3 ExoPlayer 1.3.1, Room 2.6.1, Retrofit + Kotlinx Serialization, Coil 2.6.0, DataStore, Lucide icons.
|
||
|
||
---
|
||
|
||
## Architecture
|
||
Clean Architecture with these layers:
|
||
- `domain/` — models (`Station`, `Track`, `DeeplinkService`), repository interfaces, use cases
|
||
- `data/remote/` — Retrofit APIs (`RecordApi`, `RadioBrowserApi`), DTOs, mappers
|
||
- `data/local/` — Room (`AppDatabase` v2), DAOs (`StationDao`, `TagDao`, `TrackHistoryDao`)
|
||
- `data/repository/` — impls (`StationRepositoryImpl`, `RadioBrowserRepositoryImpl`, etc.)
|
||
- `di/` — Hilt `AppModule` (two Retrofit instances: Record + Radio Browser)
|
||
- `ui/` — Compose screens (`StationsScreen`, `HistoryScreen`, `SettingsScreen`, `PlayerBottomSheet`)
|
||
- `service/` — `PlayerController` (ExoPlayer singleton), `PlayerService` (`MediaSessionService`)
|
||
- `widget/` — `PlayerWidgetProvider` (4×1 home screen widget)
|
||
|
||
---
|
||
|
||
## Current State
|
||
|
||
### ✅ Working
|
||
- **Radio Record stations** load from `https://www.radiorecord.ru/api/stations/` → cached in Room
|
||
- **Stations grid** (`LazyVerticalGrid`, 2 columns) with cover, name, favorite heart
|
||
- **Playback** — real stream URLs via ExoPlayer, background service, notification, lock screen
|
||
- **Favorites** — Room toggle, context-aware next/prev (all vs favorites only)
|
||
- **History** — auto-saves tracks to `TrackHistoryRepository` on every `_currentTrack` change
|
||
- **Audio devices** — pauses on BT/wired/USB removal, resumes on reconnect
|
||
- **Deeplink buttons** — search current track in Yandex, Spotify, etc.
|
||
- **Genre chips** — `FilterChips` row with music genres only
|
||
- **Radio Browser integration** — loads stations by tag when VPN is active
|
||
- **Icy metadata parsing** for Radio Browser streams (`Artist - Song` format)
|
||
|
||
### ⚠️ Known Issues / Limitations
|
||
1. **Radio Browser API blocked** by some ISPs in Russia (IP `91.98.4.78` times out). App detects this and disables Radio Browser features gracefully. **Workaround: use VPN**.
|
||
2. **Radio Record API** does not return per-station tags. Tags are global (`StationsResult.tags`). We map them to Room `TagEntity` table via `TagDao`.
|
||
3. **Record station-to-genre mapping** is approximate — uses keyword matching on `tooltip`/`genre` field because API lacks structured genre data per station.
|
||
4. **Widget** uses `RemoteViews` with limited Compose support; `widget_player.xml` avoids AppCompat attrs.
|
||
5. **Deprecated `Divider`** in `HistoryScreen.kt:60` — should be `HorizontalDivider`.
|
||
6. **KSP `NonExistentClass`** — clean build required if Hilt constructor params change.
|
||
|
||
---
|
||
|
||
## Key Data Flows
|
||
|
||
### Station Loading
|
||
1. `StationsViewModel.init` → `refreshStationsUseCase()` → `StationRepositoryImpl.refreshStations()`
|
||
2. `RecordApi.getStations()` returns `StationsResponse(result = StationsResult(stations, tags))`
|
||
3. `StationDao.insertAll()` + `TagDao.clearAll()` + `TagDao.insertAll()`
|
||
4. `StationsViewModel.tags` combines `stationRepository.getTags()` + `_radioBrowserTags`
|
||
|
||
### Playback
|
||
1. `PlayerViewModel.play(station)` → `PlayerController.play(url, prefix, name)`
|
||
2. **Record source** → `getNowPlayingUseCase(id)` polls `/api/stations/now/` every 10s
|
||
3. **Radio Browser source** → listens to `PlayerController.icyTitle` (ExoPlayer `onMediaMetadataChanged`)
|
||
4. `_currentTrack` updates → auto-saved to history + lock screen metadata via `updateMetadata()`
|
||
|
||
### Genre Filtering
|
||
- `tag == null` → show all Record stations
|
||
- `tag != null` → show Radio Browser stations for tag + Record stations whose `genre`/`name` matches keywords (`tagKeywords` map in `StationsViewModel`)
|
||
|
||
---
|
||
|
||
## Critical Code Rules
|
||
|
||
### ExoPlayer Lifecycle
|
||
`PlayerController` is `@Singleton`. **`PlayerService.onDestroy()` must NOT call `playerController.release()`** — releasing ExoPlayer kills its internal thread. Subsequent `play()` calls throw `IllegalStateException: dead thread` after OS restarts the service.
|
||
|
||
### Room Migrations
|
||
`AppDatabase` is version 2. `MIGRATION_1_2` creates `tags` table. Always add migration + bump version when adding entities.
|
||
|
||
### Radio Browser Null-Safety
|
||
`RadioBrowserStationDto` fields are nullable (`urlResolved`, `tags`, `country`, `codec`). Mapper falls back: `streamUrl = urlResolved ?: url ?: ""`.
|
||
|
||
---
|
||
|
||
## File Inventory
|
||
|
||
| File | Responsibility |
|
||
|------|----------------|
|
||
| `di/AppModule.kt` | Two Retrofit instances, Room DB with migration, repository bindings |
|
||
| `data/repository/StationRepositoryImpl.kt` | Fetch Record API, cache stations + tags in Room |
|
||
| `data/repository/RadioBrowserRepositoryImpl.kt` | Fetch Radio Browser stations/tags, swallow network errors |
|
||
| `ui/stations/StationsViewModel.kt` | Genre filtering, tag loading, search, Radio Browser availability flag |
|
||
| `ui/stations/StationsScreen.kt` | Grid, chips, search bar, empty state with "Show All" button |
|
||
| `service/PlayerController.kt` | ExoPlayer, audio device callback, Icy title capture, MediaSession wrapper |
|
||
| `service/PlayerService.kt` | Background service, MediaSession, notification, lock screen |
|
||
| `ui/player/PlayerViewModel.kt` | Playback orchestration, now playing polling, Icy parsing, playlist |
|
||
| `data/remote/dto/RadioBrowserDto.kt` | DTOs with nullable fields for Radio Browser API |
|
||
| `data/local/AppDatabase.kt` | Room v2 with `StationEntity`, `TrackHistoryEntity`, `TagEntity` |
|
||
|
||
---
|
||
|
||
## Next Steps / Open Items
|
||
- [ ] Replace deprecated `Divider` with `HorizontalDivider` in `HistoryScreen`
|
||
- [ ] Add empty-state illustration or better UX when Radio Browser is unavailable
|
||
- [ ] Consider caching Radio Browser stations per tag in Room for offline reuse
|
||
- [ ] Evaluate if `tagKeywords` mapping for Record stations needs tuning per user feedback
|
||
- [ ] Add error retry / exponential backoff for Radio Browser API calls when VPN is unstable
|