# 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