Skip to content

Commit

Permalink
feat(radio): add return to livestream & play similar station buttons
Browse files Browse the repository at this point in the history
Fixes #937
  • Loading branch information
Feichtmeier committed Oct 9, 2024
1 parent 3790021 commit 8aa1207
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 31 deletions.
7 changes: 5 additions & 2 deletions lib/common/data/audio.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ class Audio {
/// The duration of the audio file or stream. It can be null if was not set
final double? durationMs;

/// The artist(s) of the audio file or stream.
/// The artist(s) of the audio file or stream, for radio stations this is the language.
final String? artist;

/// The album of the audio file or stream.
Expand Down Expand Up @@ -310,11 +310,14 @@ class Audio {
);
}

String? get uuid => description;
String? get language => artist;

factory Audio.fromStation(Station station) {
return Audio(
url: station.urlResolved,
title: station.name,
artist: station.language ?? station.name,
artist: station.language,
album: station.tags ?? '',
audioType: AudioType.radio,
imageUrl: station.favicon,
Expand Down
2 changes: 1 addition & 1 deletion lib/common/view/audio_tile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class _AudioTileState extends State<AudioTile> {
final playerModel = di<PlayerModel>();
final liked = watchPropertyValue((LibraryModel m) => m.liked(widget.audio));
final starred = watchPropertyValue(
(LibraryModel m) => m.isStarredStation(widget.audio.description),
(LibraryModel m) => m.isStarredStation(widget.audio.uuid),
);
final selectedColor = widget.selectedColor ?? theme.contrastyPrimary;
final subTitle = switch (widget.audioPageType) {
Expand Down
8 changes: 4 additions & 4 deletions lib/common/view/like_icon.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,17 @@ class RadioLikeIcon extends StatelessWidget with WatchItMixin {

watchPropertyValue((LibraryModel m) => m.starredStations.length);

final isStarredStation = libraryModel.isStarredStation(audio?.description);
final isStarredStation = libraryModel.isStarredStation(audio?.uuid);

final void Function()? onLike;
if (audio == null && audio?.description == null) {
if (audio == null && audio?.uuid == null) {
onLike = null;
} else {
onLike = () {
isStarredStation
? libraryModel.unStarStation(audio!.description!)
? libraryModel.unStarStation(audio!.uuid!)
: libraryModel.addStarredStation(
audio!.description!,
audio!.uuid!,
[audio!],
);
};
Expand Down
2 changes: 2 additions & 0 deletions lib/l10n/app_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,8 @@
"closeMusicPod": "MusicPod schließen?",
"confirmCloseOrHideTip": "Bitte bestätigen, ob Sie die Anwendung schließen oder ausblenden möchten.",
"doNotAskAgain": "Nicht mehr fragen",
"skipToLivStream": "Zur Live-Übertragung springen",
"searchSimilarStation": "Ähnlichen Sender suchen",
"regionNone": "Keine",
"regionAfghanistan": "Afghanistan",
"regionAlandislands": "Ålandinseln",
Expand Down
2 changes: 2 additions & 0 deletions lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,8 @@
"closeMusicPod": "Close MusicPod?",
"confirmCloseOrHideTip": "Please confirm if you need to close the application or hide it?",
"doNotAskAgain": "Do not ask again",
"skipToLivStream": "Skip to live stream",
"searchSimilarStation": "Search similar station",
"regionNone": "None",
"regionAfghanistan": "Afghanistan",
"regionAlandislands": "Alandislands",
Expand Down
52 changes: 30 additions & 22 deletions lib/player/view/player_main_controls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import '../../common/view/theme.dart';
import '../../extensions/build_context_x.dart';
import '../../extensions/theme_data_x.dart';
import '../../l10n/l10n.dart';
import '../../radio/view/next_station_button.dart';
import '../player_model.dart';
import 'play_button.dart';
import 'repeat_button.dart';
Expand All @@ -31,24 +32,29 @@ class PlayerMainControls extends StatelessWidget with WatchItMixin {
final defaultColor = iconColor ?? theme.colorScheme.onSurface;
final queueLength = watchPropertyValue((PlayerModel m) => m.queue.length);
final audio = watchPropertyValue((PlayerModel m) => m.audio);
final showShuffleAndRepeat = audio?.audioType == AudioType.local;
final showSkipButtons =
queueLength > 1 || audio?.audioType == AudioType.local;
final isOnline = watchPropertyValue((ConnectivityModel m) => m.isOnline);
final active = audio?.path != null || isOnline;

final children = <Widget>[
if (showShuffleAndRepeat)
ShuffleButton(
active: active,
iconColor: defaultColor,
)
else if (audio?.audioType == AudioType.podcast)
SeekButton(
active: active,
forward: false,
iconColor: defaultColor,
),
switch (audio?.audioType) {
AudioType.local => ShuffleButton(
active: active,
iconColor: defaultColor,
),
AudioType.podcast => SeekButton(
active: active,
forward: false,
iconColor: defaultColor,
),
AudioType.radio => IconButton(
tooltip: context.l10n.skipToLivStream,
onPressed: di<PlayerModel>().playNext,
icon: Icon(Iconz().refresh),
),
_ => const SizedBox.shrink()
},
_flex,
if (showSkipButtons)
IconButton(
Expand Down Expand Up @@ -84,16 +90,18 @@ class PlayerMainControls extends StatelessWidget with WatchItMixin {
),
),
_flex,
if (showShuffleAndRepeat)
RepeatButton(
active: active,
iconColor: defaultColor,
)
else if (audio?.audioType == AudioType.podcast)
SeekButton(
active: active,
iconColor: defaultColor,
),
switch (audio?.audioType) {
AudioType.local => RepeatButton(
active: active,
iconColor: defaultColor,
),
AudioType.podcast => SeekButton(
active: active,
iconColor: defaultColor,
),
AudioType.radio => const NextStationButton(),
_ => const SizedBox.shrink(),
},
];

return Row(
Expand Down
40 changes: 40 additions & 0 deletions lib/radio/view/next_station_button.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import '../../common/view/icons.dart';
import '../../l10n/l10n.dart';
import '../../player/player_model.dart';
import '../../search/search_model.dart';
import 'package:flutter/material.dart';
import 'package:watch_it/watch_it.dart';

import '../radio_model.dart';

class NextStationButton extends StatelessWidget with WatchItMixin {
const NextStationButton({super.key});

@override
Widget build(BuildContext context) {
final loading = watchPropertyValue((SearchModel m) => m.loading);
final audio = watchPropertyValue((PlayerModel m) => m.audio);

return IconButton(
tooltip: context.l10n.searchSimilarStation,
onPressed: loading
? null
: () {
if (audio == null) return;
di<RadioModel>().init().then((host) {
if (host == null) return null;
return di<SearchModel>().nextSimilarStation(audio).then(
(station) {
if (station == audio) return;
di<PlayerModel>().startPlaylist(
audios: [station],
listName: station.uuid ?? station.toString(),
);
},
);
});
},
icon: Icon(Iconz().explore),
);
}
}
63 changes: 63 additions & 0 deletions lib/search/search_model.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:math';

import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:podcast_search/podcast_search.dart';
Expand Down Expand Up @@ -162,6 +164,67 @@ class SearchModel extends SafeChangeNotifier {
notifyListeners();
}

List<Station>? searchFromLanguage;
String? lastLanguage;
Future<Station?> _findSimilarStation(Audio station) async {
if (searchFromLanguage == null || station.language != lastLanguage) {
searchFromLanguage = await _radioService.search(
limit: 1000,
language: station.language,
);
}
lastLanguage = station.language;

final noNumbers = RegExp(r'^[^0-9]+$');
return searchFromLanguage
?.where(
(e) => _areTagsSimilar(
stationTags: station.tags?.where((e) => noNumbers.hasMatch(e)),
eTags:
Audio.fromStation(e).tags?.where((e) => noNumbers.hasMatch(e)),
),
)
.firstWhereOrNull((e) => e.stationUUID != station.uuid);
}

bool _areTagsSimilar({
required Iterable<String>? stationTags,
required Iterable<String>? eTags,
}) {
if (eTags == null ||
eTags.isEmpty ||
stationTags == null ||
stationTags.length < 2) {
return false;
}

final random = Random();
final randomOne = random.nextInt(stationTags.length);
var randomTwo = random.nextInt(stationTags.length);
while (randomTwo == randomOne) {
randomTwo = random.nextInt(stationTags.length);
}

return eTags.contains(stationTags.elementAt(randomOne)) &&
eTags.contains(stationTags.elementAt(randomTwo));
}

Future<Audio> nextSimilarStation(Audio station) async {
_loading = true;

Audio? match;
Station? maybe = await _findSimilarStation(station);
if (maybe != null) {
match = Audio.fromStation(maybe);
} else {
match = station;
}

_loading = false;

return match;
}

Future<void> search({
bool clear = false,
bool manualFilter = false,
Expand Down
Loading

0 comments on commit 8aa1207

Please sign in to comment.