refactor: merge connect and playback server into one server

This commit is contained in:
Kingkor Roy Tirtho 2024-06-12 20:46:49 +06:00
parent cb8d24ff31
commit 064d92d35d
10 changed files with 382 additions and 385 deletions

View File

@ -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, (_, __) {});

View File

@ -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],

View File

@ -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;
});

View 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();
});
});

View 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;
});

View 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;
});

View 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));

View 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));

View File

@ -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);
});

View File

@ -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) {