Add radiOLA design spec
This commit is contained in:
202
docs/superpowers/specs/2026-06-01-radio-record-design.md
Normal file
202
docs/superpowers/specs/2026-06-01-radio-record-design.md
Normal file
@@ -0,0 +1,202 @@
|
||||
# radiOLA — Design Spec
|
||||
|
||||
**Date:** 2026-06-01
|
||||
**Author:** brainstorming skill
|
||||
**Status:** Approved
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Android-приложение для прослушивания онлайн-радио Radio Record (кодовое название проекта: radiOLA) с фокусом на:
|
||||
- Прослушивание станций с фильтрацией и поиском
|
||||
- Отображение текущего трека (now playing)
|
||||
- Deep links в музыкальные сервисы для быстрого поиска трека
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Language:** Kotlin
|
||||
- **UI:** Jetpack Compose + Material 3
|
||||
- **Navigation:** Jetpack Navigation Compose
|
||||
- **DI:** Hilt
|
||||
- **Network:** Retrofit + Kotlinx Serialization
|
||||
- **Database:** Room
|
||||
- **Preferences:** DataStore
|
||||
- **Player:** ExoPlayer (Media3)
|
||||
- **Background Service:** MediaSessionService
|
||||
- **Images:** Coil
|
||||
- **Testing:** JUnit 5 + MockK + Turbine + Compose UI Test
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
Clean Architecture + Domain Layer (feature-based packages inside single Gradle module).
|
||||
|
||||
```
|
||||
📱 ui/
|
||||
├── player/ # PlayerScreen, PlayerViewModel
|
||||
├── stations/ # StationsScreen, StationsViewModel
|
||||
├── favorites/ # FavoritesScreen
|
||||
├── history/ # HistoryScreen
|
||||
└── settings/ # SettingsScreen
|
||||
🧠 domain/
|
||||
├── model/ # Station, Track, PlayerState, DeeplinkService
|
||||
├── repository/ # Interfaces (StationRepository, NowPlayingRepository, etc.)
|
||||
└── usecase/ # PlayStation, GetNowPlaying, SearchTrackInService, etc.
|
||||
💾 data/
|
||||
├── remote/ # Retrofit API, DTOs, mappers
|
||||
├── local/ # Room Entities, DAOs
|
||||
└── repository/ # Repository implementations
|
||||
🎵 service/ # MediaSessionService (ExoPlayer + MediaSession)
|
||||
🔗 deeplink/ # URL builders for music services
|
||||
📱 widget/ # AppWidgetProvider
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Screens & Navigation
|
||||
|
||||
Bottom Navigation (4 tabs) + Player BottomSheet.
|
||||
|
||||
| Tab | Screen | Content |
|
||||
|-----|--------|---------|
|
||||
| 🎧 Радио | `StationsScreen` | Grid of stations, tag chips (filter), search bar |
|
||||
| ⭐ Избранное | `FavoritesScreen` | Favorite stations grid, quick play |
|
||||
| 🕐 История | `HistoryScreen` | List of last 200 tracks with timestamps, search, swipe actions |
|
||||
| ⚙️ Настройки | `SettingsScreen` | Sleep timer, equalizer, recording toggle |
|
||||
|
||||
**Player:**
|
||||
- Collapsed: mini-bar at bottom (cover 48dp, station name, play/pause)
|
||||
- Expanded (tap): fullscreen with large cover, track info, deep link buttons row
|
||||
|
||||
---
|
||||
|
||||
## Data Sources
|
||||
|
||||
### Remote API (Radio Record)
|
||||
|
||||
| Endpoint | Description |
|
||||
|----------|-------------|
|
||||
| `GET https://www.radiorecord.ru/api/stations/` | List of stations with metadata (id, name, prefix, genre, tags, covers) |
|
||||
| `GET https://air.radiorecord.ru/api/stations/now/` | Current tracks for all stations (artist, song, iTunes covers) |
|
||||
|
||||
Stream URL format: `https://air.radiorecord.ru:805/{prefix}_128`
|
||||
|
||||
Cache policy: stations list cached 1 hour; now playing polled every 10 seconds while player active.
|
||||
|
||||
### Local (Room)
|
||||
|
||||
```kotlin
|
||||
@Entity
|
||||
class StationEntity(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val prefix: String,
|
||||
val streamUrl: String,
|
||||
val coverUrl: String,
|
||||
val sortOrder: Int
|
||||
)
|
||||
|
||||
@Entity
|
||||
class TrackHistoryEntity(
|
||||
val id: Int = 0,
|
||||
val artist: String,
|
||||
val song: String,
|
||||
val stationName: String,
|
||||
val coverUrl: String?,
|
||||
val timestamp: Long
|
||||
)
|
||||
```
|
||||
|
||||
Track history limit: 200 records, FIFO cleanup.
|
||||
|
||||
### Preferences (DataStore)
|
||||
|
||||
- `last_station_id`: last played station
|
||||
- `sleep_timer_minutes`: default timer value
|
||||
- `enabled_deeplink_services`: set of visible service IDs
|
||||
|
||||
---
|
||||
|
||||
## Player & Background Playback
|
||||
|
||||
- **ExoPlayer** inside `MediaSessionService` (Media3)
|
||||
- **MediaSession** for system integration: headphone buttons, Bluetooth, lock screen
|
||||
- **Notification**: MediaStyle with prev/pause/next buttons, cover art, now playing text
|
||||
- **Audio Focus**: auto-pause on call, duck on notifications
|
||||
- **Now Playing Polling**: `GET /api/stations/now/` every 10s, match by `station.id` or `prefix`
|
||||
|
||||
---
|
||||
|
||||
## Deep Links to Music Services
|
||||
|
||||
Supported services: Яндекс Музыка, ВК Музыка, BOOM, Spotify, Apple Music, YouTube Music, Tidal, Deezer.
|
||||
|
||||
URL template: `https://<service>/search?q={artist}+{track}` (URLEncoded).
|
||||
|
||||
Opening: `Intent(ACTION_VIEW, uri)` — system handles app vs browser fallback.
|
||||
|
||||
UI:
|
||||
- Player: horizontal row of service icons below cover art
|
||||
- History: swipe left → "Find in..." → BottomSheet with service list
|
||||
- Settings: toggle visibility per service
|
||||
|
||||
---
|
||||
|
||||
## Additional Features
|
||||
|
||||
| Feature | Implementation |
|
||||
|---------|----------------|
|
||||
| **Favorites** | Room + ⭐ toggle in player/station card + drag-sort |
|
||||
| **History** | Auto-log on track change (deduplicate consecutive duplicates) + search + swipe-to-delete |
|
||||
| **Sleep Timer** | Handler postDelayed / WorkManager; stop service at timeout |
|
||||
| **Equalizer** | Android `Equalizer` AudioEffect; 5 bands + presets (Rock, Pop, Jazz, Flat, Bass); DataStore persistence |
|
||||
| **Stream Recording** | MediaMuxer on ExoPlayer stream; save to `Music/RadioRecord/` via MediaStore; ⚠️ personal use only |
|
||||
| **Widget** | 4x1 AppWidgetProvider; cover + station name + play/pause; updates on track change |
|
||||
|
||||
---
|
||||
|
||||
## Error Handling
|
||||
|
||||
| Scenario | Behavior |
|
||||
|----------|----------|
|
||||
| No internet | Snackbar "Offline mode"; show cached stations from Room; disable play |
|
||||
| API down | Exponential backoff retry (1s→2s→4s→8s, max 5); fallback to cache |
|
||||
| Stream fails | ExoPlayer retry x3; then suggest switching station |
|
||||
| Recording unavailable | Request `WRITE_EXTERNAL_STORAGE`; hide button if denied |
|
||||
| Deep link fails | Toast "Opening in browser" + force chooser |
|
||||
| Bluetooth disconnect | Auto-pause |
|
||||
| Incoming call | Auto-pause; resume after hangup |
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
| Type | Scope | Tools |
|
||||
|------|-------|-------|
|
||||
| Unit | UseCases (filtering, deeplink URL building) | JUnit 5 + MockK |
|
||||
| Unit | Repositories (cache merge, local/remote) | Room in-memory + MockWebServer |
|
||||
| UI | Compose screens (search, filters, navigation) | Compose UI Test + Hilt Test |
|
||||
| Integration | Player (play/pause, station switch) | Espresso + IdlingResource |
|
||||
| Manual | Deep links on real device with all services | — |
|
||||
|
||||
---
|
||||
|
||||
## Out of Scope (Future)
|
||||
|
||||
- Android Auto / Automotive OS
|
||||
- Crossfade between stations
|
||||
- Multiple stream qualities (64/128/320)
|
||||
- Social sharing (send track to friend)
|
||||
- Lyrics display
|
||||
|
||||
---
|
||||
|
||||
## Risks & Assumptions
|
||||
|
||||
1. **Radio Record API stability** — endpoints are unofficial/public; may change without notice. Mitigation: graceful degradation to cached data.
|
||||
2. **Recording legality** — stream recording is for personal use only. Mitigation: clear disclaimer in app; no sharing functionality.
|
||||
3. **Deep link URL changes** — music services may change search URL patterns. Mitigation: configurable URL templates in code; easy to update.
|
||||
Reference in New Issue
Block a user