# 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
