mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45: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_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, (_, __) {});
|
||||
|
||||
|
@ -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],
|
||||
|
@ -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: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/<trackId>', getStreamTrackId);
|
||||
|
||||
const pipeline = Pipeline();
|
||||
|
||||
if (kDebugMode) {
|
||||
pipeline.addMiddleware(logRequests());
|
||||
}
|
||||
|
||||
serve(pipeline.addHandler(router.call), InternetAddress.loopbackIPv4, port)
|
||||
.then((server) {
|
||||
logger
|
||||
AppLogger.log
|
||||
.t('Playback server at http://${server.address.host}:${server.port}');
|
||||
|
||||
ref.onDispose(() {
|
||||
dio.close(force: true);
|
||||
server.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// @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);
|
||||
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",
|
||||
return (server: server, port: port);
|
||||
},
|
||||
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 '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) {
|
||||
|
Loading…
Reference in New Issue
Block a user