-
Notifications
You must be signed in to change notification settings - Fork 171
Add radio station support #138
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| import 'package:faker/faker.dart'; | ||
|
|
||
| class RadioStation { | ||
| final String id; | ||
| String name; | ||
| String url; | ||
| String? logo; | ||
| String? description; | ||
| bool isPublic; | ||
|
|
||
| RadioStation({ | ||
| required this.id, | ||
| required this.name, | ||
| required this.url, | ||
| this.logo, | ||
| this.description, | ||
| this.isPublic = false, | ||
| }); | ||
|
|
||
| factory RadioStation.fromJson(Map<String, dynamic> json) { | ||
| return RadioStation( | ||
| id: json['id'], | ||
| name: json['name'], | ||
| url: json['url'], | ||
| logo: json['logo'], | ||
| description: json['description'], | ||
| isPublic: json['is_public'] ?? false, | ||
| ); | ||
| } | ||
|
|
||
| factory RadioStation.fake({ | ||
| String? id, | ||
| String? name, | ||
| String? url, | ||
| }) { | ||
| final faker = Faker(); | ||
| return RadioStation( | ||
| id: id ?? faker.guid.guid(), | ||
| name: name ?? '${faker.address.city()} FM', | ||
| url: url ?? 'https://stream.example.com/live', | ||
| ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| import 'dart:async'; | ||
|
|
||
| import 'package:app/main.dart'; | ||
| import 'package:app/models/models.dart'; | ||
| import 'package:app/utils/preferences.dart' as preferences; | ||
| import 'package:flutter/foundation.dart'; | ||
| import 'package:just_audio/just_audio.dart'; | ||
|
|
||
| class RadioPlayerProvider with ChangeNotifier { | ||
| final _player = AudioPlayer(); | ||
| RadioStation? _currentStation; | ||
| var _playing = false; | ||
| var _loading = false; | ||
| String? _streamTitle; | ||
|
|
||
| StreamSubscription? _playingSubscription; | ||
| StreamSubscription? _processingSubscription; | ||
| StreamSubscription? _queuePlaybackSubscription; | ||
|
|
||
| RadioStation? get currentStation => _currentStation; | ||
| bool get playing => _playing; | ||
| bool get loading => _loading; | ||
| bool get active => _currentStation != null; | ||
| String? get streamTitle => _streamTitle; | ||
|
|
||
| RadioPlayerProvider() { | ||
| _playingSubscription = _player.playingStream.listen((playing) { | ||
| _playing = playing; | ||
| notifyListeners(); | ||
| }); | ||
|
|
||
| _processingSubscription = | ||
| _player.processingStateStream.listen((state) { | ||
| _loading = state == ProcessingState.loading || | ||
| state == ProcessingState.buffering; | ||
| notifyListeners(); | ||
| }); | ||
|
|
||
| // When queue playback starts, stop radio | ||
| _queuePlaybackSubscription = | ||
| audioHandler.playbackState.listen((state) { | ||
| if (state.playing && active) { | ||
| stop(); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| Future<void> play(RadioStation station) async { | ||
| // Pause the main queue player when radio starts | ||
| if (audioHandler.playbackState.value.playing) { | ||
| await audioHandler.pause(); | ||
| } | ||
|
|
||
| _currentStation = station; | ||
| _streamTitle = null; | ||
| _loading = true; | ||
| notifyListeners(); | ||
|
|
||
| final streamUrl = | ||
| '${preferences.host}/radio/stream/${station.id}?t=${preferences.audioToken}'; | ||
|
|
||
| try { | ||
| await _player.setUrl(streamUrl); | ||
| await _player.play(); | ||
| } catch (e) { | ||
| _currentStation = null; | ||
| _loading = false; | ||
| notifyListeners(); | ||
| rethrow; | ||
| } | ||
| } | ||
|
|
||
| Future<void> stop() async { | ||
| await _player.stop(); | ||
| _currentStation = null; | ||
| _playing = false; | ||
| _loading = false; | ||
| _streamTitle = null; | ||
| notifyListeners(); | ||
| } | ||
|
|
||
| Future<void> togglePlayPause() async { | ||
| if (_playing) { | ||
| await _player.pause(); | ||
| } else if (_currentStation != null) { | ||
| await _player.play(); | ||
| } | ||
| } | ||
|
|
||
| void updateStreamTitle(String? title) { | ||
| if (title != _streamTitle) { | ||
| _streamTitle = title; | ||
| notifyListeners(); | ||
| } | ||
| } | ||
|
|
||
| @override | ||
| void dispose() { | ||
| _playingSubscription?.cancel(); | ||
| _processingSubscription?.cancel(); | ||
| _queuePlaybackSubscription?.cancel(); | ||
| _player.dispose(); | ||
| super.dispose(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,79 @@ | ||||||||||||||||||||||||||||||||||||||
| import 'package:app/mixins/stream_subscriber.dart'; | ||||||||||||||||||||||||||||||||||||||
| import 'package:app/models/models.dart'; | ||||||||||||||||||||||||||||||||||||||
| import 'package:app/providers/auth_provider.dart'; | ||||||||||||||||||||||||||||||||||||||
| import 'package:app/utils/api_request.dart'; | ||||||||||||||||||||||||||||||||||||||
| import 'package:flutter/foundation.dart'; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| class RadioStationProvider with ChangeNotifier, StreamSubscriber { | ||||||||||||||||||||||||||||||||||||||
| var _stations = <RadioStation>[]; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| List<RadioStation> get stations => _stations.toList(); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| RadioStationProvider() { | ||||||||||||||||||||||||||||||||||||||
| subscribe(AuthProvider.userLoggedOutStream.listen((_) { | ||||||||||||||||||||||||||||||||||||||
| _stations.clear(); | ||||||||||||||||||||||||||||||||||||||
| notifyListeners(); | ||||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+13
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Drop late The logout listener clears One way to guard late responses class RadioStationProvider with ChangeNotifier, StreamSubscriber {
var _stations = <RadioStation>[];
+ var _generation = 0;
@@
RadioStationProvider() {
subscribe(AuthProvider.userLoggedOutStream.listen((_) {
+ _generation++;
_stations.clear();
notifyListeners();
}));
}
@@
Future<void> fetchAll() async {
+ final generation = _generation;
final res = await get('radio/stations');
+ if (generation != _generation) return;
+
_stations = (res as List)
.map<RadioStation>((j) => RadioStation.fromJson(j))
.toList();
notifyListeners();
}Also applies to: 19-24 🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+12
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: cat -n lib/providers/radio_station_provider.dartRepository: koel/player Length of output: 2461 🏁 Script executed: find . -name "stream_subscriber.dart" -o -name "*stream_subscriber*"Repository: koel/player Length of output: 90 🏁 Script executed: cat -n lib/mixins/stream_subscriber.dartRepository: koel/player Length of output: 365 🏁 Script executed: rg -l "StreamSubscriber" --type dart | head -10Repository: koel/player Length of output: 454 🏁 Script executed: for file in lib/providers/playlist_provider.dart lib/providers/podcast_provider.dart lib/providers/recently_played_provider.dart; do
echo "=== $file ===";
grep -A 10 "dispose()" "$file" 2>/dev/null || echo "No dispose() found";
doneRepository: koel/player Length of output: 252 🏁 Script executed: rg "void dispose\(\)" lib/providers/ --type dartRepository: koel/player Length of output: 175 🏁 Script executed: for file in lib/providers/radio_player_provider.dart lib/providers/download_sync_provider.dart; do
echo "=== $file ===";
head -50 "$file";
doneRepository: koel/player Length of output: 3105 🏁 Script executed: for file in lib/providers/radio_player_provider.dart lib/providers/download_sync_provider.dart; do
echo "=== $file ===";
grep -A 10 "void dispose()" "$file";
doneRepository: koel/player Length of output: 386 🏁 Script executed: rg "class RadioStationProvider" -A 2 lib/providers/radio_station_provider.dartRepository: koel/player Length of output: 158 🏁 Script executed: rg "RadioStationProvider" --type dart -B 2 -A 5 | grep -A 7 "dispose\|RadioStationProvider"Repository: koel/player Length of output: 3744 🏁 Script executed: rg "ChangeNotifier" lib/mixins/stream_subscriber.dartRepository: koel/player Length of output: 37 🏁 Script executed: rg "class StreamSubscriber" -A 20 lib/mixins/stream_subscriber.dartRepository: koel/player Length of output: 37 🏁 Script executed: cat -n lib/mixins/stream_subscriber.dartRepository: koel/player Length of output: 365 🏁 Script executed: cd lib && find . -name "*.dart" -exec grep -l "mixin.*ChangeNotifier.*StreamSubscriber\|with ChangeNotifier.*StreamSubscriber\|with StreamSubscriber.*ChangeNotifier" {} \;Repository: koel/player Length of output: 502 🏁 Script executed: for file in lib/providers/album_provider.dart lib/providers/favorite_provider.dart lib/providers/interaction_provider.dart; do
echo "=== $file ===";
tail -20 "$file" | head -15;
doneRepository: koel/player Length of output: 1402 🏁 Script executed: for file in lib/providers/interaction_provider.dart lib/providers/overview_provider.dart lib/providers/playable_provider.dart; do
echo "=== $file ===";
grep -c "void dispose()" "$file" && echo "Has dispose()" || echo "No dispose()";
doneRepository: koel/player Length of output: 237 Override The Suggested fix RadioStationProvider() {
subscribe(AuthProvider.userLoggedOutStream.listen((_) {
_stations.clear();
notifyListeners();
}));
}
+
+ `@override`
+ void dispose() {
+ unsubscribeAll();
+ super.dispose();
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Future<void> fetchAll() async { | ||||||||||||||||||||||||||||||||||||||
| final res = await get('radio/stations'); | ||||||||||||||||||||||||||||||||||||||
| _stations = (res as List) | ||||||||||||||||||||||||||||||||||||||
| .map<RadioStation>((j) => RadioStation.fromJson(j)) | ||||||||||||||||||||||||||||||||||||||
| .toList(); | ||||||||||||||||||||||||||||||||||||||
| notifyListeners(); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Future<RadioStation> create({ | ||||||||||||||||||||||||||||||||||||||
| required String name, | ||||||||||||||||||||||||||||||||||||||
| required String url, | ||||||||||||||||||||||||||||||||||||||
| String? description, | ||||||||||||||||||||||||||||||||||||||
| bool isPublic = false, | ||||||||||||||||||||||||||||||||||||||
| }) async { | ||||||||||||||||||||||||||||||||||||||
| final json = await post('radio/stations', data: { | ||||||||||||||||||||||||||||||||||||||
| 'name': name, | ||||||||||||||||||||||||||||||||||||||
| 'url': url, | ||||||||||||||||||||||||||||||||||||||
| 'is_public': isPublic, | ||||||||||||||||||||||||||||||||||||||
| if (description != null && description.isNotEmpty) | ||||||||||||||||||||||||||||||||||||||
| 'description': description, | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| final station = RadioStation.fromJson(json); | ||||||||||||||||||||||||||||||||||||||
| _stations.add(station); | ||||||||||||||||||||||||||||||||||||||
| notifyListeners(); | ||||||||||||||||||||||||||||||||||||||
| return station; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Future<void> update( | ||||||||||||||||||||||||||||||||||||||
| RadioStation station, { | ||||||||||||||||||||||||||||||||||||||
| required String name, | ||||||||||||||||||||||||||||||||||||||
| required String url, | ||||||||||||||||||||||||||||||||||||||
| String? description, | ||||||||||||||||||||||||||||||||||||||
| bool isPublic = false, | ||||||||||||||||||||||||||||||||||||||
| }) async { | ||||||||||||||||||||||||||||||||||||||
| await put('radio/stations/${station.id}', data: { | ||||||||||||||||||||||||||||||||||||||
| 'name': name, | ||||||||||||||||||||||||||||||||||||||
| 'url': url, | ||||||||||||||||||||||||||||||||||||||
| 'description': description ?? '', | ||||||||||||||||||||||||||||||||||||||
| 'is_public': isPublic, | ||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| station | ||||||||||||||||||||||||||||||||||||||
| ..name = name | ||||||||||||||||||||||||||||||||||||||
| ..url = url | ||||||||||||||||||||||||||||||||||||||
| ..description = description | ||||||||||||||||||||||||||||||||||||||
| ..isPublic = isPublic; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| notifyListeners(); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Future<void> remove(RadioStation station) async { | ||||||||||||||||||||||||||||||||||||||
| delete('radio/stations/${station.id}'); | ||||||||||||||||||||||||||||||||||||||
| _stations.remove(station); | ||||||||||||||||||||||||||||||||||||||
| notifyListeners(); | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+70
to
+73
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Await delete before mutating local state. Line 66 currently fire-and-forgets the server delete, then removes locally. If the request fails, UI and server diverge. Proposed fix Future<void> remove(RadioStation station) async {
- delete('radio/stations/${station.id}');
- _stations.remove(station);
+ await delete('radio/stations/${station.id}');
+ _stations.removeWhere((s) => s.id == station.id);
notifyListeners();
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Future<Map<String, dynamic>> getNowPlaying(RadioStation station) async { | ||||||||||||||||||||||||||||||||||||||
| return await get('radio/stations/${station.id}/now-playing'); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.