Skip to content

Commit

Permalink
feat: supports synchronizing queue between devices
Browse files Browse the repository at this point in the history
  • Loading branch information
phanan committed Jan 3, 2024
1 parent 884be06 commit 4a18d5c
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 21 deletions.
2 changes: 2 additions & 0 deletions ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@
PRODUCT_BUNDLE_IDENTIFIER = dev.koel.koel;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
Expand Down Expand Up @@ -556,6 +557,7 @@
PRODUCT_BUNDLE_IDENTIFIER = dev.koel.koel;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
Expand Down
82 changes: 79 additions & 3 deletions lib/audio_handler.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import 'dart:io';

import 'package:app/app_state.dart';
import 'package:app/models/models.dart';
import 'package:app/providers/providers.dart';
import 'package:app/utils/api_request.dart';
import 'package:app/values/queue_state.dart';
import 'package:audio_service/audio_service.dart';
import 'package:just_audio/just_audio.dart';
import 'package:app/utils/preferences.dart' as preferences;
import 'package:collection/collection.dart';
import 'package:version/version.dart';
import 'package:app/extensions/extensions.dart';

class KoelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
static const MAX_ERROR_COUNT = 10;
Expand All @@ -13,9 +19,11 @@ class KoelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
late final SongProvider songProvider;
late AudioServiceRepeatMode repeatMode;

var _supportsQueueStateSync = false;
var _errorCount = 0;
var _initialized = false;
var _currentMediaItem = MediaItem(id: '', title: '');

final _player = AudioPlayer();

AudioPlayer get player => _player;
Expand All @@ -41,6 +49,17 @@ class KoelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {

await this.setVolume(preferences.volume);

try {
_supportsQueueStateSync = AppState.get<Version>(['app', 'apiVersion'])! >
Version.parse('6.11.5');
} catch (e) {
print(e);
}

if (_supportsQueueStateSync) {
_trySetUpQueue();
}

_initialized = true;
}

Expand Down Expand Up @@ -97,8 +116,55 @@ class KoelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
});
}

void _trySetUpQueue() async {
final state = AppState.get<QueueState>(['app', 'queueState'])!;

if (state.songs.isEmpty) return;

try {
final songs = songProvider.syncWithVault(state.songs);
await replaceQueue(songs, shuffle: false, autoPlay: false);

if (state.currentSong != null) {
var currentSong = songProvider.syncWithVault(state.currentSong).first;

var queuedMediaItem = queue.value.firstWhereOrNull(
(item) => item.id == currentSong.id,
);

if (queuedMediaItem != null) {
_setPlayerSource(queuedMediaItem);
player.seek(Duration(seconds: state.playbackPosition));
}
}

queue.stream.listen((mediaItems) {
var songIds = mediaItems.map((item) => item.id).toList();
if (songIds.isEmpty) return;

put('queue/state', data: {
'songs': songIds,
'song': _currentMediaItem.id,
});
});

_player.positionStream.throttle(Duration(seconds: 1)).listen((position) {
if (mediaItem.value == null || position.inSeconds % 5 != 0) return;

put('queue/playback-status', data: {
'song': _currentMediaItem.id,
'position': position.inSeconds,
});
});
} catch (e) {
print(e);
}
}

