203 lines
6.9 KiB
Markdown
203 lines
6.9 KiB
Markdown
# 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.
|