Files
radiola-android/docs/superpowers/memory.md

5.9 KiB
Raw Blame History

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 chipsFilterChips 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.initrefreshStationsUseCase()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 sourcegetNowPlayingUseCase(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