mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-02-03 23:52:52 +00:00
fix: stuck because of authState running in main thread and sse no url-request event captured
This commit is contained in:
parent
bd2275a89f
commit
1e1f2ca82c
2
.vscode/launch.json
vendored
2
.vscode/launch.json
vendored
@ -52,7 +52,7 @@
|
|||||||
"--flavor",
|
"--flavor",
|
||||||
"dev"
|
"dev"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
"compounds": []
|
"compounds": []
|
||||||
}
|
}
|
||||||
@ -632,7 +632,7 @@ class SettingsMetadataProviderFormRoute
|
|||||||
SettingsMetadataProviderFormRoute({
|
SettingsMetadataProviderFormRoute({
|
||||||
_i44.Key? key,
|
_i44.Key? key,
|
||||||
required String title,
|
required String title,
|
||||||
required List<void> fields,
|
required List<dynamic> fields,
|
||||||
List<_i41.PageRouteInfo>? children,
|
List<_i41.PageRouteInfo>? children,
|
||||||
}) : super(
|
}) : super(
|
||||||
SettingsMetadataProviderFormRoute.name,
|
SettingsMetadataProviderFormRoute.name,
|
||||||
@ -670,7 +670,7 @@ class SettingsMetadataProviderFormRouteArgs {
|
|||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
|
|
||||||
final List<void> fields;
|
final List<dynamic> fields;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
|
|||||||
@ -7,7 +7,7 @@ part of 'track_sources.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
BasicSourcedTrack _$BasicSourcedTrackFromJson(Map json) => BasicSourcedTrack(
|
BasicSourcedTrack _$BasicSourcedTrackFromJson(Map json) => BasicSourcedTrack(
|
||||||
query: SpotubeTrackObject.fromJson(
|
query: SpotubeFullTrackObject.fromJson(
|
||||||
Map<String, dynamic>.from(json['query'] as Map)),
|
Map<String, dynamic>.from(json['query'] as Map)),
|
||||||
source: json['source'] as String,
|
source: json['source'] as String,
|
||||||
info: SpotubeAudioSourceMatchObject.fromJson(
|
info: SpotubeAudioSourceMatchObject.fromJson(
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import 'package:spotube/models/metadata/metadata.dart';
|
|||||||
@RoutePage()
|
@RoutePage()
|
||||||
class SettingsMetadataProviderFormPage extends HookConsumerWidget {
|
class SettingsMetadataProviderFormPage extends HookConsumerWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final List<MetadataFormFieldObject> fields;
|
final List fields;
|
||||||
const SettingsMetadataProviderFormPage({
|
const SettingsMetadataProviderFormPage({
|
||||||
super.key,
|
super.key,
|
||||||
required this.title,
|
required this.title,
|
||||||
|
|||||||
@ -19,16 +19,19 @@ class MetadataPluginAuthenticatedNotifier extends AsyncNotifier<bool> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `authState` can be called once in the SpotubePlugin's lifetime.
|
||||||
final sub = defaultPlugin.authState().listen((event) async {
|
final sub = defaultPlugin.authState().listen((event) async {
|
||||||
state = AsyncData(await defaultPlugin.auth
|
state = AsyncData(
|
||||||
.isAuthenticated(mpscTx: defaultPlugin.sender));
|
await defaultPlugin.auth.isAuthenticated(mpscTx: defaultPlugin.sender),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
ref.onDispose(() {
|
ref.onDispose(() {
|
||||||
sub.cancel();
|
sub.cancel();
|
||||||
});
|
});
|
||||||
|
|
||||||
return defaultPlugin.auth.isAuthenticated(mpscTx: defaultPlugin.sender);
|
return await defaultPlugin.auth
|
||||||
|
.isAuthenticated(mpscTx: defaultPlugin.sender);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import 'package:spotube/services/dio/dio.dart';
|
|||||||
import 'package:spotube/services/logger/logger.dart';
|
import 'package:spotube/services/logger/logger.dart';
|
||||||
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
import 'package:spotube/services/metadata/errors/exceptions.dart';
|
||||||
import 'package:spotube/services/metadata/metadata.dart';
|
import 'package:spotube/services/metadata/metadata.dart';
|
||||||
|
import 'package:spotube/src/rust/api/plugin/plugin.dart';
|
||||||
import 'package:spotube/utils/service_utils.dart';
|
import 'package:spotube/utils/service_utils.dart';
|
||||||
import 'package:archive/archive.dart';
|
import 'package:archive/archive.dart';
|
||||||
import 'package:pub_semver/pub_semver.dart';
|
import 'package:pub_semver/pub_semver.dart';
|
||||||
@ -598,11 +599,16 @@ final _pluginProvider =
|
|||||||
final pluginsNotifier = ref.read(metadataPluginsProvider.notifier);
|
final pluginsNotifier = ref.read(metadataPluginsProvider.notifier);
|
||||||
final pluginSourceCode = await pluginsNotifier.getPluginSourceCode(config);
|
final pluginSourceCode = await pluginsNotifier.getPluginSourceCode(config);
|
||||||
|
|
||||||
|
final spotubePlugin = SpotubePlugin();
|
||||||
final plugin = MetadataPlugin(
|
final plugin = MetadataPlugin(
|
||||||
|
plugin: spotubePlugin,
|
||||||
|
sender: await spotubePlugin.createContext(
|
||||||
pluginScript: pluginSourceCode,
|
pluginScript: pluginSourceCode,
|
||||||
pluginConfig: config,
|
pluginConfig: config,
|
||||||
serverEndpointUrl: "http://${server.address.host}:$port",
|
serverEndpointUrl: "http://${server.address.host}:$port",
|
||||||
serverSecret: serverSecret,
|
serverSecret: serverSecret,
|
||||||
|
localStorageDir: (await getApplicationSupportDirectory()).path,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
ref.onDispose(() {
|
ref.onDispose(() {
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:spotube/models/metadata/metadata.dart';
|
import 'package:spotube/models/metadata/metadata.dart';
|
||||||
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
|
||||||
|
|
||||||
final metadataPluginUpdateCheckerProvider =
|
final metadataPluginUpdateCheckerProvider =
|
||||||
FutureProvider<PluginUpdateAvailable?>((ref) async {
|
FutureProvider<PluginUpdateAvailable?>((ref) async {
|
||||||
final metadataPluginConfigs = await ref.watch(metadataPluginsProvider.future);
|
try {
|
||||||
|
final metadataPluginConfigs =
|
||||||
|
await ref.watch(metadataPluginsProvider.future);
|
||||||
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
|
final metadataPlugin = await ref.watch(metadataPluginProvider.future);
|
||||||
|
|
||||||
if (metadataPlugin == null ||
|
if (metadataPlugin == null ||
|
||||||
@ -12,14 +15,21 @@ final metadataPluginUpdateCheckerProvider =
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return metadataPlugin.core.checkUpdate(
|
final res = await metadataPlugin.core.checkUpdate(
|
||||||
pluginConfig: metadataPluginConfigs.defaultMetadataPluginConfig!,
|
pluginConfig: metadataPluginConfigs.defaultMetadataPluginConfig!,
|
||||||
mpscTx: metadataPlugin.sender,
|
mpscTx: metadataPlugin.sender,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error checking metadata plugin update: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
final audioSourcePluginUpdateCheckerProvider =
|
final audioSourcePluginUpdateCheckerProvider =
|
||||||
FutureProvider<PluginUpdateAvailable?>((ref) async {
|
FutureProvider<PluginUpdateAvailable?>((ref) async {
|
||||||
|
try {
|
||||||
final audioSourcePluginConfigs =
|
final audioSourcePluginConfigs =
|
||||||
await ref.watch(metadataPluginsProvider.future);
|
await ref.watch(metadataPluginsProvider.future);
|
||||||
final audioSourcePlugin = await ref.watch(audioSourcePluginProvider.future);
|
final audioSourcePlugin = await ref.watch(audioSourcePluginProvider.future);
|
||||||
@ -29,8 +39,14 @@ final audioSourcePluginUpdateCheckerProvider =
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return audioSourcePlugin.core.checkUpdate(
|
final res = await audioSourcePlugin.core.checkUpdate(
|
||||||
pluginConfig: audioSourcePluginConfigs.defaultAudioSourcePluginConfig!,
|
pluginConfig: audioSourcePluginConfigs.defaultAudioSourcePluginConfig!,
|
||||||
mpscTx: audioSourcePlugin.sender,
|
mpscTx: audioSourcePlugin.sender,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error checking audio source plugin update: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
201
lib/provider/server/libs/eventsource_publisher.dart
Normal file
201
lib/provider/server/libs/eventsource_publisher.dart
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import "dart:async";
|
||||||
|
|
||||||
|
import "package:collection/collection.dart";
|
||||||
|
import "package:logging/logging.dart" as log;
|
||||||
|
|
||||||
|
/// Just a simple [Sink] implementation that proxies the [add] and [close]
|
||||||
|
/// methods.
|
||||||
|
class ProxySink<T> implements Sink<T> {
|
||||||
|
void Function(T) onAdd;
|
||||||
|
void Function() onClose;
|
||||||
|
ProxySink({required this.onAdd, required this.onClose});
|
||||||
|
@override
|
||||||
|
void add(t) => onAdd(t);
|
||||||
|
@override
|
||||||
|
void close() => onClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventCache {
|
||||||
|
final int? cacheCapacity;
|
||||||
|
final bool comparableIds;
|
||||||
|
final Map<String, List<Event>> _caches = {};
|
||||||
|
|
||||||
|
EventCache({this.cacheCapacity, this.comparableIds = true});
|
||||||
|
|
||||||
|
void replay(Sink<Event> sink, String lastEventId, [String channel = ""]) {
|
||||||
|
List<Event>? cache = _caches[channel];
|
||||||
|
if (cache == null || cache.isEmpty) {
|
||||||
|
// nothing to replay
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// find the location of lastEventId in the queue
|
||||||
|
int index;
|
||||||
|
if (comparableIds) {
|
||||||
|
// if comparableIds, we can use binary search
|
||||||
|
index = binarySearch(cache, lastEventId);
|
||||||
|
} else {
|
||||||
|
// otherwise, we starts from the last one and look one by one
|
||||||
|
index = cache.length - 1;
|
||||||
|
while (index > 0 && cache[index].id != lastEventId) {
|
||||||
|
index--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (index >= 0) {
|
||||||
|
// add them all to the sink
|
||||||
|
cache.sublist(index).forEach(sink.add);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a new [Event] to the cache(s) of the specified channel(s).
|
||||||
|
/// Please note that we assume events are added with increasing values of
|
||||||
|
/// [Event.id].
|
||||||
|
void add(Event event, [Iterable<String> channels = const [""]]) {
|
||||||
|
for (String channel in channels) {
|
||||||
|
List<Event> cache = _caches.putIfAbsent(channel, () => []);
|
||||||
|
if (cacheCapacity != null && cache.length >= cacheCapacity!) {
|
||||||
|
cache.removeAt(0);
|
||||||
|
}
|
||||||
|
cache.add(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear([Iterable<String> channels = const [""]]) {
|
||||||
|
channels.forEach(_caches.remove);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearAll() {
|
||||||
|
_caches.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Event implements Comparable<Event> {
|
||||||
|
/// An identifier that can be used to allow a client to replay
|
||||||
|
/// missed Events by returning the Last-Event-Id header.
|
||||||
|
/// Return empty string if not required.
|
||||||
|
String? id;
|
||||||
|
|
||||||
|
/// The name of the event. Return empty string if not required.
|
||||||
|
String? event;
|
||||||
|
|
||||||
|
/// The payload of the event.
|
||||||
|
String? data;
|
||||||
|
|
||||||
|
Event({this.id, this.event, this.data});
|
||||||
|
|
||||||
|
Event.message({this.id, this.data}) : event = "message";
|
||||||
|
|
||||||
|
@override
|
||||||
|
int compareTo(Event other) => id!.compareTo(other.id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An EventSource publisher. It can manage different channels of events.
|
||||||
|
/// This class forms the backbone of an EventSource server. To actually serve
|
||||||
|
/// a web server, use this together with [shelf_eventsource] or another server
|
||||||
|
/// implementation.
|
||||||
|
class EventSourcePublisher implements Sink<Event> {
|
||||||
|
log.Logger? logger;
|
||||||
|
EventCache? _cache;
|
||||||
|
|
||||||
|
/// Create a new EventSource server.
|
||||||
|
///
|
||||||
|
/// When using a cache, for efficient replaying, it is advisable to use a
|
||||||
|
/// custom Event implementation that overrides the `Event.compareTo` method.
|
||||||
|
/// F.e. if integer events are used, sorting should be done on integers and
|
||||||
|
/// not on the string representations of them.
|
||||||
|
/// If your Event's id properties are not incremental using
|
||||||
|
/// [Comparable.compare], set [comparableIds] to false.
|
||||||
|
EventSourcePublisher({
|
||||||
|
int cacheCapacity = 0,
|
||||||
|
bool comparableIds = false,
|
||||||
|
bool enableLogging = true,
|
||||||
|
}) {
|
||||||
|
if (cacheCapacity > 0) {
|
||||||
|
_cache = EventCache(cacheCapacity: cacheCapacity);
|
||||||
|
}
|
||||||
|
if (enableLogging) {
|
||||||
|
logger = log.Logger("EventSourceServer");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, List<ProxySink>> _subsByChannel = {};
|
||||||
|
|
||||||
|
/// Creates a Sink for the specified channel.
|
||||||
|
/// The `add` and `remove` methods of this channel are equivalent to the
|
||||||
|
/// respective methods of this class with the specific channel passed along.
|
||||||
|
Sink<Event> channel(String channel) => ProxySink(
|
||||||
|
onAdd: (e) => add(e, channels: [channel]),
|
||||||
|
onClose: () => close(channels: [channel]));
|
||||||
|
|
||||||
|
/// Add a publication to the specified channels.
|
||||||
|
/// By default, only adds to the default channel.
|
||||||
|
@override
|
||||||
|
void add(Event event, {Iterable<String> channels = const [""]}) {
|
||||||
|
for (String channel in channels) {
|
||||||
|
List<ProxySink>? subs = _subsByChannel[channel];
|
||||||
|
if (subs == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_logFiner(
|
||||||
|
"Sending event on channel $channel to ${subs.length} subscribers.");
|
||||||
|
for (var sub in subs) {
|
||||||
|
sub.add(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_cache?.add(event, channels);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close the specified channels.
|
||||||
|
/// All the connections with the subscribers to this channels will be closed.
|
||||||
|
/// By default only closes the default channel.
|
||||||
|
@override
|
||||||
|
void close({Iterable<String> channels = const [""]}) {
|
||||||
|
for (String channel in channels) {
|
||||||
|
List<ProxySink>? subs = _subsByChannel[channel];
|
||||||
|
if (subs == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_logInfo("Closing channel $channel with ${subs.length} subscribers.");
|
||||||
|
for (var sub in subs) {
|
||||||
|
sub.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_cache?.clear(channels);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close all the open channels.
|
||||||
|
void closeAllChannels() => close(channels: _subsByChannel.keys);
|
||||||
|
|
||||||
|
/// Initialize a new subscription and replay when possible.
|
||||||
|
/// Should not be used by the user directly.
|
||||||
|
void newSubscription({
|
||||||
|
required void Function(Event) onEvent,
|
||||||
|
required void Function() onClose,
|
||||||
|
required String channel,
|
||||||
|
String? lastEventId,
|
||||||
|
}) {
|
||||||
|
_logFine("New subscriber on channel $channel.");
|
||||||
|
// create a sink for the subscription
|
||||||
|
ProxySink<Event> sub = ProxySink(onAdd: onEvent, onClose: onClose);
|
||||||
|
// save the subscription
|
||||||
|
_subsByChannel.putIfAbsent(channel, () => []).add(sub);
|
||||||
|
// replay past events
|
||||||
|
if (_cache != null && lastEventId != null) {
|
||||||
|
scheduleMicrotask(() {
|
||||||
|
_logFine("Replaying events on channel $channel from id $lastEventId.");
|
||||||
|
_cache!.replay(sub, lastEventId, channel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _logInfo(message) {
|
||||||
|
logger?.log(log.Level.INFO, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _logFine(message) {
|
||||||
|
logger?.log(log.Level.FINE, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _logFiner(message) {
|
||||||
|
logger?.log(log.Level.FINER, message);
|
||||||
|
}
|
||||||
|
}
|
||||||
106
lib/provider/server/libs/shelf_eventsource.dart
Normal file
106
lib/provider/server/libs/shelf_eventsource.dart
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import "dart:convert";
|
||||||
|
import "dart:io";
|
||||||
|
import "package:shelf/shelf.dart";
|
||||||
|
import "package:spotube/provider/server/libs/eventsource_publisher.dart";
|
||||||
|
|
||||||
|
class EventSourceEncoder extends Converter<Event, List<int>> {
|
||||||
|
final bool compressed;
|
||||||
|
|
||||||
|
const EventSourceEncoder({this.compressed = false});
|
||||||
|
|
||||||
|
static final Map<String, Function> _fields = {
|
||||||
|
"id: ": (e) => e.id,
|
||||||
|
"event: ": (e) => e.event,
|
||||||
|
"data: ": (e) => e.data,
|
||||||
|
};
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<int> convert(Event event) {
|
||||||
|
String payload = convertToString(event);
|
||||||
|
List<int> bytes = utf8.encode(payload);
|
||||||
|
if (compressed) {
|
||||||
|
bytes = gzip.encode(bytes);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
String convertToString(Event event) {
|
||||||
|
String payload = "";
|
||||||
|
for (String prefix in _fields.keys) {
|
||||||
|
String? value = _fields[prefix]?.call(event);
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// multi-line values need the field prefix on every line
|
||||||
|
value = value.replaceAll("\n", "\n$prefix");
|
||||||
|
payload += "$prefix$value\n";
|
||||||
|
}
|
||||||
|
payload += "\n";
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Sink<Event> startChunkedConversion(Sink<List<int>> sink) {
|
||||||
|
Sink<dynamic> inputSink = sink;
|
||||||
|
if (compressed) {
|
||||||
|
inputSink =
|
||||||
|
gzip.encoder.startChunkedConversion(inputSink as Sink<List<int>>);
|
||||||
|
}
|
||||||
|
inputSink =
|
||||||
|
utf8.encoder.startChunkedConversion(inputSink as Sink<List<int>>);
|
||||||
|
return new ProxySink(
|
||||||
|
onAdd: (Event event) => inputSink.add(convertToString(event)),
|
||||||
|
onClose: () => inputSink.close());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a shelf handler for the specified channel.
|
||||||
|
/// This handler can be passed to the [shelf.serve] method.
|
||||||
|
Handler eventSourceHandler(
|
||||||
|
EventSourcePublisher publisher, {
|
||||||
|
String channel = "",
|
||||||
|
bool gzip = false,
|
||||||
|
}) {
|
||||||
|
// define the handler
|
||||||
|
Response shelfHandler(Request request) {
|
||||||
|
if (request.method != "GET") {
|
||||||
|
return Response.notFound(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!request.canHijack) {
|
||||||
|
throw ArgumentError("eventSourceHandler may only be used with a "
|
||||||
|
"server that supports request hijacking.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// set content encoding to gzip if we allow it and the request supports it
|
||||||
|
bool useGzip =
|
||||||
|
gzip && (request.headers["Accept-Encoding"] ?? "").contains("gzip");
|
||||||
|
|
||||||
|
// hijack the raw underlying channel
|
||||||
|
request.hijack((untypedChannel) {
|
||||||
|
var socketChannel = (untypedChannel).cast<List<int>>();
|
||||||
|
// create a regular UTF8 sink to write headers
|
||||||
|
var sink = utf8.encoder.startChunkedConversion(socketChannel.sink);
|
||||||
|
// write headers
|
||||||
|
sink.add("HTTP/1.1 200 OK\r\n"
|
||||||
|
"Content-Type: text/event-stream; charset=utf-8\r\n"
|
||||||
|
"Cache-Control: no-cache, no-store, must-revalidate\r\n"
|
||||||
|
"Connection: keep-alive\r\n");
|
||||||
|
if (useGzip) sink.add("Content-Encoding: gzip\r\n");
|
||||||
|
sink.add("\r\n");
|
||||||
|
|
||||||
|
// create encoder for this connection
|
||||||
|
var encodedSink = EventSourceEncoder(compressed: useGzip)
|
||||||
|
.startChunkedConversion(socketChannel.sink);
|
||||||
|
|
||||||
|
// initialize the new subscription
|
||||||
|
publisher.newSubscription(
|
||||||
|
onEvent: encodedSink.add,
|
||||||
|
onClose: encodedSink.close,
|
||||||
|
channel: channel,
|
||||||
|
lastEventId: request.headers["Last-Event-ID"]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return shelfHandler;
|
||||||
|
}
|
||||||
@ -1,12 +1,13 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shelf/shelf.dart';
|
import 'package:shelf/shelf.dart';
|
||||||
import 'package:shelf_router/shelf_router.dart';
|
import 'package:shelf_router/shelf_router.dart';
|
||||||
|
import 'package:spotube/provider/server/libs/shelf_eventsource.dart';
|
||||||
import 'package:spotube/provider/server/routes/connect.dart';
|
import 'package:spotube/provider/server/routes/connect.dart';
|
||||||
import 'package:spotube/provider/server/routes/playback.dart';
|
import 'package:spotube/provider/server/routes/playback.dart';
|
||||||
import 'package:spotube/provider/server/routes/plugin_apis/form.dart';
|
import 'package:spotube/provider/server/routes/plugin_apis/form.dart';
|
||||||
import 'package:spotube/provider/server/routes/plugin_apis/path_provider.dart';
|
|
||||||
import 'package:spotube/provider/server/routes/plugin_apis/webview.dart';
|
import 'package:spotube/provider/server/routes/plugin_apis/webview.dart';
|
||||||
import 'package:spotube/provider/server/routes/plugin_apis/yt_engine.dart';
|
import 'package:spotube/provider/server/routes/plugin_apis/yt_engine.dart';
|
||||||
|
import 'package:spotube/provider/server/sse_publisher.dart';
|
||||||
|
|
||||||
Handler pluginApiAuthMiddleware(Handler handler) {
|
Handler pluginApiAuthMiddleware(Handler handler) {
|
||||||
return (Request request) {
|
return (Request request) {
|
||||||
@ -25,6 +26,8 @@ final serverRouterProvider = Provider((ref) {
|
|||||||
final webviewRoutes = ref.watch(serverWebviewRoutesProvider);
|
final webviewRoutes = ref.watch(serverWebviewRoutesProvider);
|
||||||
final formRoutes = ref.watch(serverFormRoutesProvider);
|
final formRoutes = ref.watch(serverFormRoutesProvider);
|
||||||
final ytEngineRoutes = ref.watch(serverYTEngineRoutesProvider);
|
final ytEngineRoutes = ref.watch(serverYTEngineRoutesProvider);
|
||||||
|
final publisher = ref.watch(ssePublisherProvider);
|
||||||
|
final sseHandler = eventSourceHandler(publisher);
|
||||||
|
|
||||||
final router = Router();
|
final router = Router();
|
||||||
|
|
||||||
@ -42,8 +45,8 @@ final serverRouterProvider = Provider((ref) {
|
|||||||
pluginApiAuthMiddleware(webviewRoutes.postCreateWebview),
|
pluginApiAuthMiddleware(webviewRoutes.postCreateWebview),
|
||||||
);
|
);
|
||||||
router.get(
|
router.get(
|
||||||
"/plugin-api/webview/<uid>/on-url-request",
|
"/plugin-api/webview/events",
|
||||||
pluginApiAuthMiddleware(webviewRoutes.getOnUrlRequestStream),
|
pluginApiAuthMiddleware(sseHandler),
|
||||||
);
|
);
|
||||||
router.post(
|
router.post(
|
||||||
"/plugin-api/webview/open",
|
"/plugin-api/webview/open",
|
||||||
@ -61,10 +64,6 @@ final serverRouterProvider = Provider((ref) {
|
|||||||
"/plugin-api/form/show",
|
"/plugin-api/form/show",
|
||||||
pluginApiAuthMiddleware(formRoutes.showForm),
|
pluginApiAuthMiddleware(formRoutes.showForm),
|
||||||
);
|
);
|
||||||
router.get(
|
|
||||||
"/plugin/localstorage/directories",
|
|
||||||
pluginApiAuthMiddleware(ServerPathProviderRoutes.getDirectories),
|
|
||||||
);
|
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/plugin-api/yt-engine/search",
|
"/plugin-api/yt-engine/search",
|
||||||
|
|||||||
@ -1,29 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:path_provider/path_provider.dart' as pp;
|
|
||||||
import 'package:shelf/shelf.dart';
|
|
||||||
|
|
||||||
class ServerPathProviderRoutes {
|
|
||||||
static Future<Response> getDirectories(Request request) async {
|
|
||||||
final directories = <String, Directory?>{
|
|
||||||
'temporary': await Future<Directory?>.value(pp.getTemporaryDirectory())
|
|
||||||
.catchError((e) => null),
|
|
||||||
'applicationDocuments':
|
|
||||||
await Future<Directory?>.value(pp.getApplicationDocumentsDirectory())
|
|
||||||
.catchError((e) => null),
|
|
||||||
'applicationSupport':
|
|
||||||
await Future<Directory?>.value(pp.getApplicationSupportDirectory())
|
|
||||||
.catchError((e) => null),
|
|
||||||
'library': await Future<Directory?>.value(pp.getLibraryDirectory())
|
|
||||||
.catchError((e) => null),
|
|
||||||
'externalStorage':
|
|
||||||
await pp.getExternalStorageDirectory().catchError((e) => null),
|
|
||||||
'downloads': await pp.getDownloadsDirectory().catchError((e) => null),
|
|
||||||
}.map((key, value) => MapEntry(key, value?.path));
|
|
||||||
return Response.ok(
|
|
||||||
jsonEncode(directories),
|
|
||||||
headers: {'Content-Type': 'application/json'},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -7,9 +7,10 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||||
import 'package:shelf/shelf.dart';
|
import 'package:shelf/shelf.dart';
|
||||||
import 'package:shelf_router/shelf_router.dart';
|
import 'package:shelf_router/shelf_router.dart';
|
||||||
|
import 'package:spotube/provider/server/libs/eventsource_publisher.dart';
|
||||||
import 'package:spotube/provider/server/server.dart';
|
import 'package:spotube/provider/server/server.dart';
|
||||||
|
import 'package:spotube/provider/server/sse_publisher.dart';
|
||||||
import 'package:spotube/src/plugin_api/webview/webview.dart';
|
import 'package:spotube/src/plugin_api/webview/webview.dart';
|
||||||
import 'package:async/async.dart';
|
|
||||||
import 'package:encrypt/encrypt.dart' as encrypt;
|
import 'package:encrypt/encrypt.dart' as encrypt;
|
||||||
|
|
||||||
class ServerWebviewRoutes {
|
class ServerWebviewRoutes {
|
||||||
@ -17,6 +18,7 @@ class ServerWebviewRoutes {
|
|||||||
ServerWebviewRoutes({required this.ref});
|
ServerWebviewRoutes({required this.ref});
|
||||||
|
|
||||||
final Map<String, Webview> _webviews = {};
|
final Map<String, Webview> _webviews = {};
|
||||||
|
final Map<String, StreamSubscription> _eventSubscriptions = {};
|
||||||
|
|
||||||
String _encryptCookies(dynamic cookies, String secret) {
|
String _encryptCookies(dynamic cookies, String secret) {
|
||||||
final keyBytes = base64.decode(secret);
|
final keyBytes = base64.decode(secret);
|
||||||
@ -39,6 +41,16 @@ class ServerWebviewRoutes {
|
|||||||
|
|
||||||
final webview = Webview(uri: uri.toString());
|
final webview = Webview(uri: uri.toString());
|
||||||
_webviews[webview.uid] = webview;
|
_webviews[webview.uid] = webview;
|
||||||
|
|
||||||
|
_eventSubscriptions[webview.uid] = webview.onUrlRequestStream.listen((url) {
|
||||||
|
ref.read(ssePublisherProvider).add(
|
||||||
|
Event(
|
||||||
|
event: "url-request",
|
||||||
|
data: jsonEncode({'uid': webview.uid, 'url': url}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return Response.ok(
|
return Response.ok(
|
||||||
jsonEncode({'uid': webview.uid}),
|
jsonEncode({'uid': webview.uid}),
|
||||||
encoding: utf8,
|
encoding: utf8,
|
||||||
@ -56,40 +68,16 @@ class ServerWebviewRoutes {
|
|||||||
return Response.notFound('Webview with uid $uid not found');
|
return Response.notFound('Webview with uid $uid not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a stream that merges URL events with keepalive pings
|
|
||||||
final controller = StreamController<List<int>>();
|
|
||||||
|
|
||||||
// Send keepalive comment every 15 seconds to prevent connection timeout
|
|
||||||
final keepaliveTimer = Stream.periodic(
|
|
||||||
const Duration(seconds: 15),
|
|
||||||
(_) => utf8.encode(": keepalive\n\n"),
|
|
||||||
);
|
|
||||||
|
|
||||||
final urlStream = webview.onUrlRequestStream.map((url) {
|
final urlStream = webview.onUrlRequestStream.map((url) {
|
||||||
return utf8.encode("event: url-request\n"
|
final payload = "event: url-request\n"
|
||||||
"data: ${jsonEncode({'url': url})}\n\n");
|
"data: ${jsonEncode({'url': url})}\n\n";
|
||||||
|
|
||||||
|
debugPrint('[server][webview] sending:\n$payload');
|
||||||
|
return utf8.encode(payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Merge both streams
|
|
||||||
final subscription = StreamGroup.merge([keepaliveTimer, urlStream]).listen(
|
|
||||||
(data) {
|
|
||||||
if (!controller.isClosed) {
|
|
||||||
controller.add(data);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDone: () {
|
|
||||||
controller.close();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Clean up when client disconnects
|
|
||||||
controller.onCancel = () {
|
|
||||||
debugPrint('Webview $uid client disconnected');
|
|
||||||
subscription.cancel();
|
|
||||||
};
|
|
||||||
|
|
||||||
return Response.ok(
|
return Response.ok(
|
||||||
controller.stream,
|
urlStream,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'text/event-stream',
|
'Content-Type': 'text/event-stream',
|
||||||
'Cache-Control': 'no-cache',
|
'Cache-Control': 'no-cache',
|
||||||
@ -117,11 +105,14 @@ class ServerWebviewRoutes {
|
|||||||
final uid = body['uid'] as String;
|
final uid = body['uid'] as String;
|
||||||
|
|
||||||
final webview = _webviews[uid];
|
final webview = _webviews[uid];
|
||||||
if (webview == null) {
|
final subscription = _eventSubscriptions[uid];
|
||||||
|
if (webview == null || subscription == null) {
|
||||||
return Response.notFound('Webview with uid $uid not found');
|
return Response.notFound('Webview with uid $uid not found');
|
||||||
}
|
}
|
||||||
|
subscription.cancel();
|
||||||
await webview.close();
|
await webview.close();
|
||||||
|
|
||||||
|
_eventSubscriptions.remove(uid);
|
||||||
_webviews.remove(uid);
|
_webviews.remove(uid);
|
||||||
return Response.ok(null);
|
return Response.ok(null);
|
||||||
}
|
}
|
||||||
@ -149,6 +140,10 @@ class ServerWebviewRoutes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
|
for (final subscription in _eventSubscriptions.values) {
|
||||||
|
await subscription.cancel();
|
||||||
|
}
|
||||||
|
_eventSubscriptions.clear();
|
||||||
for (final webview in _webviews.values) {
|
for (final webview in _webviews.values) {
|
||||||
await webview.close();
|
await webview.close();
|
||||||
}
|
}
|
||||||
|
|||||||
14
lib/provider/server/sse_publisher.dart
Normal file
14
lib/provider/server/sse_publisher.dart
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import 'package:riverpod/riverpod.dart';
|
||||||
|
import 'package:spotube/provider/server/libs/eventsource_publisher.dart';
|
||||||
|
|
||||||
|
final ssePublisherProvider = Provider<EventSourcePublisher>(
|
||||||
|
(ref) {
|
||||||
|
final publisher = EventSourcePublisher(cacheCapacity: 100);
|
||||||
|
|
||||||
|
ref.onDispose(() {
|
||||||
|
publisher.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
return publisher;
|
||||||
|
},
|
||||||
|
);
|
||||||
@ -1,5 +1,4 @@
|
|||||||
import 'package:spotube/src/rust/api/plugin/models/auth.dart';
|
import 'package:spotube/src/rust/api/plugin/models/auth.dart';
|
||||||
import 'package:spotube/src/rust/api/plugin/models/core.dart';
|
|
||||||
import 'package:spotube/src/rust/api/plugin/plugin.dart';
|
import 'package:spotube/src/rust/api/plugin/plugin.dart';
|
||||||
import 'package:spotube/src/rust/api/plugin/senders.dart';
|
import 'package:spotube/src/rust/api/plugin/senders.dart';
|
||||||
|
|
||||||
@ -7,19 +6,7 @@ class MetadataPlugin {
|
|||||||
final SpotubePlugin plugin;
|
final SpotubePlugin plugin;
|
||||||
late final OpaqueSender sender;
|
late final OpaqueSender sender;
|
||||||
|
|
||||||
MetadataPlugin({
|
MetadataPlugin({required this.sender, required this.plugin});
|
||||||
required String pluginScript,
|
|
||||||
required PluginConfiguration pluginConfig,
|
|
||||||
required String serverEndpointUrl,
|
|
||||||
required String serverSecret,
|
|
||||||
}) : plugin = SpotubePlugin() {
|
|
||||||
sender = plugin.createContext(
|
|
||||||
pluginScript: pluginScript,
|
|
||||||
pluginConfig: pluginConfig,
|
|
||||||
serverEndpointUrl: serverEndpointUrl,
|
|
||||||
serverSecret: serverSecret,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream<AuthEventObject> authState() => plugin.authState();
|
Stream<AuthEventObject> authState() => plugin.authState();
|
||||||
|
|
||||||
|
|||||||
@ -66,11 +66,12 @@ abstract class SpotubePlugin implements RustOpaqueInterface {
|
|||||||
|
|
||||||
Future<void> close({required OpaqueSender tx});
|
Future<void> close({required OpaqueSender tx});
|
||||||
|
|
||||||
OpaqueSender createContext(
|
Future<OpaqueSender> createContext(
|
||||||
{required String pluginScript,
|
{required String pluginScript,
|
||||||
required PluginConfiguration pluginConfig,
|
required PluginConfiguration pluginConfig,
|
||||||
required String serverEndpointUrl,
|
required String serverEndpointUrl,
|
||||||
required String serverSecret});
|
required String serverSecret,
|
||||||
|
required String localStorageDir});
|
||||||
|
|
||||||
factory SpotubePlugin() =>
|
factory SpotubePlugin() =>
|
||||||
RustLib.instance.api.crateApiPluginPluginSpotubePluginNew();
|
RustLib.instance.api.crateApiPluginPluginSpotubePluginNew();
|
||||||
|
|||||||
@ -170,12 +170,13 @@ abstract class RustLibApi extends BaseApi {
|
|||||||
Future<void> crateApiPluginPluginSpotubePluginClose(
|
Future<void> crateApiPluginPluginSpotubePluginClose(
|
||||||
{required SpotubePlugin that, required OpaqueSender tx});
|
{required SpotubePlugin that, required OpaqueSender tx});
|
||||||
|
|
||||||
OpaqueSender crateApiPluginPluginSpotubePluginCreateContext(
|
Future<OpaqueSender> crateApiPluginPluginSpotubePluginCreateContext(
|
||||||
{required SpotubePlugin that,
|
{required SpotubePlugin that,
|
||||||
required String pluginScript,
|
required String pluginScript,
|
||||||
required PluginConfiguration pluginConfig,
|
required PluginConfiguration pluginConfig,
|
||||||
required String serverEndpointUrl,
|
required String serverEndpointUrl,
|
||||||
required String serverSecret});
|
required String serverSecret,
|
||||||
|
required String localStorageDir});
|
||||||
|
|
||||||
SpotubePlugin crateApiPluginPluginSpotubePluginNew();
|
SpotubePlugin crateApiPluginPluginSpotubePluginNew();
|
||||||
|
|
||||||
@ -1204,14 +1205,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
OpaqueSender crateApiPluginPluginSpotubePluginCreateContext(
|
Future<OpaqueSender> crateApiPluginPluginSpotubePluginCreateContext(
|
||||||
{required SpotubePlugin that,
|
{required SpotubePlugin that,
|
||||||
required String pluginScript,
|
required String pluginScript,
|
||||||
required PluginConfiguration pluginConfig,
|
required PluginConfiguration pluginConfig,
|
||||||
required String serverEndpointUrl,
|
required String serverEndpointUrl,
|
||||||
required String serverSecret}) {
|
required String serverSecret,
|
||||||
return handler.executeSync(SyncTask(
|
required String localStorageDir}) {
|
||||||
callFfi: () {
|
return handler.executeNormal(NormalTask(
|
||||||
|
callFfi: (port_) {
|
||||||
final serializer = SseSerializer(generalizedFrbRustBinding);
|
final serializer = SseSerializer(generalizedFrbRustBinding);
|
||||||
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin(
|
sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSpotubePlugin(
|
||||||
that, serializer);
|
that, serializer);
|
||||||
@ -1219,7 +1221,9 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
sse_encode_box_autoadd_plugin_configuration(pluginConfig, serializer);
|
sse_encode_box_autoadd_plugin_configuration(pluginConfig, serializer);
|
||||||
sse_encode_String(serverEndpointUrl, serializer);
|
sse_encode_String(serverEndpointUrl, serializer);
|
||||||
sse_encode_String(serverSecret, serializer);
|
sse_encode_String(serverSecret, serializer);
|
||||||
return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 25)!;
|
sse_encode_String(localStorageDir, serializer);
|
||||||
|
pdeCallFfi(generalizedFrbRustBinding, serializer,
|
||||||
|
funcId: 25, port: port_);
|
||||||
},
|
},
|
||||||
codec: SseCodec(
|
codec: SseCodec(
|
||||||
decodeSuccessData:
|
decodeSuccessData:
|
||||||
@ -1232,7 +1236,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
pluginScript,
|
pluginScript,
|
||||||
pluginConfig,
|
pluginConfig,
|
||||||
serverEndpointUrl,
|
serverEndpointUrl,
|
||||||
serverSecret
|
serverSecret,
|
||||||
|
localStorageDir
|
||||||
],
|
],
|
||||||
apiImpl: this,
|
apiImpl: this,
|
||||||
));
|
));
|
||||||
@ -1246,7 +1251,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi {
|
|||||||
"pluginScript",
|
"pluginScript",
|
||||||
"pluginConfig",
|
"pluginConfig",
|
||||||
"serverEndpointUrl",
|
"serverEndpointUrl",
|
||||||
"serverSecret"
|
"serverSecret",
|
||||||
|
"localStorageDir"
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -6951,15 +6957,17 @@ class SpotubePluginImpl extends RustOpaque implements SpotubePlugin {
|
|||||||
Future<void> close({required OpaqueSender tx}) => RustLib.instance.api
|
Future<void> close({required OpaqueSender tx}) => RustLib.instance.api
|
||||||
.crateApiPluginPluginSpotubePluginClose(that: this, tx: tx);
|
.crateApiPluginPluginSpotubePluginClose(that: this, tx: tx);
|
||||||
|
|
||||||
OpaqueSender createContext(
|
Future<OpaqueSender> createContext(
|
||||||
{required String pluginScript,
|
{required String pluginScript,
|
||||||
required PluginConfiguration pluginConfig,
|
required PluginConfiguration pluginConfig,
|
||||||
required String serverEndpointUrl,
|
required String serverEndpointUrl,
|
||||||
required String serverSecret}) =>
|
required String serverSecret,
|
||||||
|
required String localStorageDir}) =>
|
||||||
RustLib.instance.api.crateApiPluginPluginSpotubePluginCreateContext(
|
RustLib.instance.api.crateApiPluginPluginSpotubePluginCreateContext(
|
||||||
that: this,
|
that: this,
|
||||||
pluginScript: pluginScript,
|
pluginScript: pluginScript,
|
||||||
pluginConfig: pluginConfig,
|
pluginConfig: pluginConfig,
|
||||||
serverEndpointUrl: serverEndpointUrl,
|
serverEndpointUrl: serverEndpointUrl,
|
||||||
serverSecret: serverSecret);
|
serverSecret: serverSecret,
|
||||||
|
localStorageDir: localStorageDir);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,14 +11,11 @@ pub enum PluginApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub enum PluginAbility {
|
pub enum PluginAbility {
|
||||||
#[serde(rename = "authentication")]
|
|
||||||
Authentication,
|
Authentication,
|
||||||
#[serde(rename = "scrobbling")]
|
|
||||||
Scrobbling,
|
Scrobbling,
|
||||||
#[serde(rename = "metadata")]
|
|
||||||
Metadata,
|
Metadata,
|
||||||
#[serde(rename = "audio-source")]
|
|
||||||
AudioSource,
|
AudioSource,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,7 @@ use crate::api::plugin::executors::{
|
|||||||
execute_core, execute_playlist, execute_search, execute_track, execute_user,
|
execute_core, execute_playlist, execute_search, execute_track, execute_user,
|
||||||
};
|
};
|
||||||
use crate::api::plugin::models::auth::{AuthEventObject, AuthEventType};
|
use crate::api::plugin::models::auth::{AuthEventObject, AuthEventType};
|
||||||
use crate::api::plugin::models::core::PluginConfiguration;
|
use crate::api::plugin::models::core::{PluginAbility, PluginConfiguration};
|
||||||
use crate::api::plugin::senders::{
|
use crate::api::plugin::senders::{
|
||||||
PluginAlbumSender, PluginArtistSender, PluginAudioSourceSender, PluginAuthSender,
|
PluginAlbumSender, PluginArtistSender, PluginAudioSourceSender, PluginAuthSender,
|
||||||
PluginBrowseSender, PluginCoreSender, PluginPlaylistSender, PluginSearchSender,
|
PluginBrowseSender, PluginCoreSender, PluginPlaylistSender, PluginSearchSender,
|
||||||
@ -12,9 +12,9 @@ use crate::api::plugin::senders::{
|
|||||||
};
|
};
|
||||||
use crate::frb_generated::StreamSink;
|
use crate::frb_generated::StreamSink;
|
||||||
use crate::internal::apis;
|
use crate::internal::apis;
|
||||||
use crate::internal::apis::{form, get_platform_directories, timezone, webview, yt_engine};
|
use crate::internal::apis::{form, timezone, webview, yt_engine};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use flutter_rust_bridge::{frb, Rust2DartSendError};
|
use flutter_rust_bridge::frb;
|
||||||
use llrt_modules::module_builder::ModuleBuilder;
|
use llrt_modules::module_builder::ModuleBuilder;
|
||||||
use llrt_modules::{
|
use llrt_modules::{
|
||||||
abort, buffer, console, crypto, events, exceptions, fetch, navigator, timers, url, util,
|
abort, buffer, console, crypto, events, exceptions, fetch, navigator, timers, url, util,
|
||||||
@ -37,8 +37,9 @@ async fn create_context(
|
|||||||
server_endpoint_url: String,
|
server_endpoint_url: String,
|
||||||
server_secret: String,
|
server_secret: String,
|
||||||
plugin_slug: String,
|
plugin_slug: String,
|
||||||
|
local_storage_dir: String,
|
||||||
) -> anyhow::Result<(AsyncContext, AsyncRuntime)> {
|
) -> anyhow::Result<(AsyncContext, AsyncRuntime)> {
|
||||||
let runtime = AsyncRuntime::new().expect("Unable to create async runtime");
|
let runtime = AsyncRuntime::new()?;
|
||||||
|
|
||||||
let mut module_builder = ModuleBuilder::new();
|
let mut module_builder = ModuleBuilder::new();
|
||||||
|
|
||||||
@ -64,15 +65,7 @@ async fn create_context(
|
|||||||
.set_loader((module_resolver,), (module_loader,))
|
.set_loader((module_resolver,), (module_loader,))
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let context = AsyncContext::full(&runtime)
|
let context = AsyncContext::full(&runtime).await?;
|
||||||
.await
|
|
||||||
.expect("Unable to create async context");
|
|
||||||
|
|
||||||
let directories =
|
|
||||||
get_platform_directories(server_endpoint_url.clone(), server_secret.clone()).await?;
|
|
||||||
let local_storage_dir = directories
|
|
||||||
.application_support
|
|
||||||
.ok_or_else(|| anyhow!("Application support directory not found"))?;
|
|
||||||
|
|
||||||
async_with!(context => |ctx| {
|
async_with!(context => |ctx| {
|
||||||
apis::init(&ctx, server_endpoint_url, server_secret).catch(&ctx).map_err(|e| anyhow!("Failed to initialize APIs: {}", e))?;
|
apis::init(&ctx, server_endpoint_url, server_secret).catch(&ctx).map_err(|e| anyhow!("Failed to initialize APIs: {}", e))?;
|
||||||
@ -90,9 +83,7 @@ async fn js_executor_thread(
|
|||||||
context: &AsyncContext,
|
context: &AsyncContext,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
while let Some(command) = rx.recv().await {
|
while let Some(command) = rx.recv().await {
|
||||||
println!("JS Executor thread received command: {:?}", command);
|
|
||||||
if let PluginCommand::Shutdown = command {
|
if let PluginCommand::Shutdown = command {
|
||||||
println!("JS Executor thread shutting down.");
|
|
||||||
return anyhow::Ok(());
|
return anyhow::Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +125,7 @@ pub struct SpotubePlugin {
|
|||||||
pub track: PluginTrackSender,
|
pub track: PluginTrackSender,
|
||||||
pub user: PluginUserSender,
|
pub user: PluginUserSender,
|
||||||
event_tx: Sender<AuthEventObject>,
|
event_tx: Sender<AuthEventObject>,
|
||||||
event_rx: Receiver<AuthEventObject>,
|
event_rx: Option<Receiver<AuthEventObject>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SpotubePlugin {
|
impl SpotubePlugin {
|
||||||
@ -154,29 +145,40 @@ impl SpotubePlugin {
|
|||||||
track: PluginTrackSender::new(),
|
track: PluginTrackSender::new(),
|
||||||
user: PluginUserSender::new(),
|
user: PluginUserSender::new(),
|
||||||
event_tx,
|
event_tx,
|
||||||
event_rx,
|
event_rx: Some(event_rx),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn auth_state(&mut self, sink: StreamSink<AuthEventObject>) -> anyhow::Result<()> {
|
pub async fn auth_state(&mut self, sink: StreamSink<AuthEventObject>) -> anyhow::Result<()> {
|
||||||
while let Some(event) = self.event_rx.recv().await {
|
let mut receiver = self
|
||||||
sink.add(event)
|
.event_rx
|
||||||
.map_err(|e: Rust2DartSendError| anyhow::anyhow!(e))?;
|
.take()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Receiver already consumed"))?;
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
while let Some(event) = receiver.recv().await {
|
||||||
|
if let Err(e) = sink.add(event) {
|
||||||
|
eprintln!("Failed to send auth event to stream sink: {:?}", e);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[frb(sync)]
|
pub async fn create_context(
|
||||||
pub fn create_context(
|
|
||||||
&self,
|
&self,
|
||||||
plugin_script: String,
|
plugin_script: String,
|
||||||
plugin_config: PluginConfiguration,
|
plugin_config: PluginConfiguration,
|
||||||
server_endpoint_url: String,
|
server_endpoint_url: String,
|
||||||
server_secret: String,
|
server_secret: String,
|
||||||
|
local_storage_dir: String,
|
||||||
) -> anyhow::Result<OpaqueSender> {
|
) -> anyhow::Result<OpaqueSender> {
|
||||||
let (command_tx, mut command_rx) = mpsc::channel(32);
|
let (command_tx, mut command_rx) = mpsc::channel(32);
|
||||||
|
let (init_tx, init_rx) = tokio::sync::oneshot::channel::<anyhow::Result<()>>();
|
||||||
let sender = self.event_tx.clone();
|
let sender = self.event_tx.clone();
|
||||||
|
|
||||||
let _thread_handle = thread::spawn(move || {
|
let _thread_handle = thread::spawn(move || {
|
||||||
let rt = tokio::runtime::Builder::new_current_thread()
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
@ -184,24 +186,36 @@ impl SpotubePlugin {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let local = LocalSet::new();
|
let local = LocalSet::new();
|
||||||
if let Err(e) = local.block_on(&rt, async {
|
if let Err(e) = local.block_on(&rt, async {
|
||||||
let (ctx, _) = create_context(
|
let ctx_res = create_context(
|
||||||
server_endpoint_url,
|
server_endpoint_url,
|
||||||
server_secret,
|
server_secret,
|
||||||
plugin_config.slug(),
|
plugin_config.slug(),
|
||||||
).await?;
|
local_storage_dir,
|
||||||
|
).await;
|
||||||
|
|
||||||
let injection = format!(
|
if let Err(e) = ctx_res {
|
||||||
"globalThis.pluginInstance = new {}();",
|
let _ = init_tx.send(Err(e));
|
||||||
plugin_config.entry_point
|
return anyhow::Ok(());
|
||||||
);
|
}
|
||||||
let script = format!("{}\n{}", plugin_script, injection);
|
|
||||||
|
|
||||||
async_with!(ctx => |cx| {
|
let (ctx, _runtime) = ctx_res.unwrap();
|
||||||
|
|
||||||
|
let begin_injection = "globalThis.module = {exports: {}};";
|
||||||
|
|
||||||
|
let end_injection = "globalThis.pluginInstance = new module.exports.default();";
|
||||||
|
let script = format!("{}\n{}\n{}", begin_injection, plugin_script, end_injection);
|
||||||
|
|
||||||
|
let script_eval_res = async_with!(ctx => |cx| {
|
||||||
cx.eval::<(), _>(script.as_str())
|
cx.eval::<(), _>(script.as_str())
|
||||||
.catch(&cx).map_err(|e| anyhow!("Failed to evaluate supplied plugin script: {}", e))
|
.catch(&cx).map_err(|e| anyhow!("Failed to evaluate supplied plugin script: {}", e))
|
||||||
}).await?;
|
}).await;
|
||||||
|
|
||||||
async_with!(ctx => |ctx|{
|
if let Err(e) = script_eval_res {
|
||||||
|
let _ = init_tx.send(Err(e));
|
||||||
|
return anyhow::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let on_auth_event_res = async_with!(ctx => |ctx|{
|
||||||
let globals = ctx.globals();
|
let globals = ctx.globals();
|
||||||
let callback = Func::new(move |event: Object| -> rquickjs::Result<()>{
|
let callback = Func::new(move |event: Object| -> rquickjs::Result<()>{
|
||||||
let sender_clone = sender.clone();
|
let sender_clone = sender.clone();
|
||||||
@ -223,26 +237,39 @@ impl SpotubePlugin {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if plugin_config.abilities.contains(&PluginAbility::Authentication) {
|
||||||
if let Err(e) = globals.get::<_, Object>("pluginInstance")?.get::<_, Object>("auth")?.set(
|
if let Err(e) = globals.get::<_, Object>("pluginInstance")?.get::<_, Object>("auth")?.set(
|
||||||
"onAuthEvent", callback
|
"onAuthEvent", callback
|
||||||
) {
|
) {
|
||||||
eprintln!("Error setting auth event handler: {:?}", e);
|
eprintln!("Error setting auth event handler: {:?}", e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok::<(), Error>(())
|
Ok::<(), Error>(())
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow!("[onAuthEvent] {e}"))?;
|
.map_err(|e| anyhow!("[onAuthEvent] {e}"));
|
||||||
|
|
||||||
|
if let Err(e) = on_auth_event_res {
|
||||||
|
let _ = init_tx.send(Err(e));
|
||||||
|
return anyhow::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = init_tx.send(Ok(()));
|
||||||
|
|
||||||
if let Err(e) = js_executor_thread(&mut command_rx, &ctx).await {
|
if let Err(e) = js_executor_thread(&mut command_rx, &ctx).await {
|
||||||
eprintln!("JS executor error: {}", e);
|
eprintln!("JS executor error: {}", e);
|
||||||
}
|
}
|
||||||
anyhow::Ok(())
|
anyhow::Ok(())
|
||||||
}) {
|
}) {
|
||||||
eprintln!("JS Executor thread error: {}", e);
|
eprintln!("[PluginInitializationError]: {}", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
init_rx
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow!("Failed to receive initialization result: {}", e))??;
|
||||||
|
|
||||||
Ok(OpaqueSender { sender: command_tx })
|
Ok(OpaqueSender { sender: command_tx })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -45,7 +45,7 @@ impl PluginArtistSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn top_tracks(
|
pub async fn top_tracks(
|
||||||
@ -66,7 +66,7 @@ impl PluginArtistSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn albums(
|
pub async fn albums(
|
||||||
@ -87,7 +87,7 @@ impl PluginArtistSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn related(
|
pub async fn related(
|
||||||
@ -108,7 +108,7 @@ impl PluginArtistSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn save(&self, mpsc_tx: &OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> {
|
pub async fn save(&self, mpsc_tx: &OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> {
|
||||||
@ -121,7 +121,7 @@ impl PluginArtistSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unsave(&self, mpsc_tx: &OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> {
|
pub async fn unsave(&self, mpsc_tx: &OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> {
|
||||||
@ -134,7 +134,7 @@ impl PluginArtistSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,7 +161,7 @@ impl PluginAlbumSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn tracks(
|
pub async fn tracks(
|
||||||
@ -182,7 +182,7 @@ impl PluginAlbumSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn releases(
|
pub async fn releases(
|
||||||
@ -201,7 +201,7 @@ impl PluginAlbumSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn save(&self, mpsc_tx: &OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> {
|
pub async fn save(&self, mpsc_tx: &OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> {
|
||||||
@ -214,7 +214,7 @@ impl PluginAlbumSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unsave(&self, mpsc_tx: &OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> {
|
pub async fn unsave(&self, mpsc_tx: &OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> {
|
||||||
@ -227,7 +227,7 @@ impl PluginAlbumSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -252,7 +252,7 @@ impl PluginAudioSourceSender {
|
|||||||
))
|
))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn matches(
|
pub async fn matches(
|
||||||
@ -269,7 +269,7 @@ impl PluginAudioSourceSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn streams(
|
pub async fn streams(
|
||||||
@ -286,7 +286,7 @@ impl PluginAudioSourceSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -308,7 +308,7 @@ impl PluginAuthSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn logout(&self, mpsc_tx: &OpaqueSender) -> anyhow::Result<()> {
|
pub async fn logout(&self, mpsc_tx: &OpaqueSender) -> anyhow::Result<()> {
|
||||||
@ -320,7 +320,7 @@ impl PluginAuthSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn is_authenticated(&self, mpsc_tx: &OpaqueSender) -> anyhow::Result<bool> {
|
pub async fn is_authenticated(&self, mpsc_tx: &OpaqueSender) -> anyhow::Result<bool> {
|
||||||
@ -332,7 +332,7 @@ impl PluginAuthSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -361,7 +361,7 @@ impl PluginBrowseSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn section_items(
|
pub async fn section_items(
|
||||||
@ -382,7 +382,7 @@ impl PluginBrowseSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -409,13 +409,7 @@ impl PluginCoreSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
.map_err(|e| {
|
|
||||||
eprintln!("RecvError: {}", e);
|
|
||||||
eprintln!("Stack trace:\n{:?}", Backtrace::capture());
|
|
||||||
anyhow!("{e}")
|
|
||||||
})
|
|
||||||
.and_then(|o| o)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn support(&self, mpsc_tx: &OpaqueSender) -> anyhow::Result<String> {
|
pub async fn support(&self, mpsc_tx: &OpaqueSender) -> anyhow::Result<String> {
|
||||||
@ -427,7 +421,7 @@ impl PluginCoreSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn scrobble(
|
pub async fn scrobble(
|
||||||
@ -444,7 +438,7 @@ impl PluginCoreSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -471,7 +465,7 @@ impl PluginPlaylistSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn tracks(
|
pub async fn tracks(
|
||||||
@ -492,7 +486,7 @@ impl PluginPlaylistSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_playlist(
|
pub async fn create_playlist(
|
||||||
@ -517,7 +511,7 @@ impl PluginPlaylistSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update_playlist(
|
pub async fn update_playlist(
|
||||||
@ -542,7 +536,7 @@ impl PluginPlaylistSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_playlist(
|
pub async fn delete_playlist(
|
||||||
@ -559,7 +553,7 @@ impl PluginPlaylistSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_tracks(
|
pub async fn add_tracks(
|
||||||
@ -580,7 +574,7 @@ impl PluginPlaylistSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn remove_tracks(
|
pub async fn remove_tracks(
|
||||||
@ -599,7 +593,7 @@ impl PluginPlaylistSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn save(&self, mpsc_tx: &OpaqueSender, playlist_id: String) -> anyhow::Result<()> {
|
pub async fn save(&self, mpsc_tx: &OpaqueSender, playlist_id: String) -> anyhow::Result<()> {
|
||||||
@ -612,7 +606,7 @@ impl PluginPlaylistSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unsave(&self, mpsc_tx: &OpaqueSender, playlist_id: String) -> anyhow::Result<()> {
|
pub async fn unsave(&self, mpsc_tx: &OpaqueSender, playlist_id: String) -> anyhow::Result<()> {
|
||||||
@ -625,7 +619,7 @@ impl PluginPlaylistSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -647,7 +641,7 @@ impl PluginSearchSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn all(
|
pub async fn all(
|
||||||
@ -664,7 +658,7 @@ impl PluginSearchSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn tracks(
|
pub async fn tracks(
|
||||||
@ -685,7 +679,7 @@ impl PluginSearchSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn albums(
|
pub async fn albums(
|
||||||
@ -706,7 +700,7 @@ impl PluginSearchSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn artists(
|
pub async fn artists(
|
||||||
@ -727,7 +721,7 @@ impl PluginSearchSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn playlists(
|
pub async fn playlists(
|
||||||
@ -748,7 +742,7 @@ impl PluginSearchSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -775,7 +769,7 @@ impl PluginTrackSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn save(&self, mpsc_tx: &OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> {
|
pub async fn save(&self, mpsc_tx: &OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> {
|
||||||
@ -788,7 +782,7 @@ impl PluginTrackSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unsave(&self, mpsc_tx: &OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> {
|
pub async fn unsave(&self, mpsc_tx: &OpaqueSender, ids: Vec<String>) -> anyhow::Result<()> {
|
||||||
@ -801,7 +795,7 @@ impl PluginTrackSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn radio(
|
pub async fn radio(
|
||||||
@ -818,7 +812,7 @@ impl PluginTrackSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -838,7 +832,7 @@ impl PluginUserSender {
|
|||||||
.send(PluginCommand::User(UserCommands::Me { response_tx: tx }))
|
.send(PluginCommand::User(UserCommands::Me { response_tx: tx }))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn saved_tracks(
|
pub async fn saved_tracks(
|
||||||
@ -857,7 +851,7 @@ impl PluginUserSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn saved_albums(
|
pub async fn saved_albums(
|
||||||
@ -876,7 +870,7 @@ impl PluginUserSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn saved_artists(
|
pub async fn saved_artists(
|
||||||
@ -895,7 +889,7 @@ impl PluginUserSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn saved_playlists(
|
pub async fn saved_playlists(
|
||||||
@ -914,6 +908,6 @@ impl PluginUserSender {
|
|||||||
}))
|
}))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
rx.await.map_err(|e| anyhow!("{e}")).and_then(|o| o)
|
rx.await.map_err(|e| anyhow!("{e}"))?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1253,15 +1253,16 @@ fn wire__crate__api__plugin__plugin__SpotubePlugin_close_impl(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
fn wire__crate__api__plugin__plugin__SpotubePlugin_create_context_impl(
|
fn wire__crate__api__plugin__plugin__SpotubePlugin_create_context_impl(
|
||||||
|
port_: flutter_rust_bridge::for_generated::MessagePort,
|
||||||
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr,
|
||||||
rust_vec_len_: i32,
|
rust_vec_len_: i32,
|
||||||
data_len_: i32,
|
data_len_: i32,
|
||||||
) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse {
|
) {
|
||||||
FLUTTER_RUST_BRIDGE_HANDLER.wrap_sync::<flutter_rust_bridge::for_generated::SseCodec, _>(
|
FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::<flutter_rust_bridge::for_generated::SseCodec, _, _, _>(
|
||||||
flutter_rust_bridge::for_generated::TaskInfo {
|
flutter_rust_bridge::for_generated::TaskInfo {
|
||||||
debug_name: "SpotubePlugin_create_context",
|
debug_name: "SpotubePlugin_create_context",
|
||||||
port: None,
|
port: Some(port_),
|
||||||
mode: flutter_rust_bridge::for_generated::FfiCallMode::Sync,
|
mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal,
|
||||||
},
|
},
|
||||||
move || {
|
move || {
|
||||||
let message = unsafe {
|
let message = unsafe {
|
||||||
@ -1283,19 +1284,24 @@ fn wire__crate__api__plugin__plugin__SpotubePlugin_create_context_impl(
|
|||||||
);
|
);
|
||||||
let api_server_endpoint_url = <String>::sse_decode(&mut deserializer);
|
let api_server_endpoint_url = <String>::sse_decode(&mut deserializer);
|
||||||
let api_server_secret = <String>::sse_decode(&mut deserializer);
|
let api_server_secret = <String>::sse_decode(&mut deserializer);
|
||||||
|
let api_local_storage_dir = <String>::sse_decode(&mut deserializer);
|
||||||
deserializer.end();
|
deserializer.end();
|
||||||
|
move |context| async move {
|
||||||
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
|
transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>(
|
||||||
(move || {
|
(move || async move {
|
||||||
let mut api_that_guard = None;
|
let mut api_that_guard = None;
|
||||||
let decode_indices_ =
|
let decode_indices_ =
|
||||||
flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![
|
flutter_rust_bridge::for_generated::lockable_compute_decode_order(
|
||||||
flutter_rust_bridge::for_generated::LockableOrderInfo::new(
|
vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new(
|
||||||
&api_that, 0, false,
|
&api_that, 0, false,
|
||||||
),
|
)],
|
||||||
]);
|
);
|
||||||
for i in decode_indices_ {
|
for i in decode_indices_ {
|
||||||
match i {
|
match i {
|
||||||
0 => api_that_guard = Some(api_that.lockable_decode_sync_ref()),
|
0 => {
|
||||||
|
api_that_guard =
|
||||||
|
Some(api_that.lockable_decode_async_ref().await)
|
||||||
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1306,10 +1312,14 @@ fn wire__crate__api__plugin__plugin__SpotubePlugin_create_context_impl(
|
|||||||
api_plugin_config,
|
api_plugin_config,
|
||||||
api_server_endpoint_url,
|
api_server_endpoint_url,
|
||||||
api_server_secret,
|
api_server_secret,
|
||||||
)?;
|
api_local_storage_dir,
|
||||||
Ok(output_ok)
|
|
||||||
})(),
|
|
||||||
)
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(output_ok)
|
||||||
|
})()
|
||||||
|
.await,
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -5824,6 +5834,7 @@ fn pde_ffi_dispatcher_primary_impl(
|
|||||||
match func_id {
|
match func_id {
|
||||||
3 => wire__crate__api__plugin__plugin__SpotubePlugin_auth_state_impl(port, ptr, rust_vec_len, data_len),
|
3 => wire__crate__api__plugin__plugin__SpotubePlugin_auth_state_impl(port, ptr, rust_vec_len, data_len),
|
||||||
24 => wire__crate__api__plugin__plugin__SpotubePlugin_close_impl(port, ptr, rust_vec_len, data_len),
|
24 => wire__crate__api__plugin__plugin__SpotubePlugin_close_impl(port, ptr, rust_vec_len, data_len),
|
||||||
|
25 => wire__crate__api__plugin__plugin__SpotubePlugin_create_context_impl(port, ptr, rust_vec_len, data_len),
|
||||||
27 => wire__crate__api__init_app_impl(port, ptr, rust_vec_len, data_len),
|
27 => wire__crate__api__init_app_impl(port, ptr, rust_vec_len, data_len),
|
||||||
28 => wire__crate__api__plugin__senders__plugin_album_sender_get_album_impl(port, ptr, rust_vec_len, data_len),
|
28 => wire__crate__api__plugin__senders__plugin_album_sender_get_album_impl(port, ptr, rust_vec_len, data_len),
|
||||||
29 => wire__crate__api__plugin__senders__plugin_album_sender_releases_impl(port, ptr, rust_vec_len, data_len),
|
29 => wire__crate__api__plugin__senders__plugin_album_sender_releases_impl(port, ptr, rust_vec_len, data_len),
|
||||||
@ -5906,7 +5917,6 @@ fn pde_ffi_dispatcher_sync_impl(
|
|||||||
21 => wire__crate__api__plugin__plugin__SpotubePlugin_auto_accessor_set_search_impl(ptr, rust_vec_len, data_len),
|
21 => wire__crate__api__plugin__plugin__SpotubePlugin_auto_accessor_set_search_impl(ptr, rust_vec_len, data_len),
|
||||||
22 => wire__crate__api__plugin__plugin__SpotubePlugin_auto_accessor_set_track_impl(ptr, rust_vec_len, data_len),
|
22 => wire__crate__api__plugin__plugin__SpotubePlugin_auto_accessor_set_track_impl(ptr, rust_vec_len, data_len),
|
||||||
23 => wire__crate__api__plugin__plugin__SpotubePlugin_auto_accessor_set_user_impl(ptr, rust_vec_len, data_len),
|
23 => wire__crate__api__plugin__plugin__SpotubePlugin_auto_accessor_set_user_impl(ptr, rust_vec_len, data_len),
|
||||||
25 => wire__crate__api__plugin__plugin__SpotubePlugin_create_context_impl(ptr, rust_vec_len, data_len),
|
|
||||||
26 => wire__crate__api__plugin__plugin__SpotubePlugin_new_impl(ptr, rust_vec_len, data_len),
|
26 => wire__crate__api__plugin__plugin__SpotubePlugin_new_impl(ptr, rust_vec_len, data_len),
|
||||||
47 => wire__crate__api__plugin__models__core__plugin_configuration_slug_impl(ptr, rust_vec_len, data_len),
|
47 => wire__crate__api__plugin__models__core__plugin_configuration_slug_impl(ptr, rust_vec_len, data_len),
|
||||||
75 => wire__crate__api__plugin__models__audio_source__spotube_audio_lossless_container_quality_to_string_fmt_impl(ptr, rust_vec_len, data_len),
|
75 => wire__crate__api__plugin__models__audio_source__spotube_audio_lossless_container_quality_to_string_fmt_impl(ptr, rust_vec_len, data_len),
|
||||||
|
|||||||
@ -1,10 +1,9 @@
|
|||||||
use rquickjs::Ctx;
|
use rquickjs::Ctx;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
pub mod form;
|
pub mod form;
|
||||||
pub mod local_storage;
|
pub mod local_storage;
|
||||||
pub mod webview;
|
|
||||||
pub mod timezone;
|
pub mod timezone;
|
||||||
|
pub mod webview;
|
||||||
pub mod yt_engine;
|
pub mod yt_engine;
|
||||||
|
|
||||||
pub fn init(ctx: &Ctx, endpoint_url: String, secret: String) -> rquickjs::Result<()> {
|
pub fn init(ctx: &Ctx, endpoint_url: String, secret: String) -> rquickjs::Result<()> {
|
||||||
@ -13,28 +12,3 @@ pub fn init(ctx: &Ctx, endpoint_url: String, secret: String) -> rquickjs::Result
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct DirectoriesResponse {
|
|
||||||
pub temporary: Option<String>,
|
|
||||||
pub application_documents: Option<String>,
|
|
||||||
pub application_support: Option<String>,
|
|
||||||
pub library: Option<String>,
|
|
||||||
pub external_storage: Option<String>,
|
|
||||||
pub downloads: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn get_platform_directories(
|
|
||||||
server_url: String,
|
|
||||||
server_secret: String,
|
|
||||||
) -> anyhow::Result<DirectoriesResponse> {
|
|
||||||
let client = reqwest::Client::new();
|
|
||||||
Ok(client
|
|
||||||
.get(format!("{}/plugin/localstorage/directories", server_url).as_str())
|
|
||||||
.header("X-Plugin-Secret", server_secret.as_str())
|
|
||||||
.send()
|
|
||||||
.await?
|
|
||||||
.json::<DirectoriesResponse>()
|
|
||||||
.await?)
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
use rquickjs::prelude::Func;
|
use rquickjs::prelude::Func;
|
||||||
use rquickjs::{Class, Ctx, Object};
|
use rquickjs::{Ctx, Object};
|
||||||
|
|
||||||
pub fn get_local_timezone() -> rquickjs::Result<String> {
|
pub fn get_local_timezone() -> rquickjs::Result<String> {
|
||||||
let timezone = iana_time_zone::get_timezone()
|
let timezone = iana_time_zone::get_timezone()
|
||||||
|
|||||||
@ -185,10 +185,7 @@ impl<'js> WebView<'js> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn url_change_task(&self, ctx: Ctx<'js>) {
|
async fn url_change_task(&self, ctx: Ctx<'js>) {
|
||||||
let endpoint = format!(
|
let endpoint = format!("{}/plugin-api/webview/events", self.endpoint_url);
|
||||||
"{}/plugin-api/webview/{}/on-url-request",
|
|
||||||
self.endpoint_url, self.uid
|
|
||||||
);
|
|
||||||
|
|
||||||
let secret = self.secret.clone();
|
let secret = self.secret.clone();
|
||||||
|
|
||||||
@ -201,22 +198,39 @@ impl<'js> WebView<'js> {
|
|||||||
.header("X-Plugin-Secret", &secret)
|
.header("X-Plugin-Secret", &secret)
|
||||||
.expect("Failed to set header for EventSourceClient")
|
.expect("Failed to set header for EventSourceClient")
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let mut stream = client.stream();
|
let mut stream = client.stream();
|
||||||
while let Some(event) = stream.next().await {
|
while let Some(event) = stream.next().await {
|
||||||
match event {
|
match event {
|
||||||
Ok(eventsource_client::SSE::Event(msg)) => {
|
Ok(eventsource_client::SSE::Event(msg)) => {
|
||||||
if msg.event_type != "url-request" {
|
if msg.event_type != "url-request" {
|
||||||
|
eprintln!(
|
||||||
|
"[rust][webview] Not expected event-type: {}",
|
||||||
|
msg.event_type
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
backoff = 1;
|
backoff = 1;
|
||||||
if let Ok(data) = serde_json::from_str::<HashMap<String, String>>(&msg.data)
|
if let Ok(data) = serde_json::from_str::<HashMap<String, String>>(&msg.data)
|
||||||
{
|
{
|
||||||
let url = data.get("url").cloned().unwrap_or_default();
|
let url = data.get("url").cloned().unwrap_or_default();
|
||||||
|
let uid = data.get("uid").cloned().unwrap_or_default();
|
||||||
|
|
||||||
|
if uid != self.uid {
|
||||||
|
println!(
|
||||||
|
"[rust][webview] Ignored event for different uid: {}",
|
||||||
|
uid
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
for callback in self.callbacks.iter() {
|
for callback in self.callbacks.iter() {
|
||||||
match callback.call::<_, Value>((url.clone(),)) {
|
match callback.call::<_, Value>((url.clone(),)) {
|
||||||
Ok(res) => {
|
Ok(res) => {
|
||||||
if let Some(promise) = res.into_promise() {
|
if let Some(promise) = res.into_promise() {
|
||||||
if let Err(e) = promise.into_future::<()>().await.catch(&ctx) {
|
if let Err(e) =
|
||||||
|
promise.into_future::<()>().await.catch(&ctx)
|
||||||
|
{
|
||||||
eprintln!("Error in onUrlChange promise: {}", e);
|
eprintln!("Error in onUrlChange promise: {}", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -230,7 +244,9 @@ impl<'js> WebView<'js> {
|
|||||||
eprintln!("Failed to parse event data: {}", msg.data);
|
eprintln!("Failed to parse event data: {}", msg.data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(_) => {}
|
Ok(e) => {
|
||||||
|
eprintln!("[rust][webview] Ignored non-event message: {:?}", e);
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Error in EventSource stream: {}", err);
|
eprintln!("Error in EventSource stream: {}", err);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
use crate::internal::utils::js_invoke_async_method_to_json;
|
use crate::internal::utils::{js_invoke_async_method_to_json, js_invoke_method_to_json};
|
||||||
use flutter_rust_bridge::frb;
|
use flutter_rust_bridge::frb;
|
||||||
use rquickjs::{async_with, AsyncContext};
|
use rquickjs::{async_with, AsyncContext};
|
||||||
|
|
||||||
@ -28,16 +28,14 @@ impl<'a> PluginAuthEndpoint<'a> {
|
|||||||
|
|
||||||
pub async fn is_authenticated(&self) -> anyhow::Result<bool> {
|
pub async fn is_authenticated(&self) -> anyhow::Result<bool> {
|
||||||
async_with!(self.0 => |ctx| {
|
async_with!(self.0 => |ctx| {
|
||||||
Ok(
|
let s = js_invoke_method_to_json::<(), bool>(
|
||||||
js_invoke_async_method_to_json::<(), bool>(
|
|
||||||
ctx.clone(),
|
ctx.clone(),
|
||||||
"auth",
|
"auth",
|
||||||
"is_authenticated",
|
"isAuthenticated",
|
||||||
&[]
|
&[]
|
||||||
)
|
)?.expect("[hey][smartypants] auth.isAuthenticated should return a boolean");
|
||||||
.await?
|
|
||||||
.expect("[hey][smartypants] auth.is_authenticated should return a boolean")
|
Ok(s)
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,9 +5,10 @@ use serde::de::DeserializeOwned;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::{Map, Value};
|
use serde_json::{Map, Value};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
/// Convert a `serde_json::Value` into a Boa `JsValue`
|
/// Convert a `serde_json::Value` into a Boa `JsValue`
|
||||||
pub fn json_value_to_js<'a>(value: &Value, ctx: Ctx<'a>) -> anyhow::Result<rquickjs::Value<'a>> {
|
pub fn json_value_to_js<'a>(value: &Value, ctx: Ctx<'a>) -> rquickjs::Result<rquickjs::Value<'a>> {
|
||||||
match value {
|
match value {
|
||||||
Value::Null => Ok(rquickjs::Value::new_null(ctx)),
|
Value::Null => Ok(rquickjs::Value::new_null(ctx)),
|
||||||
Value::Bool(b) => Ok(rquickjs::Value::new_bool(ctx, *b)),
|
Value::Bool(b) => Ok(rquickjs::Value::new_bool(ctx, *b)),
|
||||||
@ -30,7 +31,7 @@ pub fn json_value_to_js<'a>(value: &Value, ctx: Ctx<'a>) -> anyhow::Result<rquic
|
|||||||
let js_val = json_value_to_js(item, ctx.clone())?;
|
let js_val = json_value_to_js(item, ctx.clone())?;
|
||||||
js_arr.push(js_val);
|
js_arr.push(js_val);
|
||||||
}
|
}
|
||||||
js_arr.into_js(&ctx).map_err(|e| anyhow!(e))
|
js_arr.into_js(&ctx)
|
||||||
}
|
}
|
||||||
Value::Object(obj) => {
|
Value::Object(obj) => {
|
||||||
let mut js_obj = HashMap::<String, rquickjs::Value>::with_capacity(obj.len());
|
let mut js_obj = HashMap::<String, rquickjs::Value>::with_capacity(obj.len());
|
||||||
@ -40,12 +41,12 @@ pub fn json_value_to_js<'a>(value: &Value, ctx: Ctx<'a>) -> anyhow::Result<rquic
|
|||||||
js_obj.insert(key.clone(), js_val);
|
js_obj.insert(key.clone(), js_val);
|
||||||
}
|
}
|
||||||
|
|
||||||
js_obj.into_js(&ctx).map_err(|e| anyhow!(e))
|
js_obj.into_js(&ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert a Boa `JsValue` into a `serde_json::Value`
|
/// Convert a `Value` into a `serde_json::Value`
|
||||||
pub fn js_value_to_json<'a>(value: rquickjs::Value<'a>, ctx: Ctx<'a>) -> anyhow::Result<Value> {
|
pub fn js_value_to_json<'a>(value: rquickjs::Value<'a>, ctx: Ctx<'a>) -> anyhow::Result<Value> {
|
||||||
if value.is_null() || value.is_undefined() {
|
if value.is_null() || value.is_undefined() {
|
||||||
return Ok(Value::Null);
|
return Ok(Value::Null);
|
||||||
@ -70,7 +71,9 @@ pub fn js_value_to_json<'a>(value: rquickjs::Value<'a>, ctx: Ctx<'a>) -> anyhow:
|
|||||||
|
|
||||||
// Array?
|
// Array?
|
||||||
if obj.is_array() {
|
if obj.is_array() {
|
||||||
let obj: Array = Array::from_value(obj.into_value()).map_err(|e| anyhow!("{}", e))?;
|
let obj: Array = Array::from_value(obj.into_value())
|
||||||
|
.catch(&ctx)
|
||||||
|
.map_err(|e| anyhow!("{}", e))?;
|
||||||
let length = obj.len();
|
let length = obj.len();
|
||||||
let mut json_arr = Vec::<Value>::with_capacity(length);
|
let mut json_arr = Vec::<Value>::with_capacity(length);
|
||||||
|
|
||||||
@ -111,34 +114,53 @@ pub async fn js_invoke_async_method_to_json<'b, T, R>(
|
|||||||
) -> anyhow::Result<Option<R>>
|
) -> anyhow::Result<Option<R>>
|
||||||
where
|
where
|
||||||
T: Serialize,
|
T: Serialize,
|
||||||
R: DeserializeOwned,
|
R: DeserializeOwned + Debug,
|
||||||
{
|
{
|
||||||
let global = ctx.globals();
|
let global = ctx.globals();
|
||||||
let plugin_instance: Object<'b> = global.get("pluginInstance").map_err(|e| anyhow!("{e}"))?;
|
let plugin_instance: Object<'b> = global
|
||||||
|
.get("pluginInstance")
|
||||||
|
.catch(&ctx)
|
||||||
|
.map_err(|e| anyhow!("{e}"))?;
|
||||||
let core_val: Object<'b> = plugin_instance
|
let core_val: Object<'b> = plugin_instance
|
||||||
.get(endpoint_name)
|
.get(endpoint_name)
|
||||||
|
.catch(&ctx)
|
||||||
.map_err(|e| anyhow!("{e}"))?;
|
.map_err(|e| anyhow!("{e}"))?;
|
||||||
let js_fn: Function<'b> = core_val.get(name).map_err(|e| anyhow!("{e}"))?;
|
let js_fn: Function<'b> = core_val.get(name).catch(&ctx).map_err(|e| anyhow!("{e}"))?;
|
||||||
let mut args_js = Args::new(ctx.clone(), args.len() as usize);
|
let mut args_js = Args::new(ctx.clone(), args.len() as usize);
|
||||||
|
|
||||||
|
args_js
|
||||||
|
.this(core_val)
|
||||||
|
.catch(&ctx)
|
||||||
|
.map_err(|e| anyhow!("{e}"))?;
|
||||||
|
|
||||||
for arg in args.iter() {
|
for arg in args.iter() {
|
||||||
let arg_value = serde_json::to_value(arg).map_err(|e| anyhow!("{e}"))?;
|
let arg_value = serde_json::to_value(arg).map_err(|e| anyhow!("{e}"))?;
|
||||||
let arg_js = json_value_to_js(&arg_value, ctx.clone()).map_err(|e| anyhow!("{e}"))?;
|
let arg_js = json_value_to_js(&arg_value, ctx.clone())
|
||||||
args_js.push_arg(arg_js).map_err(|e| anyhow!("{e}"))?;
|
.catch(&ctx)
|
||||||
|
.map_err(|e| anyhow!("{e}"))?;
|
||||||
|
args_js
|
||||||
|
.push_arg(arg_js)
|
||||||
|
.catch(&ctx)
|
||||||
|
.map_err(|e| anyhow!("{e}"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let result_promise: Promise = js_fn.call_arg(args_js).map_err(|e| anyhow!("{e}"))?;
|
let result_promise: Promise = js_fn
|
||||||
|
.call_arg(args_js)
|
||||||
|
.catch(&ctx)
|
||||||
|
.map_err(|e| anyhow!("{e}"))?;
|
||||||
|
|
||||||
|
println!("Sync Result: {:?}", result_promise);
|
||||||
let result_future: rquickjs::Value = result_promise
|
let result_future: rquickjs::Value = result_promise
|
||||||
.into_future()
|
.into_future()
|
||||||
.await
|
.await
|
||||||
.catch(&ctx)
|
.catch(&ctx)
|
||||||
.map_err(|e| anyhow!("{e}"))?;
|
.map_err(|e| anyhow!("{e}"))?;
|
||||||
|
|
||||||
let value = js_value_to_json(result_future, ctx.clone()).map_err(|e| anyhow!("{e}"))?;
|
let value = js_value_to_json(result_future, ctx.clone())?;
|
||||||
|
|
||||||
if value.is_null() {
|
if value.is_null() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Some(
|
Ok(Some(
|
||||||
serde_json::from_value::<R>(value).map_err(|e| anyhow!("{e}"))?,
|
serde_json::from_value::<R>(value).map_err(|e| anyhow!("{e}"))?,
|
||||||
))
|
))
|
||||||
@ -155,20 +177,38 @@ where
|
|||||||
R: DeserializeOwned,
|
R: DeserializeOwned,
|
||||||
{
|
{
|
||||||
let global = ctx.globals();
|
let global = ctx.globals();
|
||||||
let plugin_instance: Object<'b> = global.get("pluginInstance").map_err(|e| anyhow!("{e}"))?;
|
let plugin_instance: Object<'b> = global
|
||||||
|
.get("pluginInstance")
|
||||||
|
.catch(&ctx)
|
||||||
|
.map_err(|e| anyhow!("{e}"))?;
|
||||||
let core_val: Object<'b> = plugin_instance
|
let core_val: Object<'b> = plugin_instance
|
||||||
.get(endpoint_name)
|
.get(endpoint_name)
|
||||||
|
.catch(&ctx)
|
||||||
.map_err(|e| anyhow!("{e}"))?;
|
.map_err(|e| anyhow!("{e}"))?;
|
||||||
let js_fn: Function<'b> = core_val.get(name).map_err(|e| anyhow!("{e}"))?;
|
let js_fn: Function<'b> = core_val.get(name).catch(&ctx).map_err(|e| anyhow!("{e}"))?;
|
||||||
let mut args_js = Args::new(ctx.clone(), args.len() as usize);
|
let mut args_js = Args::new(ctx.clone(), args.len() as usize);
|
||||||
|
|
||||||
|
args_js
|
||||||
|
.this(core_val)
|
||||||
|
.catch(&ctx)
|
||||||
|
.map_err(|e| anyhow!("{e}"))?;
|
||||||
|
|
||||||
for arg in args.iter().enumerate() {
|
for arg in args.iter().enumerate() {
|
||||||
let arg_value = serde_json::to_value(arg).map_err(|e| anyhow!("{e}"))?;
|
let arg_value = serde_json::to_value(arg).map_err(|e| anyhow!("{e}"))?;
|
||||||
let arg_js = json_value_to_js(&arg_value, ctx.clone()).map_err(|e| anyhow!("{e}"))?;
|
let arg_js = json_value_to_js(&arg_value, ctx.clone())
|
||||||
args_js.push_arg(arg_js).map_err(|e| anyhow!("{e}"))?;
|
.catch(&ctx)
|
||||||
|
.map_err(|e| anyhow!("{e}"))?;
|
||||||
|
args_js
|
||||||
|
.push_arg(arg_js)
|
||||||
|
.catch(&ctx)
|
||||||
|
.map_err(|e| anyhow!("{e}"))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let result: rquickjs::Value = js_fn.call_arg(args_js).map_err(|e| anyhow!("{e}"))?;
|
let result: rquickjs::Value = js_fn
|
||||||
let value = js_value_to_json(result, ctx.clone()).map_err(|e| anyhow!("{e}"))?;
|
.call_arg(args_js)
|
||||||
|
.catch(&ctx)
|
||||||
|
.map_err(|e| anyhow!("{e}"))?;
|
||||||
|
let value = js_value_to_json(result, ctx.clone())?;
|
||||||
|
|
||||||
if value.is_null() {
|
if value.is_null() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
|||||||
@ -100,7 +100,7 @@ async fn plugin() -> anyhow::Result<()> {
|
|||||||
repository: None,
|
repository: None,
|
||||||
version: "0.1.0".to_string(),
|
version: "0.1.0".to_string(),
|
||||||
};
|
};
|
||||||
let sender = plugin.create_context(PLUGIN_JS.to_string(), config.clone(), "".to_string(), "".to_string())?;
|
let sender = plugin.create_context(PLUGIN_JS.to_string(), config.clone(), "".to_string(), "".to_string(), "".into()).await?;
|
||||||
let (r1, r2) = tokio::join!(
|
let (r1, r2) = tokio::join!(
|
||||||
plugin.core.check_update(&sender, config.clone()),
|
plugin.core.check_update(&sender, config.clone()),
|
||||||
plugin.core.check_update(&sender, config.clone())
|
plugin.core.check_update(&sender, config.clone())
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user