From 064d92d35d5d2752ac0625d5204be2d098acdc51 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Wed, 12 Jun 2024 20:46:49 +0600 Subject: [PATCH] refactor: merge connect and playback server into one server --- lib/main.dart | 8 +- lib/pages/root/root_app.dart | 5 +- lib/provider/connect/server.dart | 270 -------------------- lib/provider/server/bonsoir.dart | 41 +++ lib/provider/server/pipeline.dart | 11 + lib/provider/server/router.dart | 20 ++ lib/provider/server/routes/connect.dart | 205 +++++++++++++++ lib/provider/server/routes/playback.dart | 73 ++++++ lib/provider/server/server.dart | 130 ++-------- lib/services/audio_player/audio_player.dart | 4 +- 10 files changed, 382 insertions(+), 385 deletions(-) delete mode 100644 lib/provider/connect/server.dart create mode 100644 lib/provider/server/bonsoir.dart create mode 100644 lib/provider/server/pipeline.dart create mode 100644 lib/provider/server/router.dart create mode 100644 lib/provider/server/routes/connect.dart create mode 100644 lib/provider/server/routes/playback.dart diff --git a/lib/main.dart b/lib/main.dart index 75d2ada5..1f5e5909 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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_disable_battery_optimizations.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/l10n/l10n.dart'; import 'package:spotube/models/skip_segment.dart'; import 'package:spotube/models/source_match.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/server/server.dart'; import 'package:spotube/provider/user_preferences/user_preferences_provider.dart'; import 'package:spotube/services/audio_player/audio_player.dart'; import 'package:spotube/services/cli/cli.dart'; @@ -130,8 +130,8 @@ class Spotube extends HookConsumerWidget { ref.watch(paletteProvider.select((s) => s?.dominantColor?.color)); final router = ref.watch(routerProvider); - ref.listen(playbackServerProvider, (_, __) {}); - ref.listen(connectServerProvider, (_, __) {}); + ref.listen(serverProvider, (_, __) {}); + ref.listen(bonsoirProvider, (_, __) {}); ref.listen(connectClientsProvider, (_, __) {}); ref.listen(trayManagerProvider, (_, __) {}); diff --git a/lib/pages/root/root_app.dart b/lib/pages/root/root_app.dart index 6f14a10b..93a84f0a 100644 --- a/lib/pages/root/root_app.dart +++ b/lib/pages/root/root_app.dart @@ -15,9 +15,9 @@ import 'package:spotube/modules/root/spotube_navigation_bar.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/configurators/use_endless_playback.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/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/provider/server/routes/connect.dart'; import 'package:spotube/services/connectivity_adapter.dart'; import 'package:spotube/utils/persisted_state_notifier.dart'; import 'package:spotube/utils/platform.dart'; @@ -36,6 +36,7 @@ class RootApp extends HookConsumerWidget { final downloader = ref.watch(downloadManagerProvider); final scaffoldMessenger = ScaffoldMessenger.of(context); final theme = Theme.of(context); + final connectRoutes = ref.watch(serverConnectRoutesProvider); useEffect(() { WidgetsBinding.instance.addPostFrameCallback((_) async { @@ -90,7 +91,7 @@ class RootApp extends HookConsumerWidget { ); } }), - connectClientStream.listen((clientOrigin) { + connectRoutes.connectClientStream.listen((clientOrigin) { scaffoldMessenger.showSnackBar( SnackBar( backgroundColor: Colors.yellow[600], diff --git a/lib/provider/connect/server.dart b/lib/provider/connect/server.dart deleted file mode 100644 index aeaaf149..00000000 --- a/lib/provider/connect/server.dart +++ /dev/null @@ -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.broadcast(); - -Stream 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 = []; - - FutureOr 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; -}); diff --git a/lib/provider/server/bonsoir.dart b/lib/provider/server/bonsoir.dart new file mode 100644 index 00000000..fcc40e54 --- /dev/null +++ b/lib/provider/server/bonsoir.dart @@ -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(); + }); +}); diff --git a/lib/provider/server/pipeline.dart b/lib/provider/server/pipeline.dart new file mode 100644 index 00000000..8f97ce89 --- /dev/null +++ b/lib/provider/server/pipeline.dart @@ -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; +}); diff --git a/lib/provider/server/router.dart b/lib/provider/server/router.dart new file mode 100644 index 00000000..e2a579cc --- /dev/null +++ b/lib/provider/server/router.dart @@ -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/", playbackRoutes.getStreamTrackId); + + router.all("/ws", connectRoutes.websocket); + + return router; +}); diff --git a/lib/provider/server/routes/connect.dart b/lib/provider/server/routes/connect.dart new file mode 100644 index 00000000..eee3365e --- /dev/null +++ b/lib/provider/server/routes/connect.dart @@ -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 _connectClientStreamController; + final List subscriptions; + final SpotubeLogger logger; + ServerConnectRoutes(this.ref) + : _connectClientStreamController = StreamController.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 get connectClientStream => + _connectClientStreamController.stream; + + FutureOr 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)); diff --git a/lib/provider/server/routes/playback.dart b/lib/provider/server/routes/playback.dart new file mode 100644 index 00000000..dd9d6c3b --- /dev/null +++ b/lib/provider/server/routes/playback.dart @@ -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/') + Future 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?)?.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)); diff --git a/lib/provider/server/server.dart b/lib/provider/server/server.dart index b6a7dfe9..5232bb17 100644 --- a/lib/provider/server/server.dart +++ b/lib/provider/server/server.dart @@ -1,119 +1,35 @@ import 'dart:io'; import 'dart:math'; -import 'package:spotube/services/logger/logger.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:hooks_riverpod/hooks_riverpod.dart'; import 'package:shelf/shelf_io.dart'; -import 'package:shelf_router/shelf_router.dart'; -import 'package:spotube/models/logger.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/provider/server/pipeline.dart'; +import 'package:spotube/provider/server/router.dart'; +import 'package:spotube/services/logger/logger.dart'; -class PlaybackServer { - final Ref ref; - UserPreferences get userPreferences => ref.read(userPreferencesProvider); - ProxyPlaylist get playlist => ref.read(proxyPlaylistProvider); - final Logger logger; - final Dio dio; +int serverPort = 0; +final serverProvider = FutureProvider( + (ref) async { + final pipeline = ref.watch(pipelineProvider); + final router = ref.watch(serverRouterProvider); - 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) - : logger = getLogger('PlaybackServer'), - dio = Dio(), - router = Router() { - router.get('/stream/', getStreamTrackId); + AppLogger.log + .t('Playback server at http://${server.address.host}:${server.port}'); - 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}'); - - ref.onDispose(() { - dio.close(force: true); - server.close(); - }); + ref.onDispose(() { + server.close(); }); - } - /// @get('/stream/') - Future 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); + serverPort = port; - 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?)?.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((ref) { - return PlaybackServer(ref); -}); + return (server: server, port: port); + }, +); diff --git a/lib/services/audio_player/audio_player.dart b/lib/services/audio_player/audio_player.dart index 8b391a07..df23039c 100644 --- a/lib/services/audio_player/audio_player.dart +++ b/lib/services/audio_player/audio_player.dart @@ -1,10 +1,10 @@ import 'dart:io'; +import 'package:spotube/provider/server/server.dart'; import 'package:spotube/services/logger/logger.dart'; import 'package:flutter/foundation.dart'; import 'package:spotify/spotify.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 'dart:async'; @@ -27,7 +27,7 @@ class SpotubeMedia extends mk.Media { }) : super( track is LocalTrack ? track.path - : "http://${InternetAddress.loopbackIPv4.address}:${PlaybackServer.port}/stream/${track.id}", + : "http://${InternetAddress.anyIPv4.address}:$serverPort/stream/${track.id}", extras: { ...?extras, "track": switch (track) {