_setPlayerSource(MediaItem mediaItem) async {
_currentMediaItem = mediaItem;
this.mediaItem.add(_currentMediaItem);

final song = songProvider.byId(mediaItem.id)!;
final download = downloadProvider.getForSong(song);

Expand Down Expand Up @@ -173,12 +239,16 @@ class KoelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
if (queue.value.length <= index) return;

final mediaItem = queue.value[index];
this.mediaItem.add(mediaItem);

try {
await _setPlayerSource(mediaItem);
await play();

put('queue/playback-status', data: {
'song': mediaItem.id,
'position': _player.position.inSeconds,
});

// Reset the error count if the song is successfully loaded.
_errorCount = 0;
} catch (e) {
Expand Down Expand Up @@ -236,13 +306,19 @@ class KoelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {

Future<void> setVolume(double value) async => await _player.setVolume(value);

Future<void> replaceQueue(List<Song> songs, {bool shuffle = false}) async {
Future<void> replaceQueue(
List<Song> songs, {
bool shuffle = false,
bool autoPlay = true,
}) async {
final items = await Future.wait(songs.map((song) => song.asMediaItem()));
if (shuffle) items.shuffle();

await updateQueue(items);

await _playAtIndex(0);
if (autoPlay) {
await _playAtIndex(0);
}
}

Future<AudioServiceRepeatMode> rotateRepeatMode() async {
Expand Down
1 change: 1 addition & 0 deletions lib/extensions/extensions.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export 'duration.dart';
export 'song_list.dart';
export 'string.dart';
export 'stream.dart';
17 changes: 17 additions & 0 deletions lib/extensions/stream.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import 'dart:async';

extension StreamExtensions<T> on Stream<T> {
Stream<T> throttle(Duration duration) {
Timer? throttleTimer;
StreamController<T> resultStreamController = StreamController<T>();

listen((event) {
if (throttleTimer == null || !throttleTimer!.isActive) {
throttleTimer = Timer(duration, () {});
resultStreamController.add(event);
}
});

return resultStreamController.stream;
}
}
17 changes: 17 additions & 0 deletions lib/providers/data_provider.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'package:app/app_state.dart';
import 'package:app/providers/providers.dart';
import 'package:app/utils/api_request.dart';
import 'package:app/values/values.dart';
import 'package:flutter/widgets.dart';
import 'package:version/version.dart';

class DataProvider with ChangeNotifier {
final PlaylistProvider _playlistProvider;
Expand All @@ -17,6 +19,21 @@ class DataProvider with ChangeNotifier {
AppState.set(['app', 'cdnUrl'], data['cdn_url']);
AppState.set(['app', 'transcoding'], data['transcoding']);

// Since the API version starts with v, we need to remove it before parsing
AppState.set(
['app', 'apiVersion'],
Version.parse(data['current_version'].toString().substring(1)),
);

if (data.containsKey('queue_state')) {
AppState.set(
['app', 'queueState'],
QueueState.parse(data['queue_state']),
);
} else {
AppState.set(['app', 'queueState'], QueueState.empty());
}

await _playlistProvider.init(data['playlists']);
}
}
17 changes: 0 additions & 17 deletions lib/values/parse_result.dart

This file was deleted.

33 changes: 33 additions & 0 deletions lib/values/queue_state.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'package:app/models/models.dart';

class QueueState {
List<Song> songs;
Song? currentSong;
int playbackPosition;

QueueState({
required this.songs,
this.currentSong,
this.playbackPosition = 0,
});

static parse(Map<String, dynamic> json) {
return QueueState(
songs: (json['songs'] as List<dynamic>)
.map<Song>((song) => Song.fromJson(song))
.toList(),
currentSong: json['current_song'] != null
? Song.fromJson(json['current_song'])
: null,
playbackPosition: json['playback_position'],
);
}

static empty() {
return QueueState(
songs: [],
currentSong: null,
playbackPosition: 0,
);
}
}
2 changes: 1 addition & 1 deletion lib/values/values.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export 'pagination_result.dart';
export 'parse_result.dart';
export 'song_sort_config.dart';
export 'queue_state.dart';
8 changes: 8 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.4"
version:
dependency: "direct main"
description:
name: version
sha256: "3d4140128e6ea10d83da32fef2fa4003fccbf6852217bb854845802f04191f94"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
video_player:
dependency: transitive
description:
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ dependencies:
path_provider: ^2.0.14
scrolls_to_top: ^2.1.0
fading_edge_scrollview: ^3.0.0
version: ^3.0.2

dev_dependencies:
golden_toolkit: ^0.12.0
Expand Down

0 comments on commit 4a18d5c

Please sign in to comment.