mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 07:55:18 +00:00
refactor: merge connect and playback server into one server
This commit is contained in:
parent
cb8d24ff31
commit
064d92d35d
@ -18,14 +18,14 @@ import 'package:spotube/hooks/configurators/use_close_behavior.dart';
|
|||||||
import 'package:spotube/hooks/configurators/use_deep_linking.dart';
|
import 'package:spotube/hooks/configurators/use_deep_linking.dart';
|
||||||
import 'package:spotube/hooks/configurators/use_disable_battery_optimizations.dart';
|
import 'package:spotube/hooks/configurators/use_disable_battery_optimizations.dart';
|
||||||
import 'package:spotube/hooks/configurators/use_get_storage_perms.dart';
|
import 'package:spotube/hooks/configurators/use_get_storage_perms.dart';
|
||||||
|
import 'package:spotube/provider/server/bonsoir.dart';
|
||||||
|
import 'package:spotube/provider/server/server.dart';
|
||||||
import 'package:spotube/provider/tray_manager/tray_manager.dart';
|
import 'package:spotube/provider/tray_manager/tray_manager.dart';
|
||||||
import 'package:spotube/l10n/l10n.dart';
|
import 'package:spotube/l10n/l10n.dart';
|
||||||
import 'package:spotube/models/skip_segment.dart';
|
import 'package:spotube/models/skip_segment.dart';
|
||||||
import 'package:spotube/models/source_match.dart';
|
import 'package:spotube/models/source_match.dart';
|
||||||
import 'package:spotube/provider/connect/clients.dart';
|
import 'package:spotube/provider/connect/clients.dart';
|
||||||
import 'package:spotube/provider/connect/server.dart';
|
|
||||||
import 'package:spotube/provider/palette_provider.dart';
|
import 'package:spotube/provider/palette_provider.dart';
|
||||||
import 'package:spotube/provider/server/server.dart';
|
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
import 'package:spotube/services/cli/cli.dart';
|
import 'package:spotube/services/cli/cli.dart';
|
||||||
@ -130,8 +130,8 @@ class Spotube extends HookConsumerWidget {
|
|||||||
ref.watch(paletteProvider.select((s) => s?.dominantColor?.color));
|
ref.watch(paletteProvider.select((s) => s?.dominantColor?.color));
|
||||||
final router = ref.watch(routerProvider);
|
final router = ref.watch(routerProvider);
|
||||||
|
|
||||||
ref.listen(playbackServerProvider, (_, __) {});
|
ref.listen(serverProvider, (_, __) {});
|
||||||
ref.listen(connectServerProvider, (_, __) {});
|
ref.listen(bonsoirProvider, (_, __) {});
|
||||||
ref.listen(connectClientsProvider, (_, __) {});
|
ref.listen(connectClientsProvider, (_, __) {});
|
||||||
ref.listen(trayManagerProvider, (_, __) {});
|
ref.listen(trayManagerProvider, (_, __) {});
|
||||||
|
|
||||||
|
@ -15,9 +15,9 @@ import 'package:spotube/modules/root/spotube_navigation_bar.dart';
|
|||||||
import 'package:spotube/extensions/context.dart';
|
import 'package:spotube/extensions/context.dart';
|
||||||
import 'package:spotube/hooks/configurators/use_endless_playback.dart';
|
import 'package:spotube/hooks/configurators/use_endless_playback.dart';
|
||||||
import 'package:spotube/pages/home/home.dart';
|
import 'package:spotube/pages/home/home.dart';
|
||||||
import 'package:spotube/provider/connect/server.dart';
|
|
||||||
import 'package:spotube/provider/download_manager_provider.dart';
|
import 'package:spotube/provider/download_manager_provider.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
|
import 'package:spotube/provider/server/routes/connect.dart';
|
||||||
import 'package:spotube/services/connectivity_adapter.dart';
|
import 'package:spotube/services/connectivity_adapter.dart';
|
||||||
import 'package:spotube/utils/persisted_state_notifier.dart';
|
import 'package:spotube/utils/persisted_state_notifier.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
@ -36,6 +36,7 @@ class RootApp extends HookConsumerWidget {
|
|||||||
final downloader = ref.watch(downloadManagerProvider);
|
final downloader = ref.watch(downloadManagerProvider);
|
||||||
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
final scaffoldMessenger = ScaffoldMessenger.of(context);
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
final connectRoutes = ref.watch(serverConnectRoutesProvider);
|
||||||
|
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
@ -90,7 +91,7 @@ class RootApp extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
connectClientStream.listen((clientOrigin) {
|
connectRoutes.connectClientStream.listen((clientOrigin) {
|
||||||
scaffoldMessenger.showSnackBar(
|
scaffoldMessenger.showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
backgroundColor: Colors.yellow[600],
|
backgroundColor: Colors.yellow[600],
|
||||||
|
@ -1,270 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:math';
|
|
||||||
|
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
|
||||||
import 'package:shelf/shelf.dart';
|
|
||||||
import 'package:shelf/shelf_io.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:shelf_router/shelf_router.dart';
|
|
||||||
import 'package:shelf_web_socket/shelf_web_socket.dart';
|
|
||||||
import 'package:spotify/spotify.dart';
|
|
||||||
import 'package:spotube/models/connect/connect.dart';
|
|
||||||
import 'package:spotube/models/logger.dart';
|
|
||||||
import 'package:spotube/provider/connect/clients.dart';
|
|
||||||
import 'package:spotube/provider/history/history.dart';
|
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
|
||||||
import 'package:spotube/services/audio_player/audio_player.dart';
|
|
||||||
import 'package:bonsoir/bonsoir.dart';
|
|
||||||
import 'package:spotube/services/device_info/device_info.dart';
|
|
||||||
import 'package:spotube/utils/primitive_utils.dart';
|
|
||||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
|
||||||
import 'package:spotube/provider/volume_provider.dart';
|
|
||||||
|
|
||||||
final logger = getLogger('ConnectServer');
|
|
||||||
final _connectClientStreamController = StreamController<String>.broadcast();
|
|
||||||
|
|
||||||
Stream<String> get connectClientStream => _connectClientStreamController.stream;
|
|
||||||
|
|
||||||
final connectServerProvider = FutureProvider((ref) async {
|
|
||||||
final enabled =
|
|
||||||
ref.watch(userPreferencesProvider.select((s) => s.enableConnect));
|
|
||||||
final resolvedService = await ref
|
|
||||||
.watch(connectClientsProvider.selectAsync((s) => s.resolvedService));
|
|
||||||
final playbackNotifier = ref.read(proxyPlaylistProvider.notifier);
|
|
||||||
final historyNotifier = ref.read(playbackHistoryProvider.notifier);
|
|
||||||
|
|
||||||
if (!enabled || resolvedService != null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final app = Router();
|
|
||||||
|
|
||||||
app.get(
|
|
||||||
"/ping",
|
|
||||||
(Request req) {
|
|
||||||
return Response.ok("pong");
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
final subscriptions = <StreamSubscription>[];
|
|
||||||
|
|
||||||
FutureOr<Response> websocket(Request req) => webSocketHandler(
|
|
||||||
(WebSocketChannel channel, String? protocol) async {
|
|
||||||
final context =
|
|
||||||
(req.context["shelf.io.connection_info"] as HttpConnectionInfo?);
|
|
||||||
final origin =
|
|
||||||
"${context?.remoteAddress.host}:${context?.remotePort}";
|
|
||||||
_connectClientStreamController.add(origin);
|
|
||||||
|
|
||||||
ref.listen(
|
|
||||||
proxyPlaylistProvider,
|
|
||||||
(previous, next) {
|
|
||||||
channel.sink.add(
|
|
||||||
WebSocketQueueEvent(next).toJson(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
fireImmediately: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
// because audioPlayer events doesn't fireImmediately
|
|
||||||
channel.sink.add(
|
|
||||||
WebSocketPlayingEvent(audioPlayer.isPlaying).toJson(),
|
|
||||||
);
|
|
||||||
channel.sink.add(
|
|
||||||
WebSocketPositionEvent(await audioPlayer.position ?? Duration.zero)
|
|
||||||
.toJson(),
|
|
||||||
);
|
|
||||||
channel.sink.add(
|
|
||||||
WebSocketDurationEvent(await audioPlayer.duration ?? Duration.zero)
|
|
||||||
.toJson(),
|
|
||||||
);
|
|
||||||
channel.sink.add(
|
|
||||||
WebSocketShuffleEvent(audioPlayer.isShuffled).toJson(),
|
|
||||||
);
|
|
||||||
channel.sink.add(
|
|
||||||
WebSocketLoopEvent(audioPlayer.loopMode).toJson(),
|
|
||||||
);
|
|
||||||
channel.sink.add(
|
|
||||||
WebSocketVolumeEvent(audioPlayer.volume).toJson(),
|
|
||||||
);
|
|
||||||
|
|
||||||
subscriptions.addAll([
|
|
||||||
audioPlayer.positionStream.listen(
|
|
||||||
(position) {
|
|
||||||
channel.sink.add(
|
|
||||||
WebSocketPositionEvent(position).toJson(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
audioPlayer.playingStream.listen(
|
|
||||||
(playing) {
|
|
||||||
channel.sink.add(
|
|
||||||
WebSocketPlayingEvent(playing).toJson(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
audioPlayer.durationStream.listen(
|
|
||||||
(duration) {
|
|
||||||
channel.sink.add(
|
|
||||||
WebSocketDurationEvent(duration).toJson(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
audioPlayer.shuffledStream.listen(
|
|
||||||
(shuffled) {
|
|
||||||
channel.sink.add(
|
|
||||||
WebSocketShuffleEvent(shuffled).toJson(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
audioPlayer.loopModeStream.listen(
|
|
||||||
(loopMode) {
|
|
||||||
channel.sink.add(
|
|
||||||
WebSocketLoopEvent(loopMode).toJson(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
audioPlayer.volumeStream.listen(
|
|
||||||
(volume) {
|
|
||||||
channel.sink.add(
|
|
||||||
WebSocketVolumeEvent(volume).toJson(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
channel.stream.listen(
|
|
||||||
(message) {
|
|
||||||
try {
|
|
||||||
final event = WebSocketEvent.fromJson(
|
|
||||||
jsonDecode(message),
|
|
||||||
(data) => data,
|
|
||||||
);
|
|
||||||
|
|
||||||
event.onLoad((event) async {
|
|
||||||
await playbackNotifier.load(
|
|
||||||
event.data.tracks,
|
|
||||||
autoPlay: true,
|
|
||||||
initialIndex: event.data.initialIndex ?? 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (event.data.collectionId == null) return;
|
|
||||||
playbackNotifier.addCollection(event.data.collectionId!);
|
|
||||||
if (event.data.collection is AlbumSimple) {
|
|
||||||
historyNotifier
|
|
||||||
.addAlbums([event.data.collection as AlbumSimple]);
|
|
||||||
} else {
|
|
||||||
historyNotifier.addPlaylists(
|
|
||||||
[event.data.collection as PlaylistSimple]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
event.onPause((event) async {
|
|
||||||
await audioPlayer.pause();
|
|
||||||
});
|
|
||||||
|
|
||||||
event.onResume((event) async {
|
|
||||||
await audioPlayer.resume();
|
|
||||||
});
|
|
||||||
|
|
||||||
event.onStop((event) async {
|
|
||||||
await audioPlayer.stop();
|
|
||||||
});
|
|
||||||
|
|
||||||
event.onNext((event) async {
|
|
||||||
await playbackNotifier.next();
|
|
||||||
});
|
|
||||||
|
|
||||||
event.onPrevious((event) async {
|
|
||||||
await playbackNotifier.previous();
|
|
||||||
});
|
|
||||||
|
|
||||||
event.onJump((event) async {
|
|
||||||
await playbackNotifier.jumpTo(event.data);
|
|
||||||
});
|
|
||||||
|
|
||||||
event.onSeek((event) async {
|
|
||||||
await audioPlayer.seek(event.data);
|
|
||||||
});
|
|
||||||
|
|
||||||
event.onShuffle((event) async {
|
|
||||||
await audioPlayer.setShuffle(event.data);
|
|
||||||
});
|
|
||||||
|
|
||||||
event.onLoop((event) async {
|
|
||||||
await audioPlayer.setLoopMode(event.data);
|
|
||||||
});
|
|
||||||
|
|
||||||
event.onAddTrack((event) async {
|
|
||||||
await playbackNotifier.addTrack(event.data);
|
|
||||||
});
|
|
||||||
|
|
||||||
event.onRemoveTrack((event) async {
|
|
||||||
await playbackNotifier.removeTrack(event.data);
|
|
||||||
});
|
|
||||||
|
|
||||||
event.onReorder((event) async {
|
|
||||||
await playbackNotifier.moveTrack(
|
|
||||||
event.data.oldIndex,
|
|
||||||
event.data.newIndex,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
event.onVolume((event) async {
|
|
||||||
ref.read(volumeProvider.notifier).setVolume(event.data);
|
|
||||||
});
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
AppLogger.reportError(e, stackTrace);
|
|
||||||
channel.sink.add(WebSocketErrorEvent(e.toString()).toJson());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDone: () {
|
|
||||||
logger.i('Connection closed');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
)(req);
|
|
||||||
|
|
||||||
final port = Random().nextInt(17000) + 3000;
|
|
||||||
|
|
||||||
final server = await serve(
|
|
||||||
(request) {
|
|
||||||
if (request.url.path.startsWith('ws')) {
|
|
||||||
return websocket(request);
|
|
||||||
}
|
|
||||||
return app(request);
|
|
||||||
},
|
|
||||||
InternetAddress.anyIPv4,
|
|
||||||
port,
|
|
||||||
);
|
|
||||||
|
|
||||||
logger.i('Server running on http://${server.address.host}:${server.port}');
|
|
||||||
|
|
||||||
final service = BonsoirService(
|
|
||||||
name: await DeviceInfoService.instance.computerName(),
|
|
||||||
type: '_spotube._tcp',
|
|
||||||
port: port,
|
|
||||||
attributes: {
|
|
||||||
"id": PrimitiveUtils.uuid.v4(),
|
|
||||||
"deviceId": await DeviceInfoService.instance.deviceId(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
final broadcast = BonsoirBroadcast(service: service);
|
|
||||||
|
|
||||||
await broadcast.ready;
|
|
||||||
await broadcast.start();
|
|
||||||
|
|
||||||
ref.onDispose(() async {
|
|
||||||
logger.i('Stopping server');
|
|
||||||
for (final subscription in subscriptions) {
|
|
||||||
await subscription.cancel();
|
|
||||||
}
|
|
||||||
await broadcast.stop();
|
|
||||||
await server.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
return app;
|
|
||||||
});
|
|
41
lib/provider/server/bonsoir.dart
Normal file
41
lib/provider/server/bonsoir.dart
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import 'package:bonsoir/bonsoir.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:spotube/provider/connect/clients.dart';
|
||||||
|
import 'package:spotube/provider/server/server.dart';
|
||||||
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
|
import 'package:spotube/services/device_info/device_info.dart';
|
||||||
|
import 'package:spotube/utils/primitive_utils.dart';
|
||||||
|
|
||||||
|
final bonsoirProvider = FutureProvider((ref) async {
|
||||||
|
final enabled = ref.watch(
|
||||||
|
userPreferencesProvider.select((s) => s.enableConnect),
|
||||||
|
);
|
||||||
|
final resolvedService = await ref.watch(
|
||||||
|
connectClientsProvider.selectAsync((s) => s.resolvedService),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!enabled || resolvedService != null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final (server: _, :port) = await ref.watch(serverProvider.future);
|
||||||
|
|
||||||
|
final service = BonsoirService(
|
||||||
|
name: await DeviceInfoService.instance.computerName(),
|
||||||
|
type: '_spotube._tcp',
|
||||||
|
port: port,
|
||||||
|
attributes: {
|
||||||
|
"id": PrimitiveUtils.uuid.v4(),
|
||||||
|
"deviceId": await DeviceInfoService.instance.deviceId(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
final broadcast = BonsoirBroadcast(service: service);
|
||||||
|
|
||||||
|
await broadcast.ready;
|
||||||
|
await broadcast.start();
|
||||||
|
|
||||||
|
ref.onDispose(() async {
|
||||||
|
await broadcast.stop();
|
||||||
|
});
|
||||||
|
});
|
11
lib/provider/server/pipeline.dart
Normal file
11
lib/provider/server/pipeline.dart
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:shelf/shelf.dart';
|
||||||
|
|
||||||
|
final pipelineProvider = Provider((ref) {
|
||||||
|
const pipeline = Pipeline();
|
||||||
|
if (kDebugMode) {
|
||||||
|
pipeline.addMiddleware(logRequests());
|
||||||
|
}
|
||||||
|
return pipeline;
|
||||||
|
});
|
20
lib/provider/server/router.dart
Normal file
20
lib/provider/server/router.dart
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:shelf/shelf.dart';
|
||||||
|
import 'package:shelf_router/shelf_router.dart';
|
||||||
|
import 'package:spotube/provider/server/routes/connect.dart';
|
||||||
|
import 'package:spotube/provider/server/routes/playback.dart';
|
||||||
|
|
||||||
|
final serverRouterProvider = Provider((ref) {
|
||||||
|
final playbackRoutes = ref.watch(serverPlaybackRoutesProvider);
|
||||||
|
final connectRoutes = ref.watch(serverConnectRoutesProvider);
|
||||||
|
|
||||||
|
final router = Router();
|
||||||
|
|
||||||
|
router.get("/ping", (Request request) => Response.ok("pong"));
|
||||||
|
|
||||||
|
router.get("/stream/<trackId>", playbackRoutes.getStreamTrackId);
|
||||||
|
|
||||||
|
router.all("/ws", connectRoutes.websocket);
|
||||||
|
|
||||||
|
return router;
|
||||||
|
});
|
205
lib/provider/server/routes/connect.dart
Normal file
205
lib/provider/server/routes/connect.dart
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:shelf/shelf.dart';
|
||||||
|
import 'package:shelf_web_socket/shelf_web_socket.dart';
|
||||||
|
import 'package:spotify/spotify.dart';
|
||||||
|
import 'package:spotube/models/connect/connect.dart';
|
||||||
|
import 'package:spotube/models/logger.dart';
|
||||||
|
import 'package:spotube/provider/history/history.dart';
|
||||||
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
|
import 'package:spotube/provider/volume_provider.dart';
|
||||||
|
import 'package:spotube/services/audio_player/audio_player.dart';
|
||||||
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||||
|
|
||||||
|
extension _WebsocketSinkExts on WebSocketSink {
|
||||||
|
void addEvent(WebSocketEvent event) {
|
||||||
|
add(event.toJson());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ServerConnectRoutes {
|
||||||
|
final Ref ref;
|
||||||
|
final StreamController<String> _connectClientStreamController;
|
||||||
|
final List<StreamSubscription> subscriptions;
|
||||||
|
final SpotubeLogger logger;
|
||||||
|
ServerConnectRoutes(this.ref)
|
||||||
|
: _connectClientStreamController = StreamController<String>.broadcast(),
|
||||||
|
subscriptions = [],
|
||||||
|
logger = getLogger('ConnectServer') {
|
||||||
|
ref.onDispose(() {
|
||||||
|
_connectClientStreamController.close();
|
||||||
|
for (final subscription in subscriptions) {
|
||||||
|
subscription.cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ProxyPlaylistNotifier get playbackNotifier =>
|
||||||
|
ref.read(proxyPlaylistProvider.notifier);
|
||||||
|
PlaybackHistoryNotifier get historyNotifier =>
|
||||||
|
ref.read(playbackHistoryProvider.notifier);
|
||||||
|
Stream<String> get connectClientStream =>
|
||||||
|
_connectClientStreamController.stream;
|
||||||
|
|
||||||
|
FutureOr<Response> websocket(Request req) {
|
||||||
|
return webSocketHandler(
|
||||||
|
(
|
||||||
|
WebSocketChannel channel,
|
||||||
|
String? protocol,
|
||||||
|
) async {
|
||||||
|
final context =
|
||||||
|
(req.context["shelf.io.connection_info"] as HttpConnectionInfo?);
|
||||||
|
final origin = "${context?.remoteAddress.host}:${context?.remotePort}";
|
||||||
|
_connectClientStreamController.add(origin);
|
||||||
|
|
||||||
|
ref.listen(
|
||||||
|
proxyPlaylistProvider,
|
||||||
|
(previous, next) {
|
||||||
|
channel.sink.addEvent(WebSocketQueueEvent(next));
|
||||||
|
},
|
||||||
|
fireImmediately: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
// because audioPlayer events doesn't fireImmediately
|
||||||
|
channel.sink.addEvent(WebSocketPlayingEvent(audioPlayer.isPlaying));
|
||||||
|
channel.sink.addEvent(
|
||||||
|
WebSocketPositionEvent(await audioPlayer.position ?? Duration.zero),
|
||||||
|
);
|
||||||
|
channel.sink.addEvent(
|
||||||
|
WebSocketDurationEvent(await audioPlayer.duration ?? Duration.zero),
|
||||||
|
);
|
||||||
|
channel.sink.addEvent(WebSocketShuffleEvent(audioPlayer.isShuffled));
|
||||||
|
channel.sink.addEvent(WebSocketLoopEvent(audioPlayer.loopMode));
|
||||||
|
channel.sink.addEvent(WebSocketVolumeEvent(audioPlayer.volume));
|
||||||
|
|
||||||
|
subscriptions.addAll([
|
||||||
|
audioPlayer.positionStream.listen(
|
||||||
|
(position) {
|
||||||
|
channel.sink.addEvent(WebSocketPositionEvent(position));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
audioPlayer.playingStream.listen(
|
||||||
|
(playing) {
|
||||||
|
channel.sink.addEvent(WebSocketPlayingEvent(playing));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
audioPlayer.durationStream.listen(
|
||||||
|
(duration) {
|
||||||
|
channel.sink.addEvent(WebSocketDurationEvent(duration));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
audioPlayer.shuffledStream.listen(
|
||||||
|
(shuffled) {
|
||||||
|
channel.sink.addEvent(WebSocketShuffleEvent(shuffled));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
audioPlayer.loopModeStream.listen(
|
||||||
|
(loopMode) {
|
||||||
|
channel.sink.addEvent(WebSocketLoopEvent(loopMode));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
audioPlayer.volumeStream.listen(
|
||||||
|
(volume) {
|
||||||
|
channel.sink.addEvent(WebSocketVolumeEvent(volume));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
channel.stream.listen(
|
||||||
|
(message) {
|
||||||
|
try {
|
||||||
|
final event = WebSocketEvent.fromJson(
|
||||||
|
jsonDecode(message),
|
||||||
|
(data) => data,
|
||||||
|
);
|
||||||
|
|
||||||
|
event.onLoad((event) async {
|
||||||
|
await playbackNotifier.load(
|
||||||
|
event.data.tracks,
|
||||||
|
autoPlay: true,
|
||||||
|
initialIndex: event.data.initialIndex ?? 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (event.data.collectionId == null) return;
|
||||||
|
playbackNotifier.addCollection(event.data.collectionId!);
|
||||||
|
if (event.data.collection is AlbumSimple) {
|
||||||
|
historyNotifier
|
||||||
|
.addAlbums([event.data.collection as AlbumSimple]);
|
||||||
|
} else {
|
||||||
|
historyNotifier.addPlaylists(
|
||||||
|
[event.data.collection as PlaylistSimple]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
event.onPause((event) async {
|
||||||
|
await audioPlayer.pause();
|
||||||
|
});
|
||||||
|
|
||||||
|
event.onResume((event) async {
|
||||||
|
await audioPlayer.resume();
|
||||||
|
});
|
||||||
|
|
||||||
|
event.onStop((event) async {
|
||||||
|
await audioPlayer.stop();
|
||||||
|
});
|
||||||
|
|
||||||
|
event.onNext((event) async {
|
||||||
|
await playbackNotifier.next();
|
||||||
|
});
|
||||||
|
|
||||||
|
event.onPrevious((event) async {
|
||||||
|
await playbackNotifier.previous();
|
||||||
|
});
|
||||||
|
|
||||||
|
event.onJump((event) async {
|
||||||
|
await playbackNotifier.jumpTo(event.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
event.onSeek((event) async {
|
||||||
|
await audioPlayer.seek(event.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
event.onShuffle((event) async {
|
||||||
|
await audioPlayer.setShuffle(event.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
event.onLoop((event) async {
|
||||||
|
await audioPlayer.setLoopMode(event.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
event.onAddTrack((event) async {
|
||||||
|
await playbackNotifier.addTrack(event.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
event.onRemoveTrack((event) async {
|
||||||
|
await playbackNotifier.removeTrack(event.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
event.onReorder((event) async {
|
||||||
|
await playbackNotifier.moveTrack(
|
||||||
|
event.data.oldIndex,
|
||||||
|
event.data.newIndex,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
event.onVolume((event) async {
|
||||||
|
ref.read(volumeProvider.notifier).setVolume(event.data);
|
||||||
|
});
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
AppLogger.reportError(e, stackTrace);
|
||||||
|
channel.sink.addEvent(WebSocketErrorEvent(e.toString()));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDone: () {
|
||||||
|
logger.i('Connection closed');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
)(req);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final serverConnectRoutesProvider = Provider((ref) => ServerConnectRoutes(ref));
|
73
lib/provider/server/routes/playback.dart
Normal file
73
lib/provider/server/routes/playback.dart
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import 'package:dio/dio.dart' hide Response;
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:shelf/shelf.dart';
|
||||||
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
||||||
|
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
||||||
|
import 'package:spotube/provider/server/active_sourced_track.dart';
|
||||||
|
import 'package:spotube/provider/server/sourced_track.dart';
|
||||||
|
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
||||||
|
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
||||||
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
|
|
||||||
|
class ServerPlaybackRoutes {
|
||||||
|
final Ref ref;
|
||||||
|
UserPreferences get userPreferences => ref.read(userPreferencesProvider);
|
||||||
|
ProxyPlaylist get playlist => ref.read(proxyPlaylistProvider);
|
||||||
|
final Dio dio;
|
||||||
|
|
||||||
|
ServerPlaybackRoutes(this.ref) : dio = Dio();
|
||||||
|
|
||||||
|
/// @get('/stream/<trackId>')
|
||||||
|
Future<Response> getStreamTrackId(Request request, String trackId) async {
|
||||||
|
try {
|
||||||
|
final track =
|
||||||
|
playlist.tracks.firstWhere((element) => element.id == trackId);
|
||||||
|
final activeSourcedTrack = ref.read(activeSourcedTrackProvider);
|
||||||
|
final sourcedTrack = activeSourcedTrack?.id == track.id
|
||||||
|
? activeSourcedTrack
|
||||||
|
: await ref.read(sourcedTrackProvider(track).future);
|
||||||
|
|
||||||
|
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack);
|
||||||
|
|
||||||
|
final res = await dio.get(
|
||||||
|
sourcedTrack!.url,
|
||||||
|
options: Options(
|
||||||
|
headers: {
|
||||||
|
...request.headers,
|
||||||
|
"User-Agent":
|
||||||
|
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
|
||||||
|
"host": Uri.parse(sourcedTrack.url).host,
|
||||||
|
"Cache-Control": "max-age=0",
|
||||||
|
"Connection": "keep-alive",
|
||||||
|
},
|
||||||
|
responseType: ResponseType.stream,
|
||||||
|
validateStatus: (status) => status! < 500,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final audioStream =
|
||||||
|
(res.data?.stream as Stream<Uint8List>?)?.asBroadcastStream();
|
||||||
|
|
||||||
|
audioStream!.listen(
|
||||||
|
(event) {},
|
||||||
|
cancelOnError: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Response(
|
||||||
|
res.statusCode!,
|
||||||
|
body: audioStream,
|
||||||
|
context: {
|
||||||
|
"shelf.io.buffer_output": false,
|
||||||
|
},
|
||||||
|
headers: res.headers.map,
|
||||||
|
);
|
||||||
|
} catch (e, stack) {
|
||||||
|
AppLogger.reportError(e, stack);
|
||||||
|
return Response.internalServerError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final serverPlaybackRoutesProvider =
|
||||||
|
Provider((ref) => ServerPlaybackRoutes(ref));
|
@ -1,119 +1,35 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:dio/dio.dart' hide Response;
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
||||||
import 'package:logger/logger.dart';
|
|
||||||
import 'package:shelf/shelf.dart';
|
|
||||||
import 'package:shelf/shelf_io.dart';
|
import 'package:shelf/shelf_io.dart';
|
||||||
import 'package:shelf_router/shelf_router.dart';
|
import 'package:spotube/provider/server/pipeline.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/provider/server/router.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
|
|
||||||
import 'package:spotube/provider/server/active_sourced_track.dart';
|
|
||||||
import 'package:spotube/provider/server/sourced_track.dart';
|
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
|
|
||||||
import 'package:spotube/provider/user_preferences/user_preferences_state.dart';
|
|
||||||
|
|
||||||
class PlaybackServer {
|
int serverPort = 0;
|
||||||
final Ref ref;
|
final serverProvider = FutureProvider(
|
||||||
UserPreferences get userPreferences => ref.read(userPreferencesProvider);
|
(ref) async {
|
||||||
ProxyPlaylist get playlist => ref.read(proxyPlaylistProvider);
|
final pipeline = ref.watch(pipelineProvider);
|
||||||
final Logger logger;
|
final router = ref.watch(serverRouterProvider);
|
||||||
final Dio dio;
|
|
||||||
|
|
||||||
final Router router;
|
final port = Random().nextInt(17000) + 1500;
|
||||||
|
|
||||||
static final port = Random().nextInt(17000) + 1500;
|
final server = await serve(
|
||||||
|
pipeline.addHandler(router.call),
|
||||||
|
InternetAddress.anyIPv4,
|
||||||
|
port,
|
||||||
|
);
|
||||||
|
|
||||||
PlaybackServer(this.ref)
|
AppLogger.log
|
||||||
: logger = getLogger('PlaybackServer'),
|
|
||||||
dio = Dio(),
|
|
||||||
router = Router() {
|
|
||||||
router.get('/stream/<trackId>', getStreamTrackId);
|
|
||||||
|
|
||||||
const pipeline = Pipeline();
|
|
||||||
|
|
||||||
if (kDebugMode) {
|
|
||||||
pipeline.addMiddleware(logRequests());
|
|
||||||
}
|
|
||||||
|
|
||||||
serve(pipeline.addHandler(router.call), InternetAddress.loopbackIPv4, port)
|
|
||||||
.then((server) {
|
|
||||||
logger
|
|
||||||
.t('Playback server at http://${server.address.host}:${server.port}');
|
.t('Playback server at http://${server.address.host}:${server.port}');
|
||||||
|
|
||||||
ref.onDispose(() {
|
ref.onDispose(() {
|
||||||
dio.close(force: true);
|
|
||||||
server.close();
|
server.close();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @get('/stream/<trackId>')
|
serverPort = port;
|
||||||
Future<Response> getStreamTrackId(Request request, String trackId) async {
|
|
||||||
try {
|
|
||||||
final track =
|
|
||||||
playlist.tracks.firstWhere((element) => element.id == trackId);
|
|
||||||
final activeSourcedTrack = ref.read(activeSourcedTrackProvider);
|
|
||||||
final sourcedTrack = activeSourcedTrack?.id == track.id
|
|
||||||
? activeSourcedTrack
|
|
||||||
: await ref.read(sourcedTrackProvider(track).future);
|
|
||||||
|
|
||||||
ref.read(activeSourcedTrackProvider.notifier).update(sourcedTrack);
|
return (server: server, port: port);
|
||||||
|
|
||||||
final res = await dio.get(
|
|
||||||
sourcedTrack!.url,
|
|
||||||
options: Options(
|
|
||||||
headers: {
|
|
||||||
...request.headers,
|
|
||||||
"User-Agent":
|
|
||||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
|
|
||||||
"host": Uri.parse(sourcedTrack.url).host,
|
|
||||||
"Cache-Control": "max-age=0",
|
|
||||||
"Connection": "keep-alive",
|
|
||||||
},
|
},
|
||||||
responseType: ResponseType.stream,
|
);
|
||||||
validateStatus: (status) => status! < 500,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final audioStream =
|
|
||||||
(res.data?.stream as Stream<Uint8List>?)?.asBroadcastStream();
|
|
||||||
|
|
||||||
// if (res.statusCode! > 300) {
|
|
||||||
// debugPrint(
|
|
||||||
// "[[Request]]\n"
|
|
||||||
// "URI: ${res.requestOptions.uri}\n"
|
|
||||||
// "Status: ${res.statusCode}\n"
|
|
||||||
// "Request Headers: ${res.requestOptions.headers}\n"
|
|
||||||
// "Response Body: ${res.data}\n"
|
|
||||||
// "Response Headers: ${res.headers.map}",
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
audioStream!.listen(
|
|
||||||
(event) {},
|
|
||||||
cancelOnError: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
res.statusCode!,
|
|
||||||
body: audioStream,
|
|
||||||
context: {
|
|
||||||
"shelf.io.buffer_output": false,
|
|
||||||
},
|
|
||||||
headers: res.headers.map,
|
|
||||||
);
|
|
||||||
} catch (e, stack) {
|
|
||||||
AppLogger.reportError(e, stack);
|
|
||||||
return Response.internalServerError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final playbackServerProvider = Provider<PlaybackServer>((ref) {
|
|
||||||
return PlaybackServer(ref);
|
|
||||||
});
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:spotube/provider/server/server.dart';
|
||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/models/local_track.dart';
|
import 'package:spotube/models/local_track.dart';
|
||||||
import 'package:spotube/provider/server/server.dart';
|
|
||||||
import 'package:spotube/services/audio_player/custom_player.dart';
|
import 'package:spotube/services/audio_player/custom_player.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ class SpotubeMedia extends mk.Media {
|
|||||||
}) : super(
|
}) : super(
|
||||||
track is LocalTrack
|
track is LocalTrack
|
||||||
? track.path
|
? track.path
|
||||||
: "http://${InternetAddress.loopbackIPv4.address}:${PlaybackServer.port}/stream/${track.id}",
|
: "http://${InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}",
|
||||||
extras: {
|
extras: {
|
||||||
...?extras,
|
...?extras,
|
||||||
"track": switch (track) {
|
"track": switch (track) {
|
||||||
|
Loading…
Reference in New Issue
Block a user