feat: add rquickjs based JavaScript plugin system in Rust with common web API support

This commit is contained in:
Kingkor Roy Tirtho 2025-12-02 00:14:05 +06:00
parent a1672594a2
commit f3a809752a
26 changed files with 2306 additions and 2583 deletions

View File

@ -7,6 +7,7 @@ import '../../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `AlbumCommands`, `ArtistCommands`, `AudioSourceCommands`, `AuthCommands`, `BrowseCommands`, `CoreCommands`, `PlaylistCommands`, `SearchCommands`, `TrackCommands`, `UserCommands`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<PluginCommand>>
abstract class PluginCommand implements RustOpaqueInterface {}

View File

@ -4,13 +4,13 @@
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../../frb_generated.dart';
import '../../lib.dart';
import 'models/core.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
import 'senders.dart';
// These functions are ignored because they are not marked as `pub`: `js_executor_thread`
// These functions are ignored because they are not marked as `pub`: `console_log`, `js_executor_thread`, `register_globals`, `set_timeout`
// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `fmt`
// These functions are ignored (category: IgnoreBecauseExplicitAttribute): `create_context`
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<OpaqueSender>>
abstract class OpaqueSender implements RustOpaqueInterface {
@ -19,9 +19,6 @@ abstract class OpaqueSender implements RustOpaqueInterface {
set sender(SenderPluginCommand sender);
}
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<Sender < PluginCommand >>>
abstract class SenderPluginCommand implements RustOpaqueInterface {}
class SpotubePlugin {
final PluginArtistSender artist;
final PluginAlbumSender album;

View File

@ -22,6 +22,7 @@ import 'dart:convert';
import 'frb_generated.dart';
import 'frb_generated.io.dart'
if (dart.library.js_interop) 'frb_generated.web.dart';
import 'lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
/// Main entrypoint of the Rust API

View File

@ -21,6 +21,7 @@ import 'dart:async';
import 'dart:convert';
import 'dart:ffi' as ffi;
import 'frb_generated.dart';
import 'lib.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {

View File

@ -0,0 +1,10 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import '../frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<PluginCoreEndpoint>>
abstract class PluginCoreEndpoint implements RustOpaqueInterface {}

10
lib/src/rust/lib.dart Normal file
View File

@ -0,0 +1,10 @@
// This file is automatically generated, so please do not edit it.
// @generated by `flutter_rust_bridge`@ 2.11.1.
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
import 'frb_generated.dart';
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<Sender < PluginCommand >>>
abstract class SenderPluginCommand implements RustOpaqueInterface {}

2374
rust/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -12,20 +12,16 @@ path = "src/main.rs"
[dependencies]
flutter_rust_bridge = "=2.11.1"
boa_engine = "0.21.0"
boa_runtime = "0.21.0"
boa_gc = "0.21.0"
anyhow = "1"
reqwest = { version = "0.12.x" }
http = { version = "1.3.1" }
serde_json = "1"
serde = { version = "1.0.228", features = ["derive"] }
rquickjs = { version = "0", features = ["chrono", "futures"] }
tokio = { version = "1.48.0", features = ["full"] }
heck = "0.5.0"
futures-concurrency = "7.6.3"
futures-lite = "2.6.1"
llrt_modules = { git = "https://github.com/awslabs/llrt.git", rev = "7d749dd18cf26a2e51119094c3b945975ae57bd4", features = ["abort", "buffer", "console", "crypto", "events", "exceptions", "fetch", "navigator", "url", "timers"] }
[patch."https://github.com/DelSkayn/rquickjs"]
rquickjs = "0.10.0"
[lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] }

View File

@ -1,121 +0,0 @@
use boa_engine::context::time::JsInstant;
use boa_engine::job::{GenericJob, Job, JobExecutor, NativeAsyncJob, PromiseJob, TimeoutJob};
use boa_engine::{Context, JsResult};
use flutter_rust_bridge::frb;
use futures_concurrency::future::FutureGroup;
use futures_lite::{future, StreamExt};
use std::cell::RefCell;
use std::collections::{BTreeMap, VecDeque};
use std::ops::DerefMut;
use std::rc::Rc;
use tokio::task;
#[frb(ignore)]
pub struct Queue {
async_jobs: RefCell<VecDeque<NativeAsyncJob>>,
promise_jobs: RefCell<VecDeque<PromiseJob>>,
timeout_jobs: RefCell<BTreeMap<JsInstant, TimeoutJob>>,
generic_jobs: RefCell<VecDeque<GenericJob>>,
}
impl Queue {
pub fn new() -> Self {
Self {
async_jobs: RefCell::default(),
promise_jobs: RefCell::default(),
timeout_jobs: RefCell::default(),
generic_jobs: RefCell::default(),
}
}
fn drain_timeout_jobs(&self, context: &mut Context) {
let now = context.clock().now();
let mut timeouts_borrow = self.timeout_jobs.borrow_mut();
let mut jobs_to_keep = timeouts_borrow.split_off(&now);
jobs_to_keep.retain(|_, job| !job.is_cancelled());
let jobs_to_run = std::mem::replace(timeouts_borrow.deref_mut(), jobs_to_keep);
drop(timeouts_borrow);
for job in jobs_to_run.into_values() {
if let Err(e) = job.call(context) {
eprintln!("Uncaught {e}");
}
}
}
fn drain_jobs(&self, context: &mut Context) {
// Run the timeout jobs first.
self.drain_timeout_jobs(context);
let job = self.generic_jobs.borrow_mut().pop_front();
if let Some(generic) = job {
if let Err(err) = generic.call(context) {
eprintln!("Uncaught {err}");
}
}
let jobs = std::mem::take(&mut *self.promise_jobs.borrow_mut());
for job in jobs {
if let Err(e) = job.call(context) {
eprintln!("Uncaught {e}");
}
}
context.clear_kept_objects();
}
}
impl JobExecutor for Queue {
fn enqueue_job(self: Rc<Self>, job: Job, context: &mut Context) {
match job {
Job::PromiseJob(job) => self.promise_jobs.borrow_mut().push_back(job),
Job::AsyncJob(job) => self.async_jobs.borrow_mut().push_back(job),
Job::TimeoutJob(t) => {
let now = context.clock().now();
self.timeout_jobs.borrow_mut().insert(now + t.timeout(), t);
}
Job::GenericJob(g) => self.generic_jobs.borrow_mut().push_back(g),
_ => panic!("unsupported job type"),
}
}
// While the sync flavor of `run_jobs` will block the current thread until all the jobs have finished...
fn run_jobs(self: Rc<Self>, context: &mut Context) -> JsResult<()> {
task::block_in_place(|| {
let runtime = tokio::runtime::Handle::current(); // Get the existing runtime handle
// Use LocalSet to run the async job on the current thread
runtime.block_on(self.run_jobs_async(&RefCell::new(context)))
})
}
// ...the async flavor won't, which allows concurrent execution with external async tasks.
async fn run_jobs_async(self: Rc<Self>, context: &RefCell<&mut Context>) -> JsResult<()> {
let mut group = FutureGroup::new();
loop {
for job in std::mem::take(&mut *self.async_jobs.borrow_mut()) {
group.insert(job.call(context));
}
if group.is_empty()
&& self.promise_jobs.borrow().is_empty()
&& self.timeout_jobs.borrow().is_empty()
&& self.generic_jobs.borrow().is_empty()
{
// All queues are empty. We can exit.
return Ok(());
}
// We have some jobs pending on the microtask queue. Try to poll the pending
// tasks once to see if any of them finished, and run the pending microtasks
// otherwise.
if let Some(Err(err)) = future::poll_once(group.next()).await.flatten() {
eprintln!("Uncaught {err}");
};
// Only one macrotask can be executed before the next drain of the microtask queue.
self.drain_jobs(&mut context.borrow_mut());
task::yield_now().await
}
}
}

View File

@ -12,9 +12,9 @@ use crate::internal::playlist::PluginPlaylistEndpoint;
use crate::internal::search::PluginSearchEndpoint;
use crate::internal::track::PluginTrackEndpoint;
use crate::internal::user::PluginUserEndpoint;
use boa_engine::Context;
use flutter_rust_bridge::frb;
use std::fmt::Debug;
use rquickjs::AsyncContext;
use tokio::sync::oneshot;
fn send_response<T>(tx: oneshot::Sender<T>, response: T) -> anyhow::Result<()>
@ -26,8 +26,8 @@ where
}
#[frb(ignore)]
pub async fn execute_artists(command: ArtistCommands, context: &mut Context) -> anyhow::Result<()> {
let mut artist = PluginArtistEndpoint::new(context);
pub async fn execute_artists(command: ArtistCommands, context: &AsyncContext) -> anyhow::Result<()> {
let artist = PluginArtistEndpoint::new(context);
match command {
ArtistCommands::GetArtist { id, response_tx } => {
let artist = artist.get_artist(id).await;
@ -72,8 +72,8 @@ pub async fn execute_artists(command: ArtistCommands, context: &mut Context) ->
}
#[frb(ignore)]
pub async fn execute_albums(command: AlbumCommands, context: &mut Context) -> anyhow::Result<()> {
let mut album = PluginAlbumEndpoint::new(context);
pub async fn execute_albums(command: AlbumCommands, context: &AsyncContext) -> anyhow::Result<()> {
let album = PluginAlbumEndpoint::new(context);
match command {
AlbumCommands::GetAlbum { id, response_tx } => {
let album = album.get_album(id).await;
@ -110,9 +110,9 @@ pub async fn execute_albums(command: AlbumCommands, context: &mut Context) -> an
#[frb(ignore)]
pub async fn execute_audio_source(
command: AudioSourceCommands,
context: &mut Context,
context: &AsyncContext,
) -> anyhow::Result<()> {
let mut audio_source = PluginAudioSourceEndpoint::new(context);
let audio_source = PluginAudioSourceEndpoint::new(context);
match command {
AudioSourceCommands::Matches { track, response_tx } => {
let audio_source = audio_source.matches(track).await;
@ -129,15 +129,15 @@ pub async fn execute_audio_source(
}
#[frb(ignore)]
pub async fn execute_auth(command: AuthCommands, context: &mut Context) -> anyhow::Result<()> {
let mut auth = PluginAuthEndpoint::new(context);
pub async fn execute_auth(command: AuthCommands, context: &AsyncContext) -> anyhow::Result<()> {
let auth = PluginAuthEndpoint::new(context);
match command {
AuthCommands::Authenticate { response_tx } => {
let res = auth.authenticate().await;
send_response(response_tx, res)
}
AuthCommands::IsAuthenticated { response_tx } => {
let res = auth.is_authenticated();
let res = auth.is_authenticated().await;
send_response(response_tx, res)
}
AuthCommands::Logout { response_tx } => {
@ -148,8 +148,8 @@ pub async fn execute_auth(command: AuthCommands, context: &mut Context) -> anyho
}
#[frb(ignore)]
pub async fn execute_browse(command: BrowseCommands, context: &mut Context) -> anyhow::Result<()> {
let mut browse = PluginBrowseEndpoint::new(context);
pub async fn execute_browse(command: BrowseCommands, context: &AsyncContext) -> anyhow::Result<()> {
let browse = PluginBrowseEndpoint::new(context);
match command {
BrowseCommands::Sections {
offset,
@ -172,8 +172,8 @@ pub async fn execute_browse(command: BrowseCommands, context: &mut Context) -> a
}
#[frb(ignore)]
pub async fn execute_core(command: CoreCommands, context: &mut Context) -> anyhow::Result<()> {
let mut core = PluginCoreEndpoint::new(context);
pub async fn execute_core(command: CoreCommands, context: &AsyncContext) -> anyhow::Result<()> {
let core = PluginCoreEndpoint::new(context);
match command {
CoreCommands::CheckUpdate {
response_tx,
@ -190,7 +190,7 @@ pub async fn execute_core(command: CoreCommands, context: &mut Context) -> anyho
send_response(response_tx, res)
}
CoreCommands::Support { response_tx } => {
let res = core.support();
let res = core.support().await;
send_response(response_tx, res)
}
}
@ -199,9 +199,9 @@ pub async fn execute_core(command: CoreCommands, context: &mut Context) -> anyho
#[frb(ignore)]
pub async fn execute_playlist(
command: PlaylistCommands,
context: &mut Context,
context: &AsyncContext,
) -> anyhow::Result<()> {
let mut playlist = PluginPlaylistEndpoint::new(context);
let playlist = PluginPlaylistEndpoint::new(context);
match command {
PlaylistCommands::GetPlaylist { id, response_tx } => {
let playlist = playlist.get_playlist(id).await;
@ -284,11 +284,11 @@ pub async fn execute_playlist(
}
#[frb(ignore)]
pub async fn execute_search(command: SearchCommands, context: &mut Context) -> anyhow::Result<()> {
let mut search = PluginSearchEndpoint::new(context);
pub async fn execute_search(command: SearchCommands, context: &AsyncContext) -> anyhow::Result<()> {
let search = PluginSearchEndpoint::new(context);
match command {
SearchCommands::Chips { response_tx } => {
let chips = search.chips();
let chips = search.chips().await;
send_response(response_tx, chips)
}
SearchCommands::All { query, response_tx } => {
@ -335,8 +335,8 @@ pub async fn execute_search(command: SearchCommands, context: &mut Context) -> a
}
#[frb(ignore)]
pub async fn execute_track(command: TrackCommands, context: &mut Context) -> anyhow::Result<()> {
let mut track = PluginTrackEndpoint::new(context);
pub async fn execute_track(command: TrackCommands, context: &AsyncContext) -> anyhow::Result<()> {
let track = PluginTrackEndpoint::new(context);
match command {
TrackCommands::GetTrack { id, response_tx } => {
let res = track.get_track(id).await;
@ -358,8 +358,8 @@ pub async fn execute_track(command: TrackCommands, context: &mut Context) -> any
}
#[frb(ignore)]
pub async fn execute_user(command: UserCommands, context: &mut Context) -> anyhow::Result<()> {
let mut user = PluginUserEndpoint::new(context);
pub async fn execute_user(command: UserCommands, context: &AsyncContext) -> anyhow::Result<()> {
let user = PluginUserEndpoint::new(context);
match command {
UserCommands::Me { response_tx } => {
let me = user.me().await;

View File

@ -3,4 +3,3 @@ pub mod plugin;
pub mod executors;
pub mod senders;
pub mod models;
mod event_loop;

View File

@ -1,5 +1,4 @@
use crate::api::plugin::commands::PluginCommand;
use crate::api::plugin::event_loop::Queue;
use crate::api::plugin::executors::{
execute_albums, execute_artists, execute_audio_source, execute_auth, execute_browse,
execute_core, execute_playlist, execute_search, execute_track, execute_user,
@ -10,122 +9,96 @@ use crate::api::plugin::senders::{
PluginBrowseSender, PluginCoreSender, PluginPlaylistSender, PluginSearchSender,
PluginTrackSender, PluginUserSender,
};
use crate::internal::apis::fetcher::ReqwestFetcher;
use anyhow::anyhow;
use boa_engine::job::JobExecutor;
use boa_engine::{Context, Source};
use boa_runtime::{fetch, interval, microtask, text, Console, DefaultLogger};
use flutter_rust_bridge::frb;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::Arc;
use llrt_modules::{abort, buffer, console, crypto, events, exceptions, fetch, navigator, timers, url, util};
use llrt_modules::module_builder::ModuleBuilder;
use rquickjs::{async_with, AsyncContext, AsyncRuntime, Error};
use std::thread;
use std::time::Duration;
use tokio::runtime::Runtime;
use tokio::sync::mpsc;
use tokio::sync::mpsc::Sender;
use tokio::sync::{mpsc, Mutex};
use tokio::task;
use tokio::task::LocalSet;
#[derive(Debug, Clone)]
#[frb(opaque)]
pub struct OpaqueSender {
pub sender: Sender<PluginCommand>,
}
async fn js_poller_thread(context: Arc<Mutex<Context>>, queue: Rc<Queue>) -> anyhow::Result<()> {
let local_set = task::LocalSet::new();
#[frb(ignore)]
async fn create_context() -> anyhow::Result<(AsyncContext, AsyncRuntime)> {
let runtime = AsyncRuntime::new().expect("Unable to create async runtime");
local_set
.run_until(async {
let mut ctx = context.lock().await;
queue.run_jobs_async(&RefCell::new(&mut *ctx)).await
let mut module_builder = ModuleBuilder::new();
module_builder = module_builder
.with_global(abort::init)
.with_global(buffer::init)
.with_global(console::init)
.with_global(crypto::init)
.with_global(events::init)
.with_global(exceptions::init)
.with_global(fetch::init)
.with_global(navigator::init)
.with_global(url::init)
.with_global(timers::init)
.with_global(util::init)
;
let (module_resolver, module_loader, global_attachment) = module_builder.build();
runtime
.set_loader((module_resolver,), (module_loader,))
.await;
let context = AsyncContext::full(&runtime)
.await
.expect("Unable to create async context");
async_with!(context => |ctx| {
global_attachment.attach(&ctx)?;
Ok::<(), Error>(())
})
.await
.map_err(|e| anyhow!("{}", e))?;
Ok(())
}
.map_err(|e| anyhow!("Failed to register globals: {}", e))?;
// #[frb(ignore)]
Ok((context, runtime))
}
#[frb(ignore)]
async fn js_executor_thread(
rx: &mut mpsc::Receiver<PluginCommand>,
context: Arc<Mutex<Context>>,
ctx: &AsyncContext,
) -> anyhow::Result<()> {
if let Some(command) = rx.recv().await {
let result = {
while let Some(command) = rx.recv().await {
println!("JS Executor thread received command: {:?}", command);
match command {
PluginCommand::Artist(commands) => {
let mut ctx = context.lock().await;
execute_artists(commands, &mut *ctx).await
}
PluginCommand::Album(commands) => {
let mut ctx = context.lock().await;
execute_albums(commands, &mut *ctx).await
}
PluginCommand::AudioSource(commands) => {
let mut ctx = context.lock().await;
execute_audio_source(commands, &mut *ctx).await
}
PluginCommand::Auth(commands) => {
let mut ctx = context.lock().await;
execute_auth(commands, &mut *ctx).await
}
PluginCommand::Browse(commands) => {
let mut ctx = context.lock().await;
execute_browse(commands, &mut *ctx).await
}
PluginCommand::Core(commands) => {
let mut ctx = context.lock().await;
execute_core(commands, &mut *ctx).await
}
PluginCommand::Playlist(commands) => {
let mut ctx = context.lock().await;
execute_playlist(commands, &mut *ctx).await
}
PluginCommand::Search(commands) => {
let mut ctx = context.lock().await;
execute_search(commands, &mut *ctx).await
}
PluginCommand::Track(commands) => {
let mut ctx = context.lock().await;
execute_track(commands, &mut *ctx).await
}
PluginCommand::User(commands) => {
let mut ctx = context.lock().await;
execute_user(commands, &mut *ctx).await
}
PluginCommand::Shutdown => {
if let PluginCommand::Shutdown = command {
println!("JS Executor thread shutting down.");
return anyhow::Ok(());
}
}
let ctx = ctx.clone();
task::spawn_local(async move {
let result = match command {
PluginCommand::Artist(commands) => execute_artists(commands, &ctx).await,
PluginCommand::Album(commands) => execute_albums(commands, &ctx).await,
PluginCommand::AudioSource(commands) => execute_audio_source(commands, &ctx).await,
PluginCommand::Auth(commands) => execute_auth(commands, &ctx).await,
PluginCommand::Browse(commands) => execute_browse(commands, &ctx).await,
PluginCommand::Core(commands) => execute_core(commands, &ctx).await,
PluginCommand::Playlist(commands) => execute_playlist(commands, &ctx).await,
PluginCommand::Search(commands) => execute_search(commands, &ctx).await,
PluginCommand::Track(commands) => execute_track(commands, &ctx).await,
PluginCommand::User(commands) => execute_user(commands, &ctx).await,
PluginCommand::Shutdown => unreachable!(),
};
println!("JS executor command completed");
return result;
if let Err(e) = result {
eprintln!("Error executing command: {:?}", e);
}
});
}
Ok(())
}
#[frb(ignore)]
pub async fn create_context() -> anyhow::Result<(Context, Rc<Queue>)> {
let queue = Rc::new(Queue::new());
let mut context = Context::builder()
.job_executor(queue.clone())
.build()
.map_err(|e| anyhow!("{}", e))?;
Console::register_with_logger(DefaultLogger, &mut context).map_err(|e| anyhow!("{}", e))?;
fetch::register(ReqwestFetcher::new(), None, &mut context).map_err(|e| anyhow!("{}", e))?;
interval::register(&mut context).map_err(|e| anyhow!("{}", e))?;
microtask::register(None, &mut context).map_err(|e| anyhow!("{}", e))?;
text::register(None, &mut context).map_err(|e| anyhow!("{}", e))?;
interval::register(&mut context).map_err(|e| anyhow!("{}", e))?;
microtask::register(None, &mut context).map_err(|e| anyhow!("{}", e))?;
Ok((context, queue))
}
pub struct SpotubePlugin {
pub artist: PluginArtistSender,
pub album: PluginAlbumSender,
@ -156,7 +129,7 @@ impl SpotubePlugin {
}
}
// #[frb(sync)]
#[frb(sync)]
pub fn new_context(
plugin_script: String,
plugin_config: PluginConfiguration,
@ -164,10 +137,13 @@ impl SpotubePlugin {
let (command_tx, mut command_rx) = mpsc::channel(32);
let _thread_handle = thread::spawn(move || {
let rt = Runtime::new().unwrap();
if let Err(e) = rt.block_on(async {
let (context, queue) = create_context().await.unwrap();
let context_arc_mutex = Arc::new(Mutex::new(context));
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let local = LocalSet::new();
if let Err(e) = local.block_on(&rt, async {
let (ctx, runtime) = create_context().await?;
let injection = format!(
"globalThis.pluginInstance = new {}();",
@ -175,38 +151,11 @@ impl SpotubePlugin {
);
let script = format!("{}\n{}", plugin_script, injection);
{
let context_refcell = context_arc_mutex.clone();
ctx.with(|cx| cx.eval::<(), _>(script.as_str())).await?;
context_refcell
.lock()
.await
.eval(Source::from_bytes(script.as_bytes()))
.map_err(|e| anyhow!("{}", e))?;
if let Err(e) = js_executor_thread(&mut command_rx, &ctx).await {
eprintln!("JS executor error: {}", e);
}
loop {
let executor = js_executor_thread(&mut command_rx, context_arc_mutex.clone());
let poller = js_poller_thread(context_arc_mutex.clone(), queue.clone());
let sleep_timer = tokio::time::sleep(Duration::from_millis(10));
tokio::select!(
res = executor => {
if let Err(e) = res {
eprintln!("JS Executor task error: {}", e);
break;
}
},
res = poller => {
if let Err(e) = res {
eprintln!("JS Poller task error: {}", e);
break;
}
},
_ = sleep_timer => {},
);
}
anyhow::Ok(())
}) {
eprintln!("JS Executor thread error: {}", e);

View File

@ -27,6 +27,7 @@
use crate::api::plugin::commands::*;
use crate::api::plugin::plugin::*;
use crate::*;
use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt};
use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable};
use flutter_rust_bridge::{Handler, IntoIntoDart};
@ -5764,6 +5765,7 @@ mod io {
use super::*;
use crate::api::plugin::commands::*;
use crate::api::plugin::plugin::*;
use crate::*;
use flutter_rust_bridge::for_generated::byteorder::{
NativeEndian, ReadBytesExt, WriteBytesExt,
};

View File

@ -1,148 +1,94 @@
use crate::api::plugin::models::album::SpotubeFullAlbumObject;
use crate::api::plugin::models::pagination::SpotubePaginationResponseObject;
use crate::internal::utils;
use anyhow::anyhow;
use boa_engine::{js_string, Context, JsValue};
use crate::internal::utils::js_invoke_async_method_to_json;
use flutter_rust_bridge::frb;
use rquickjs::{async_with, AsyncContext};
use serde_json::Value;
#[derive(Debug)]
pub struct PluginAlbumEndpoint<'a>(&'a mut Context);
pub struct PluginAlbumEndpoint<'a>(&'a AsyncContext);
impl<'a> PluginAlbumEndpoint<'a> {
#[frb(ignore)]
pub fn new(context: &'a mut Context) -> PluginAlbumEndpoint<'a> {
pub fn new(context: &'a AsyncContext) -> PluginAlbumEndpoint<'a> {
PluginAlbumEndpoint(context)
}
fn album_obj(&mut self) -> anyhow::Result<JsValue> {
let global = self.0.global_object();
let plugin_instance = global
.get(js_string!("pluginInstance"), self.0)
.map_err(|e| anyhow!("{}", e))
.and_then(|a| a.as_object().ok_or(anyhow!("Not an object")))?;
plugin_instance
.get(js_string!("album"), self.0)
.or_else(|e| Err(anyhow!("album not found: \n{}", e)))
}
pub async fn get_album(&mut self, id: String) -> anyhow::Result<SpotubeFullAlbumObject> {
let album_val = self.album_obj()?;
let album_object = album_val.as_object().ok_or(anyhow!("Not an object"))?;
let get_album_fn = album_object
.get(js_string!("getAlbum"), self.0)
.map_err(|e| anyhow!("JS error while accessing getAlbum: {}", e))?
.as_function()
.ok_or(anyhow!("getAlbum is not a function"))?;
let args = [JsValue::from(js_string!(id))];
let res_json =
utils::js_call_to_json(get_album_fn.call(&album_val, &args, self.0), self.0).await?;
serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
pub async fn get_album(&self, id: String) -> anyhow::Result<SpotubeFullAlbumObject> {
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json(ctx.clone(), "album", "getAlbum", &[id]).await
?.expect("[hey][smartypants] album.getAlbum should return a SpotifyFullAlbumObject")
)
})
.await
}
pub async fn tracks(
&mut self,
&self,
id: String,
offset: Option<u32>,
limit: Option<u32>,
) -> anyhow::Result<SpotubePaginationResponseObject> {
let album_val = self.album_obj()?;
let album_object = album_val.as_object().ok_or(anyhow!("Not an object"))?;
let tracks_fn = album_object
.get(js_string!("tracks"), self.0)
.map_err(|e| anyhow!("JS error while accessing tracks: {}", e))?
.as_function()
.ok_or(anyhow!("tracks is not a function"))?;
let args: [JsValue; 3] = [
JsValue::from(js_string!(id)),
match offset {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
match limit {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
];
let res_json = utils::js_call_to_json(tracks_fn.call(&album_val, &args, self.0), self.0).await?;
serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
ctx.clone(),
"album",
"tracks",
&[Value::String(id), serde_json::to_value(offset)?, serde_json::to_value(limit.unwrap())?]
)
.await?
.expect("[hey][smartypants] album.tracks should return a SpotifyPaginationResponseObject")
)
}).await
}
pub async fn releases(
&mut self,
&self,
offset: Option<u32>,
limit: Option<u32>,
) -> anyhow::Result<SpotubePaginationResponseObject> {
let album_val = self.album_obj()?;
let album_object = album_val.as_object().ok_or(anyhow!("Not an object"))?;
let releases_fn = album_object
.get(js_string!("releases"), self.0)
.map_err(|e| anyhow!("JS error while accessing releases: {}", e))?
.as_function()
.ok_or(anyhow!("releases is not a function"))?;
let args: [JsValue; 2] = [
match offset {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
match limit {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
];
let res_json =
utils::js_call_to_json(releases_fn.call(&album_val, &args, self.0), self.0).await?;
serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
ctx.clone(),
"album",
"releases",
&[serde_json::to_value(offset)?, serde_json::to_value(limit.unwrap())?]
)
.await?
.expect("[hey][smartypants] album.releases should return a SpotifyPaginationResponseObject")
)
}).await
}
pub async fn save(&mut self, ids: Vec<String>) -> anyhow::Result<()> {
let album_val = self.album_obj()?;
let album_object = album_val.as_object().ok_or(anyhow!("Not an object"))?;
let save_fn = album_object
.get(js_string!("save"), self.0)
.map_err(|e| anyhow!("JS error while accessing save: {}", e))?
.as_function()
.ok_or(anyhow!("save is not a function"))?;
let ids_val = utils::vec_string_to_js_array(ids, self.0)?;
let args = [ids_val.into()];
utils::js_call_to_void(save_fn.call(&album_val, &args, self.0), self.0).await?;
Ok(())
pub async fn save(&self, ids: Vec<String>) -> anyhow::Result<()> {
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<_, ()>(
ctx.clone(),
"album",
"save",
&[Value::Array(ids.into_iter().map(|id| Value::String(id)).collect())]
)
.await?
.expect("[hey][smartypants] album.save should return a SpotifyPaginationResponseObject")
)
}).await
}
pub async fn unsave(&mut self, ids: Vec<String>) -> anyhow::Result<()> {
let album_val = self.album_obj()?;
let album_object = album_val.as_object().ok_or(anyhow!("Not an object"))?;
let unsave_fn = album_object
.get(js_string!("unsave"), self.0)
.map_err(|e| anyhow!("JS error while accessing save: {}", e))?
.as_function()
.ok_or(anyhow!("save is not a function"))?;
let ids_val = utils::vec_string_to_js_array(ids, self.0)?;
let args = [ids_val.into()];
utils::js_call_to_void(unsave_fn.call(&album_val, &args, self.0), self.0).await?;
Ok(())
pub async fn unsave(&self, ids: Vec<String>) -> anyhow::Result<()> {
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<_, ()>(
ctx.clone(),
"album",
"unsave",
&[Value::Array(ids.into_iter().map(|id| Value::String(id)).collect())]
)
.await?
.expect("[hey][smartypants] album.unsave should return a SpotifyPaginationResponseObject")
)
}).await
}
}

View File

@ -1,59 +0,0 @@
use std::{cell::RefCell, rc::Rc};
use boa_engine::{Context, Finalize, JsData, JsResult, Trace};
use boa_runtime::fetch::{request::JsRequest, response::JsResponse, Fetcher};
#[derive(Default, Debug, Clone, Trace, Finalize, JsData)]
pub struct ReqwestFetcher {
#[unsafe_ignore_trace]
client: reqwest::Client,
}
impl ReqwestFetcher {
pub fn new() -> Self {
ReqwestFetcher {
client: reqwest::Client::new(),
}
}
}
impl Fetcher for ReqwestFetcher {
async fn fetch(
self: Rc<Self>,
request: JsRequest,
_context: &RefCell<&mut Context>,
) -> JsResult<JsResponse> {
use boa_engine::{JsError, JsString};
let request = request.into_inner();
let method = request.method().clone();
let url = request.uri().to_string();
let req = self
.client
.request(method, &url)
.headers(request.headers().clone());
let req = req
.body(request.body().clone())
.build()
.map_err(JsError::from_rust)?;
let resp = self.client.execute(req).await.map_err(JsError::from_rust)?;
let status = resp.status();
let headers = resp.headers().clone();
let bytes = resp.bytes().await.map_err(JsError::from_rust)?;
let mut builder = http::Response::builder().status(status.as_u16());
for k in headers.keys() {
for v in headers.get_all(k) {
builder = builder.header(k.as_str(), v);
}
}
builder
.body(bytes.to_vec())
.map_err(JsError::from_rust)
.map(|request| JsResponse::basic(JsString::from(url), request))
}
}

View File

@ -1,178 +1,115 @@
use flutter_rust_bridge::frb;
use crate::api::plugin::models::artist::SpotubeFullArtistObject;
use crate::api::plugin::models::pagination::SpotubePaginationResponseObject;
use crate::internal::utils;
use anyhow::anyhow;
use boa_engine::{js_string, Context, JsValue};
use flutter_rust_bridge::frb;
use crate::internal::utils::js_invoke_async_method_to_json;
use rquickjs::{async_with, AsyncContext};
use serde_json::Value;
#[derive(Debug)]
pub struct PluginArtistEndpoint<'a>(&'a mut Context);
pub struct PluginArtistEndpoint<'a>(&'a AsyncContext);
impl<'a> PluginArtistEndpoint<'a> {
#[frb(ignore)]
pub fn new(context: &'a mut Context) -> PluginArtistEndpoint<'a> {
pub fn new(context: &'a AsyncContext) -> PluginArtistEndpoint<'a> {
PluginArtistEndpoint(context)
}
fn artist_obj(&mut self) -> anyhow::Result<JsValue> {
let global = self.0.global_object();
let plugin_instance = global
.get(js_string!("pluginInstance"), self.0)
.map_err(|e| anyhow!("{}", e))
.and_then(|a| a.as_object().ok_or(anyhow!("Not an object")))?;
return plugin_instance
.get(js_string!("artist"), self.0)
.or_else(|e| Err(anyhow!("artist not found: \n{}", e)));
}
pub async fn get_artist(&mut self, id: String) -> anyhow::Result<SpotubeFullArtistObject> {
let artist_val = self.artist_obj()?;
let artist_object = artist_val.as_object().ok_or(anyhow!("Not an object"))?;
let get_artist_fn = artist_object
.get(js_string!("getArtist"), self.0)
.map_err(|e| anyhow!("JS error while accessing getArtist: {}", e))?
.as_function()
.ok_or(anyhow!("getArtist is not a function"))?;
let args = [JsValue::from(js_string!(id))];
let res_json =
utils::js_call_to_json(get_artist_fn.call(&artist_val, &args, self.0), self.0).await?;
serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
pub async fn get_artist(&self, id: String) -> anyhow::Result<SpotubeFullArtistObject> {
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json(ctx.clone(), "artist", "getArtist", &[id]).await
?.expect("[hey][smartypants] artist.getArtist should return a SpotifyFullArtistObject")
)
})
.await
}
pub async fn top_tracks(
&mut self,
&self,
id: String,
offset: Option<u32>,
limit: Option<u32>,
) -> anyhow::Result<SpotubePaginationResponseObject> {
let artist_val = self.artist_obj()?;
let artist_object = artist_val.as_object().ok_or(anyhow!("Not an object"))?;
let top_tracks_fn = artist_object
.get(js_string!("topTracks"), self.0)
.map_err(|e| anyhow!("JS error while accessing getArtist: {}", e))?
.as_function()
.ok_or(anyhow!("getArtist is not a function"))?;
let args: [JsValue; 3] = [
JsValue::from(js_string!(id)),
match offset {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
match limit {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
];
let res_json =
utils::js_call_to_json(top_tracks_fn.call(&artist_val, &args, self.0), self.0).await?;
serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
ctx.clone(),
"artist",
"topTracks",
&[Value::String(id), serde_json::to_value(offset)?, serde_json::to_value(limit.unwrap())?]
)
.await?
.expect("[hey][smartypants] album.tracks should return a SpotifyPaginationResponseObject")
)
}).await
}
pub async fn albums(
&mut self,
&self,
id: String,
offset: Option<u32>,
limit: Option<u32>,
) -> anyhow::Result<SpotubePaginationResponseObject> {
let artist_val = self.artist_obj()?;
let artist_object = artist_val.as_object().ok_or(anyhow!("Not an object"))?;
let albums_fn = artist_object
.get(js_string!("albums"), self.0)
.map_err(|e| anyhow!("JS error while accessing albums: {}", e))?
.as_function()
.ok_or(anyhow!("albums is not a function"))?;
let args: [JsValue; 3] = [
JsValue::from(js_string!(id)),
match offset {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
match limit {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
];
let res_json =
utils::js_call_to_json(albums_fn.call(&artist_val, &args, self.0), self.0).await?;
serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
ctx.clone(),
"artist",
"albums",
&[Value::String(id), serde_json::to_value(offset)?, serde_json::to_value(limit.unwrap())?]
)
.await?
.expect("[hey][smartypants] artist.albums should return a SpotifyPaginationResponseObject")
)
}).await
}
pub async fn related(
&mut self,
&self,
id: String,
offset: Option<u32>,
limit: Option<u32>,
) -> anyhow::Result<SpotubePaginationResponseObject> {
let artist_val = self.artist_obj()?;
let artist_object = artist_val.as_object().ok_or(anyhow!("Not an object"))?;
let related_fn = artist_object
.get(js_string!("related"), self.0)
.map_err(|e| anyhow!("JS error while accessing related: {}", e))?
.as_function()
.ok_or(anyhow!("related is not a function"))?;
let args = [
JsValue::from(js_string!(id)),
match offset {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
match limit {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
];
let res_json =
utils::js_call_to_json(related_fn.call(&artist_val, &args, self.0), self.0).await?;
serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
ctx.clone(),
"artist",
"related",
&[Value::String(id), serde_json::to_value(offset)?, serde_json::to_value(limit.unwrap())?]
)
.await?
.expect("[hey][smartypants] artist.related should return a SpotifyPaginationResponseObject")
)
}).await
}
pub async fn save(&mut self, ids: Vec<String>) -> anyhow::Result<()> {
let artist_val = self.artist_obj()?;
let artist_object = artist_val.as_object().ok_or(anyhow!("Not an object"))?;
let save_fn = artist_object
.get(js_string!("save"), self.0)
.map_err(|e| anyhow!("JS error while accessing save: {}", e))?
.as_function()
.ok_or(anyhow!("save is not a function"))?;
let ids_val = utils::vec_string_to_js_array(ids, self.0)?;
let args = [ids_val.into()];
utils::js_call_to_void(save_fn.call(&artist_val, &args, self.0), self.0).await
pub async fn save(&self, ids: Vec<String>) -> anyhow::Result<()> {
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<_, ()>(
ctx.clone(),
"artist",
"save",
&[Value::Array(ids.into_iter().map(|id| Value::String(id)).collect())]
)
.await?
.expect("[hey][smartypants] artist.save should return a SpotifyPaginationResponseObject")
)
}).await
}
pub async fn unsave(&mut self, ids: Vec<String>) -> anyhow::Result<()> {
let artist_val = self.artist_obj()?;
let artist_object = artist_val.as_object().ok_or(anyhow!("Not an object"))?;
let unsave_fn = artist_object
.get(js_string!("unsave"), self.0)
.map_err(|e| anyhow!("JS error while accessing save: {}", e))?
.as_function()
.ok_or(anyhow!("save is not a function"))?;
let ids_val = utils::vec_string_to_js_array(ids, self.0)?;
let args = [ids_val.into()];
utils::js_call_to_void(unsave_fn.call(&artist_val, &args, self.0), self.0).await
pub async fn unsave(&self, ids: Vec<String>) -> anyhow::Result<()> {
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<_, ()>(
ctx.clone(),
"artist",
"unsave",
&[Value::Array(ids.into_iter().map(|id| Value::String(id)).collect())]
)
.await?
.expect("[hey][smartypants] artist.unsave should return a SpotifyPaginationResponseObject")
)
}).await
}
}

View File

@ -2,80 +2,51 @@ use crate::api::plugin::models::audio_source::{
SpotubeAudioSourceMatchObject, SpotubeAudioSourceStreamObject,
};
use crate::api::plugin::models::track::SpotubeTrackObject;
use crate::internal::utils;
use anyhow::anyhow;
use boa_engine::{js_string, Context, JsValue};
use crate::internal::utils::js_invoke_async_method_to_json;
use flutter_rust_bridge::frb;
use rquickjs::{async_with, AsyncContext};
#[derive(Debug)]
pub struct PluginAudioSourceEndpoint<'a>(&'a mut Context);
pub struct PluginAudioSourceEndpoint<'a>(&'a AsyncContext);
impl<'a> PluginAudioSourceEndpoint<'a> {
#[frb(ignore)]
pub fn new(context: &'a mut Context) -> PluginAudioSourceEndpoint<'a> {
pub fn new(context: &'a AsyncContext) -> PluginAudioSourceEndpoint<'a> {
PluginAudioSourceEndpoint(context)
}
fn audio_source_obj(&mut self) -> anyhow::Result<JsValue> {
let global = self.0.global_object();
let plugin_instance = global
.get(js_string!("pluginInstance"), self.0)
.map_err(|e| anyhow!("{}", e))
.and_then(|a| a.as_object().ok_or(anyhow!("Not an object")))?;
plugin_instance
.get(js_string!("audioSource"), self.0)
.or_else(|e| Err(anyhow!("artist not found: \n{}", e)))
}
pub async fn matches(
&mut self,
&self,
track: SpotubeTrackObject,
) -> anyhow::Result<Vec<SpotubeAudioSourceMatchObject>> {
let audio_source_val = self.audio_source_obj()?;
let audio_source_object = audio_source_val
.as_object()
.ok_or(anyhow!("Not an object"))?;
let matches_fn = audio_source_object
.get(js_string!("matches"), self.0)
.map_err(|e| anyhow!("JS error while accessing matches: {}", e))?
.as_function()
.ok_or(anyhow!("matches is not a function"))?;
let value = serde_json::to_value(track)?;
let track_val = utils::json_value_to_js(&value, self.0).map_err(|e| anyhow!("{}", e))?;
let args = [track_val];
let res =
utils::js_call_to_json(matches_fn.call(&audio_source_val, &args, self.0), self.0).await?;
serde_json::from_value(res).map_err(|e| anyhow!("{}", e))
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json(
ctx.clone(),
"audioSource",
"matches",
&[track]
)
.await?
.expect("[hey][smartypants] album.tracks should return a SpotifyPaginationResponseObject")
)
}).await
}
pub async fn streams(
&mut self,
&self,
matched: SpotubeAudioSourceMatchObject,
) -> anyhow::Result<Vec<SpotubeAudioSourceStreamObject>> {
let audio_source_val = self.audio_source_obj()?;
let audio_source_object = audio_source_val
.as_object()
.ok_or(anyhow!("Not an object"))?;
let matches_fn = audio_source_object
.get(js_string!("streams"), self.0)
.map_err(|e| anyhow!("JS error while accessing matches: {}", e))?
.as_function()
.ok_or(anyhow!("matches is not a function"))?;
let value = serde_json::to_value(matched)?;
let matched_val = utils::json_value_to_js(&value, self.0).map_err(|e| anyhow!("{}", e))?;
let args = [matched_val];
let res =
utils::js_call_to_json(matches_fn.call(&audio_source_val, &args, self.0), self.0).await?;
serde_json::from_value(res).map_err(|e| anyhow!("{}", e))
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json(
ctx.clone(),
"audioSource",
"streams",
&[matched]
)
.await?
.expect("[hey][smartypants] audioSource.streams should return a SpotifyPaginationResponseObject")
)
}).await
}
}

View File

@ -1,74 +1,60 @@
use crate::internal::utils;
use anyhow::anyhow;
use boa_engine::{js_string, Context, JsValue};
use crate::internal::utils::js_invoke_async_method_to_json;
use flutter_rust_bridge::frb;
use rquickjs::{async_with, AsyncContext};
#[derive(Debug)]
pub struct PluginAuthEndpoint<'a>(&'a mut Context);
pub struct PluginAuthEndpoint<'a>(&'a AsyncContext);
impl<'a> PluginAuthEndpoint<'a> {
#[frb(ignore)]
pub fn new(context: &'a mut Context) -> PluginAuthEndpoint<'a> {
pub fn new(context: &'a AsyncContext) -> PluginAuthEndpoint<'a> {
PluginAuthEndpoint(context)
}
fn auth_obj(&mut self) -> anyhow::Result<JsValue> {
let global = self.0.global_object();
let plugin_instance = global
.get(js_string!("pluginInstance"), self.0)
.map_err(|e| anyhow!("{}", e))
.and_then(|a| a.as_object().ok_or(anyhow!("Not an object")))?;
return plugin_instance
.get(js_string!("auth"), self.0)
.or_else(|e| Err(anyhow!("auth not found:\n{}", e)));
pub async fn authenticate(&self) -> anyhow::Result<()> {
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<(), ()>(
ctx.clone(),
"auth",
"authenticate",
&[]
)
.await?
.expect("[hey][smartypants] auth.authenticate should return a void")
)
})
.await
}
pub async fn authenticate(&mut self) -> anyhow::Result<()> {
let auth_val = self.auth_obj()?;
let auth_object = auth_val.as_object().ok_or(anyhow!("Not an object"))?;
let authenticate_fn = auth_object
.get(js_string!("authenticate"), self.0)
.map_err(|e| anyhow!("JS error while accessing authenticate: {}", e))?
.as_function()
.ok_or(anyhow!("authenticate is not a function"))?;
let args = [];
utils::js_call_to_void(authenticate_fn.call(&auth_val, &args, self.0), self.0).await
pub async fn is_authenticated(&self) -> anyhow::Result<bool> {
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<(), bool>(
ctx.clone(),
"auth",
"is_authenticated",
&[]
)
.await?
.expect("[hey][smartypants] auth.is_authenticated should return a boolean")
)
})
.await
}
pub fn is_authenticated(&mut self) -> anyhow::Result<bool> {
let auth_val = self.auth_obj()?;
let auth_object = auth_val.as_object().ok_or(anyhow!("Not an object"))?;
let authenticate_fn = auth_object
.get(js_string!("is_authenticated"), self.0)
.map_err(|e| anyhow!("JS error while accessing authenticate: {}", e))?
.as_function()
.ok_or(anyhow!("authenticate is not a function"))?;
authenticate_fn
.call(&auth_val, &[], self.0)
.map_err(|e| anyhow!("{}", e))?
.as_boolean()
.ok_or(anyhow!("Not a boolean"))
}
pub async fn logout(&mut self) -> anyhow::Result<()> {
let auth_val = self.auth_obj()?;
let auth_object = auth_val.as_object().ok_or(anyhow!("Not an object"))?;
let logout_fn = auth_object
.get(js_string!("logout"), self.0)
.map_err(|e| anyhow!("JS error while accessing authenticate: {}", e))?
.as_function()
.ok_or(anyhow!("authenticate is not a function"))?;
let args = [];
utils::js_call_to_void(logout_fn.call(&auth_val, &args, self.0), self.0).await
pub async fn logout(&self) -> anyhow::Result<()> {
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<(), ()>(
ctx.clone(),
"auth",
"logout",
&[]
)
.await?
.expect("[hey][smartypants] auth.logout should return a void")
)
})
.await
}
}

View File

@ -1,91 +1,56 @@
use crate::internal::utils;
use anyhow::anyhow;
use boa_engine::{js_string, Context, JsValue};
use flutter_rust_bridge::frb;
use crate::api::plugin::models::pagination::SpotubePaginationResponseObject;
use crate::internal::utils::js_invoke_async_method_to_json;
use flutter_rust_bridge::frb;
use rquickjs::{async_with, AsyncContext};
#[derive(Debug)]
pub struct PluginBrowseEndpoint<'a>(&'a mut Context);
pub struct PluginBrowseEndpoint<'a>(&'a AsyncContext);
impl<'a> PluginBrowseEndpoint<'a> {
#[frb(ignore)]
pub fn new(context: &'a mut Context) -> PluginBrowseEndpoint<'a> {
pub fn new(context: &'a AsyncContext) -> PluginBrowseEndpoint<'a> {
PluginBrowseEndpoint(context)
}
fn browse_obj(&mut self) -> anyhow::Result<JsValue> {
let global = self.0.global_object();
let plugin_instance = global
.get(js_string!("pluginInstance"), self.0)
.map_err(|e| anyhow!("{}", e))
.and_then(|a| a.as_object().ok_or(anyhow!("Not an object")))?;
plugin_instance
.get(js_string!("browse"), self.0)
.or_else(|e| Err(anyhow!("browse not found:\n{}", e)))
}
pub async fn sections(
&mut self,
&self,
offset: Option<u32>,
limit: Option<u32>,
) -> anyhow::Result<SpotubePaginationResponseObject> {
let browse_val = self.browse_obj()?;
let browse_object = browse_val.as_object().ok_or(anyhow!("Not an object"))?;
let sections_fn = browse_object
.get(js_string!("sections"), self.0)
.map_err(|e| anyhow!("JS error while accessing sections: {}", e))?
.as_function()
.ok_or(anyhow!("sections is not a function"))?;
let args = [
match offset {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
match limit {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
];
let res = utils::js_call_to_json(sections_fn.call(&browse_val, &args, self.0), self.0).await?;
serde_json::from_value(res).map_err(|e| anyhow!("{}", e))
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
ctx.clone(),
"browse",
"sections",
&[serde_json::to_value(offset)?, serde_json::to_value(limit)?]
)
.await?
.expect("[hey][smartypants] browse.sections should return a SpotifyPaginationResponseObject")
)
}).await
}
pub async fn section_items(
&mut self,
&self,
id: String,
offset: Option<u32>,
limit: Option<u32>,
) -> anyhow::Result<SpotubePaginationResponseObject> {
let browse_val = self.browse_obj()?;
let browse_object = browse_val.as_object().ok_or(anyhow!("Not an object"))?;
let section_items_fn = browse_object
.get(js_string!("sectionItems"), self.0)
.map_err(|e| anyhow!("JS error while accessing sectionItems: {}", e))?
.as_function()
.ok_or(anyhow!("sectionItems is not a function"))?;
let args = [
JsValue::from(js_string!(id)),
match offset {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
match limit {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
];
let res =
utils::js_call_to_json(section_items_fn.call(&browse_val, &args, self.0), self.0).await?;
serde_json::from_value(res).map_err(|e| anyhow!("{}", e))
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
ctx.clone(),
"browse",
"sectionItems",
&[
serde_json::to_value(id)?,
serde_json::to_value(offset)?,
serde_json::to_value(limit)?,
]
)
.await?
.expect("[hey][smartypants] browse.sectionItems should return a SpotifyPaginationResponseObject")
)
}).await
}
}

View File

@ -1,85 +1,49 @@
use crate::api::plugin::models::core::{PluginConfiguration, PluginUpdateAvailable, ScrobbleDetails};
use crate::internal::utils;
use crate::api::plugin::models::core::{
PluginConfiguration, PluginUpdateAvailable, ScrobbleDetails,
};
use crate::internal::utils::{js_invoke_async_method_to_json, js_invoke_method_to_json};
use anyhow::anyhow;
use boa_engine::{js_string, Context, JsValue};
use flutter_rust_bridge::frb;
use rquickjs::{async_with, AsyncContext};
#[derive(Debug)]
pub struct PluginCoreEndpoint<'a>(&'a mut Context);
pub struct PluginCoreEndpoint<'a>(&'a AsyncContext);
impl<'a> PluginCoreEndpoint<'a> {
#[frb(ignore)]
pub fn new(context: &'a mut Context) -> PluginCoreEndpoint<'a> {
pub fn new(context: &'a AsyncContext) -> PluginCoreEndpoint<'a> {
PluginCoreEndpoint(context)
}
fn core_obj(&mut self) -> anyhow::Result<JsValue> {
let global = self.0.global_object();
let plugin_instance = global
.get(js_string!("pluginInstance"), self.0)
.map_err(|e| anyhow!("{}", e))
.and_then(|a| a.as_object().ok_or(anyhow!("Not an object")))?;
plugin_instance
.get(js_string!("core"), self.0)
.or_else(|e| Err(anyhow!("core not found:\n{}", e)))
}
pub async fn check_update(
&mut self,
&self,
plugin_config: PluginConfiguration,
) -> anyhow::Result<Option<PluginUpdateAvailable>> {
let core_val = self.core_obj()?;
let core_object = core_val.as_object().ok_or(anyhow!("Not an object"))?;
let check_update_fn = core_object
.get(js_string!("checkUpdate"), self.0)
.map_err(|e| anyhow!("JS error while accessing checkUpdate: {}", e))?
.as_function()
.ok_or(anyhow!("checkUpdate is not a function"))?;
let value = serde_json::to_value(plugin_config)?;
let config_val = utils::json_value_to_js(&value, self.0).map_err(|e| anyhow!("{}", e))?;
let args = [config_val];
let res = utils::js_call_to_json(check_update_fn.call(&core_val, &args, self.0), self.0).await?;
if res.is_null() {
Ok(None)
} else {
serde_json::from_value(res).map_err(|e| anyhow!("{}", e))
}
async_with!(self.0 => |ctx| {
js_invoke_async_method_to_json(ctx.clone(), "core", "checkUpdate", &[plugin_config]).await
}).await
}
pub fn support(&mut self) -> anyhow::Result<String> {
let core_val = self.core_obj()?;
let core_object = core_val.as_object().ok_or(anyhow!("Not an object"))?;
let support_val = core_object
.get(js_string!("support"), self.0)
.map_err(|e| anyhow!("JS error while accessing support: {}", e))?;
support_val
.as_string()
.ok_or(anyhow!("support is not a string"))?
.to_std_string()
.map_err(|e| anyhow!("{}", e))
pub async fn support(&self) -> anyhow::Result<String> {
self.0
.with(|ctx| {
anyhow::Ok(
js_invoke_method_to_json::<String, String>(
ctx.clone(),
"core",
"support",
&[],
)?
.expect("[hey][smartypants] core.support should return a string"),
)
})
.await
}
pub async fn scrobble(&mut self, details: ScrobbleDetails) -> anyhow::Result<()> {
let core_val = self.core_obj()?;
let core_object = core_val.as_object().ok_or(anyhow!("Not an object"))?;
let scrobble_fn = core_object
.get(js_string!("scrobble"), self.0)
.map_err(|e| anyhow!("JS error while accessing scrobble: {}", e))?
.as_function()
.ok_or(anyhow!("scrobble is not a function"))?;
let value = serde_json::to_value(details)?;
let details_val = utils::json_value_to_js(&value, self.0).map_err(|e| anyhow!("{}", e))?;
let args = [details_val];
utils::js_call_to_void(scrobble_fn.call(&core_val, &args, self.0), self.0).await
pub async fn scrobble(&self, details: ScrobbleDetails) -> anyhow::Result<()> {
async_with!(self.0 => |ctx| {
js_invoke_async_method_to_json::<_, ()>(ctx.clone(), "core", "scrobble", &[details]).await?;
anyhow::Ok(())
})
.await
}
}

View File

@ -1,134 +1,82 @@
use crate::api::plugin::models::pagination::SpotubePaginationResponseObject;
use crate::api::plugin::models::playlist::SpotubeFullPlaylistObject;
use crate::internal::utils;
use anyhow::anyhow;
use boa_engine::{js_string, Context, JsValue};
use crate::internal::utils::js_invoke_async_method_to_json;
use flutter_rust_bridge::frb;
use rquickjs::{async_with, AsyncContext};
use serde_json::Value;
#[derive(Debug)]
pub struct PluginPlaylistEndpoint<'a>(&'a mut Context);
pub struct PluginPlaylistEndpoint<'a>(&'a AsyncContext);
impl<'a> PluginPlaylistEndpoint<'a> {
#[frb(ignore)]
pub fn new(context: &'a mut Context) -> PluginPlaylistEndpoint<'a> {
pub fn new(context: &'a AsyncContext) -> PluginPlaylistEndpoint<'a> {
PluginPlaylistEndpoint(context)
}
fn playlist_obj(&mut self) -> anyhow::Result<JsValue> {
let global = self.0.global_object();
let plugin_instance = global
.get(js_string!("pluginInstance"), self.0)
.map_err(|e| anyhow!("{}", e))
.and_then(|a| a.as_object().ok_or(anyhow!("Not an object")))?;
plugin_instance
.get(js_string!("playlist"), self.0)
.or_else(|e| Err(anyhow!("playlist not found: \n{}", e)))
}
pub async fn get_playlist(&mut self, id: String) -> anyhow::Result<SpotubeFullPlaylistObject> {
let playlist_val = self.playlist_obj()?;
let playlist_object = playlist_val.as_object().ok_or(anyhow!("Not an object"))?;
let get_playlist_fn = playlist_object
.get(js_string!("getPlaylist"), self.0)
.map_err(|e| anyhow!("{}", e))?
.as_function()
.ok_or(anyhow!("getPlaylist is not a function"))?;
let args = [JsValue::from(js_string!(id))];
let res_json =
utils::js_call_to_json(get_playlist_fn.call(&playlist_val, &args, self.0), self.0)
.await?;
serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
pub async fn get_playlist(&self, id: String) -> anyhow::Result<SpotubeFullPlaylistObject> {
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json(
ctx.clone(),
"playlist",
"getPlaylist",
&[id]
)
.await?
.expect("[hey][smartypants] playlist.getPlaylist should return a SpotifyFullPlaylistObject")
)
}).await
}
pub async fn tracks(
&mut self,
&self,
id: String,
offset: Option<u32>,
limit: Option<u32>,
) -> anyhow::Result<SpotubePaginationResponseObject> {
let playlist_val = self.playlist_obj()?;
let playlist_object = playlist_val.as_object().ok_or(anyhow!("Not an object"))?;
let tracks_fn = playlist_object
.get(js_string!("tracks"), self.0)
.map_err(|e| anyhow!("{}", e))?
.as_function()
.ok_or(anyhow!("tracks is not a function"))?;
let args: [JsValue; 3] = [
JsValue::from(js_string!(id)),
match offset {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
match limit {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
];
let res_json =
utils::js_call_to_json(tracks_fn.call(&playlist_val, &args, self.0), self.0).await?;
serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
ctx.clone(),
"playlist",
"tracks",
&[Value::String(id), serde_json::to_value(offset)?, serde_json::to_value(limit.unwrap())?]
)
.await?
.expect("[hey][smartypants] artist.related should return a SpotifyPaginationResponseObject")
)
}).await
}
pub async fn create(
&mut self,
&self,
user_id: String,
name: String,
description: Option<String>,
public: Option<bool>,
collaborative: Option<bool>,
) -> anyhow::Result<Option<SpotubeFullPlaylistObject>> {
let playlist_val = self.playlist_obj()?;
let playlist_object = playlist_val.as_object().ok_or(anyhow!("Not an object"))?;
let create_fn = playlist_object
.get(js_string!("create"), self.0)
.map_err(|e| anyhow!("{}", e))?
.as_function()
.ok_or(anyhow!("create is not a function"))?;
let args = [
JsValue::from(js_string!(user_id)),
JsValue::from(js_string!(name)),
match description {
Some(o) => JsValue::from(js_string!(o)),
None => JsValue::undefined(),
},
match public {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
match collaborative {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
];
let res_json =
utils::js_call_to_json(create_fn.call(&playlist_val, &args, self.0), self.0).await?;
if res_json.is_null() {
Ok(None)
} else {
serde_json::from_value(res_json)
.map(Some)
.map_err(|e| anyhow!("{}", e))
}
async_with!(self.0 => |ctx| {
js_invoke_async_method_to_json(
ctx.clone(),
"playlist",
"create",
&[
Value::String(user_id),
Value::String(name),
Value::String(description.unwrap_or_default()),
serde_json::to_value(public.unwrap_or_default())?,
serde_json::to_value(collaborative.unwrap_or_default())?,
]
)
.await
})
.await
}
pub async fn update(
&mut self,
&self,
playlist_id: String,
name: Option<String>,
@ -136,135 +84,104 @@ impl<'a> PluginPlaylistEndpoint<'a> {
public: Option<bool>,
collaborative: Option<bool>,
) -> anyhow::Result<()> {
let playlist_val = self.playlist_obj()?;
let playlist_object = playlist_val.as_object().ok_or(anyhow!("Not an object"))?;
let update_fn = playlist_object
.get(js_string!("update"), self.0)
.map_err(|e| anyhow!("{}", e))?
.as_function()
.ok_or(anyhow!("update is not a function"))?;
let args = [
JsValue::from(js_string!(playlist_id)),
match name {
Some(o) => JsValue::from(js_string!(o)),
None => JsValue::undefined(),
},
match description {
Some(o) => JsValue::from(js_string!(o)),
None => JsValue::undefined(),
},
match public {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
match collaborative {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
];
utils::js_call_to_void(update_fn.call(&playlist_val, &args, self.0), self.0).await
async_with!(self.0 => |ctx| {
js_invoke_async_method_to_json::<_, ()>(
ctx.clone(),
"playlist",
"update",
&[
Value::String(playlist_id),
serde_json::to_value(name)?,
Value::String(description.unwrap_or_default()),
serde_json::to_value(public.unwrap_or_default())?,
serde_json::to_value(collaborative.unwrap_or_default())?,
]
)
.await.and_then(|_| Ok(()))
})
.await
}
pub async fn add_tracks(
&mut self,
&self,
playlist_id: String,
track_ids: Vec<String>,
position: Option<u32>,
) -> anyhow::Result<()> {
let playlist_val = self.playlist_obj()?;
let playlist_object = playlist_val.as_object().ok_or(anyhow!("Not an object"))?;
let add_tracks_fn = playlist_object
.get(js_string!("addTracks"), self.0)
.map_err(|e| anyhow!("{}", e))?
.as_function()
.ok_or(anyhow!("addTracks is not a function"))?;
let args = [
JsValue::from(js_string!(playlist_id)),
utils::vec_string_to_js_array(track_ids, self.0)?,
match position {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
];
utils::js_call_to_void(add_tracks_fn.call(&playlist_val, &args, self.0), self.0).await
async_with!(self.0 => |ctx| {
js_invoke_async_method_to_json::<_, ()>(
ctx.clone(),
"playlist",
"addTracks",
&[
Value::String(playlist_id),
Value::Array(track_ids.into_iter().map(|id| Value::String(id)).collect()),
serde_json::to_value(position)?,
]
)
.await.and_then(|_| Ok(()))
})
.await
}
pub async fn remove_tracks(
&mut self,
&self,
playlist_id: String,
track_ids: Vec<String>,
) -> anyhow::Result<()> {
let playlist_val = self.playlist_obj()?;
let playlist_object = playlist_val.as_object().ok_or(anyhow!("Not an object"))?;
let remove_tracks_fn = playlist_object
.get(js_string!("removeTracks"), self.0)
.map_err(|e| anyhow!("{}", e))?
.as_function()
.ok_or(anyhow!("removeTracks is not a function"))?;
let args = [
JsValue::from(js_string!(playlist_id)),
utils::vec_string_to_js_array(track_ids, self.0)?,
];
utils::js_call_to_void(remove_tracks_fn.call(&playlist_val, &args, self.0), self.0).await
}
pub async fn save(&mut self, playlist_id: String) -> anyhow::Result<()> {
let playlist_val = self.playlist_obj()?;
let playlist_object = playlist_val.as_object().ok_or(anyhow!("Not an object"))?;
let save_fn = playlist_object
.get(js_string!("save"), self.0)
.map_err(|e| anyhow!("{}", e))?
.as_function()
.ok_or(anyhow!("save is not a function"))?;
let args = [JsValue::from(js_string!(playlist_id))];
utils::js_call_to_void(save_fn.call(&playlist_val, &args, self.0), self.0).await
}
pub async fn unsave(&mut self, playlist_id: String) -> anyhow::Result<()> {
let playlist_val = self.playlist_obj()?;
let playlist_object = playlist_val.as_object().ok_or(anyhow!("Not an object"))?;
let unsave_fn = playlist_object
.get(js_string!("unsave"), self.0)
.map_err(|e| anyhow!("{}", e))?
.as_function()
.ok_or(anyhow!("unsave is not a function"))?;
let args = [JsValue::from(js_string!(playlist_id))];
utils::js_call_to_void(unsave_fn.call(&playlist_val, &args, self.0), self.0).await
}
pub async fn delete_playlist(&mut self, playlist_id: String) -> anyhow::Result<()> {
let playlist_val = self.playlist_obj()?;
let playlist_object = playlist_val.as_object().ok_or(anyhow!("Not an object"))?;
let delete_playlist_fn = playlist_object
.get(js_string!("deletePlaylist"), self.0)
.map_err(|e| anyhow!("{}", e))?
.as_function()
.ok_or(anyhow!("deletePlaylist is not a function"))?;
let args = [JsValue::from(js_string!(playlist_id))];
utils::js_call_to_void(
delete_playlist_fn.call(&playlist_val, &args, self.0),
self.0,
async_with!(self.0 => |ctx| {
js_invoke_async_method_to_json::<_, ()>(
ctx.clone(),
"playlist",
"removeTracks",
&[
Value::String(playlist_id),
Value::Array(track_ids.into_iter().map(|id| Value::String(id)).collect()),
]
)
.await.and_then(|_| Ok(()))
})
.await
}
pub async fn save(&self, playlist_id: String) -> anyhow::Result<()> {
async_with!(self.0 => |ctx| {
js_invoke_async_method_to_json::<_, ()>(
ctx.clone(),
"playlist",
"save",
&[Value::String(playlist_id)]
)
.await.and_then(|_| Ok(()))
})
.await
}
pub async fn unsave(&self, playlist_id: String) -> anyhow::Result<()> {
async_with!(self.0 => |ctx| {
js_invoke_async_method_to_json::<_, ()>(
ctx.clone(),
"playlist",
"unsave",
&[Value::String(playlist_id)]
)
.await.and_then(|_| Ok(()))
})
.await
}
pub async fn delete_playlist(&self, playlist_id: String) -> anyhow::Result<()> {
async_with!(self.0 => |ctx| {
js_invoke_async_method_to_json::<_, ()>(
ctx.clone(),
"playlist",
"deletePlaylist",
&[Value::String(playlist_id)]
)
.await.and_then(|_| Ok(()))
})
.await
}
}

View File

@ -1,201 +1,149 @@
use crate::api::plugin::models::pagination::SpotubePaginationResponseObject;
use crate::api::plugin::models::search::SpotubeSearchResponseObject;
use crate::internal::utils;
use anyhow::anyhow;
use boa_engine::object::builtins::JsArray;
use boa_engine::{js_string, Context, JsValue};
use crate::internal::utils::{js_invoke_async_method_to_json, js_invoke_method_to_json};
use flutter_rust_bridge::frb;
use rquickjs::{async_with, AsyncContext};
use serde_json::Value;
#[derive(Debug)]
pub struct PluginSearchEndpoint<'a>(&'a mut Context);
pub struct PluginSearchEndpoint<'a>(&'a AsyncContext);
impl<'a> PluginSearchEndpoint<'a> {
#[frb(ignore)]
pub fn new(context: &'a mut Context) -> PluginSearchEndpoint<'a> {
pub fn new(context: &'a AsyncContext) -> PluginSearchEndpoint<'a> {
PluginSearchEndpoint(context)
}
fn search_obj(&mut self) -> anyhow::Result<JsValue> {
let global = self.0.global_object();
let plugin_instance = global
.get(js_string!("pluginInstance"), self.0)
.map_err(|e| anyhow!("{}", e))
.and_then(|a| a.as_object().ok_or(anyhow!("Not an object")))?;
plugin_instance
.get(js_string!("search"), self.0)
.or_else(|e| Err(anyhow!("search not found: \n{}", e)))
pub async fn chips(&self) -> anyhow::Result<Vec<String>> {
self.0
.with(|ctx| {
anyhow::Ok(
js_invoke_method_to_json::<(), Vec<String>>(
ctx.clone(),
"search",
"chips",
&[],
)?
.expect("[hey][smartypants] search.chips should return a string"),
)
})
.await
}
pub fn chips(&mut self) -> anyhow::Result<Vec<String>> {
let search_val = self.search_obj()?;
let search_object = search_val.as_object().ok_or(anyhow!("Not an object"))?;
let chips_val = search_object
.get(js_string!("chips"), self.0)
.map_err(|e| anyhow!("{}", e))?;
let chips_obj = chips_val.as_object().ok_or(anyhow!("Not an object"))?;
if !chips_obj.is_array() {
return Err(anyhow!("chips is not an array"));
}
let chips_array = JsArray::from_object(chips_obj.clone()).map_err(|e| anyhow!("{}", e))?;
let length = chips_array.length(self.0).map_err(|e| anyhow!("{}", e))?;
let mut result = Vec::new();
for i in 0..length {
let item = chips_array.get(i, self.0).map_err(|e| anyhow!("{}", e))?;
if let Some(s) = item.as_string() {
result.push(s.to_std_string().map_err(|e| anyhow!("{}", e))?);
}
}
Ok(result)
}
pub async fn all(&mut self, query: String) -> anyhow::Result<SpotubeSearchResponseObject> {
let search_val = self.search_obj()?;
let search_object = search_val.as_object().ok_or(anyhow!("Not an object"))?;
let all_fn = search_object
.get(js_string!("all"), self.0)
.map_err(|e| anyhow!("{}", e))?
.as_function()
.ok_or(anyhow!("all is not a function"))?;
let args = [JsValue::from(js_string!(query))];
let res_json = utils::js_call_to_json(all_fn.call(&search_val, &args, self.0), self.0).await?;
serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
pub async fn all(&self, query: String) -> anyhow::Result<SpotubeSearchResponseObject> {
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<_, SpotubeSearchResponseObject>(
ctx.clone(),
"search",
"all",
&[query],
)
.await?
.expect("[hey][smartypants] search.all should return a SpotifySearchResponseObject")
)
})
.await
}
pub async fn albums(
&mut self,
&self,
query: String,
offset: Option<u32>,
limit: Option<u32>,
) -> anyhow::Result<SpotubePaginationResponseObject> {
let search_val = self.search_obj()?;
let search_object = search_val.as_object().ok_or(anyhow!("Not an object"))?;
let albums_fn = search_object
.get(js_string!("albums"), self.0)
.map_err(|e| anyhow!("{}", e))?
.as_function()
.ok_or(anyhow!("albums is not a function"))?;
let args: [JsValue; 3] = [
JsValue::from(js_string!(query)),
match offset {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
match limit {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
];
let res_json = utils::js_call_to_json(albums_fn.call(&search_val, &args, self.0), self.0).await?;
serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
ctx.clone(),
"search",
"albums",
&[
Value::String(query),
serde_json::to_value(offset)?,
serde_json::to_value(limit)?
],
)
.await?
.expect("[hey][smartypants] search.albums should return a SpotifyPaginationResponseObject")
)
})
.await
}
pub async fn artists(
&mut self,
&self,
query: String,
offset: Option<u32>,
limit: Option<u32>,
) -> anyhow::Result<SpotubePaginationResponseObject> {
let search_val = self.search_obj()?;
let search_object = search_val.as_object().ok_or(anyhow!("Not an object"))?;
let artists_fn = search_object
.get(js_string!("artists"), self.0)
.map_err(|e| anyhow!("{}", e))?
.as_function()
.ok_or(anyhow!("artists is not a function"))?;
let args: [JsValue; 3] = [
JsValue::from(js_string!(query)),
match offset {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
match limit {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
];
let res_json = utils::js_call_to_json(artists_fn.call(&search_val, &args, self.0), self.0).await?;
serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
ctx.clone(),
"search",
"artists",
&[
Value::String(query),
serde_json::to_value(offset)?,
serde_json::to_value(limit)?
],
)
.await?
.expect("[hey][smartypants] search.artists should return a SpotifyPaginationResponseObject")
)
})
.await
}
pub async fn playlists(
&mut self,
&self,
query: String,
offset: Option<u32>,
limit: Option<u32>,
) -> anyhow::Result<SpotubePaginationResponseObject> {
let search_val = self.search_obj()?;
let search_object = search_val.as_object().ok_or(anyhow!("Not an object"))?;
let playlists_fn = search_object
.get(js_string!("playlists"), self.0)
.map_err(|e| anyhow!("{}", e))?
.as_function()
.ok_or(anyhow!("playlists is not a function"))?;
let args: [JsValue; 3] = [
JsValue::from(js_string!(query)),
match offset {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
match limit {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
];
let res_json =
utils::js_call_to_json(playlists_fn.call(&search_val, &args, self.0), self.0).await?;
serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
ctx.clone(),
"search",
"playlists",
&[
Value::String(query),
serde_json::to_value(offset)?,
serde_json::to_value(limit)?
],
)
.await?
.expect("[hey][smartypants] search.playlists should return a SpotifyPaginationResponseObject")
)
})
.await
}
pub async fn tracks(
&mut self,
&self,
query: String,
offset: Option<u32>,
limit: Option<u32>,
) -> anyhow::Result<SpotubePaginationResponseObject> {
let search_val = self.search_obj()?;
let search_object = search_val.as_object().ok_or(anyhow!("Not an object"))?;
let tracks_fn = search_object
.get(js_string!("tracks"), self.0)
.map_err(|e| anyhow!("{}", e))?
.as_function()
.ok_or(anyhow!("tracks is not a function"))?;
let args: [JsValue; 3] = [
JsValue::from(js_string!(query)),
match offset {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
match limit {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
];
let res_json = utils::js_call_to_json(tracks_fn.call(&search_val, &args, self.0), self.0).await?;
serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
ctx.clone(),
"search",
"tracks",
&[
Value::String(query),
serde_json::to_value(offset)?,
serde_json::to_value(limit)?
],
)
.await?
.expect("[hey][smartypants] search.tracks should return a SpotifyPaginationResponseObject")
)
})
.await
}
}

View File

@ -1,96 +1,77 @@
use crate::api::plugin::models::track::SpotubeTrackObject;
use crate::internal::utils;
use anyhow::anyhow;
use boa_engine::{js_string, Context, JsValue};
use crate::internal::utils::js_invoke_async_method_to_json;
use flutter_rust_bridge::frb;
use rquickjs::{async_with, AsyncContext};
use serde_json::Value;
#[derive(Debug)]
pub struct PluginTrackEndpoint<'a>(&'a mut Context);
pub struct PluginTrackEndpoint<'a>(&'a AsyncContext);
impl<'a> PluginTrackEndpoint<'a> {
#[frb(ignore)]
pub fn new(context: &'a mut Context) -> PluginTrackEndpoint<'a> {
pub fn new(context: &'a AsyncContext) -> PluginTrackEndpoint<'a> {
PluginTrackEndpoint(context)
}
fn track_obj(&mut self) -> anyhow::Result<JsValue> {
let global = self.0.global_object();
let plugin_instance = global
.get(js_string!("pluginInstance"), self.0)
.map_err(|e| anyhow!("{}", e))
.and_then(|a| a.as_object().ok_or(anyhow!("Not an object")))?;
plugin_instance
.get(js_string!("track"), self.0)
.or_else(|e| Err(anyhow!("track not found: \n{}", e)))
pub async fn get_track(&self, id: String) -> anyhow::Result<SpotubeTrackObject> {
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json(
ctx.clone(),
"track",
"getTrack",
&[
id
],
)
.await?
.expect("[hey][smartypants] track.getTrack should return a SpotifyTrackObject")
)
})
.await
}
pub async fn get_track(&mut self, id: String) -> anyhow::Result<SpotubeTrackObject> {
let track_val = self.track_obj()?;
let track_object = track_val.as_object().ok_or(anyhow!("Not an object"))?;
let get_track_fn = track_object
.get(js_string!("getTrack"), self.0)
.map_err(|e| anyhow!("JS error while accessing getTrack: {}", e))?
.as_function()
.ok_or(anyhow!("getTrack is not a function"))?;
let args = [JsValue::from(js_string!(id))];
let res_json =
utils::js_call_to_json(get_track_fn.call(&track_val, &args, self.0), self.0).await?;
serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
pub async fn save(&self, ids: Vec<String>) -> anyhow::Result<()> {
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<_, ()>(
ctx.clone(),
"track",
"save",
&[Value::Array(ids.into_iter().map(|id| Value::String(id)).collect())]
)
.await?
.expect("[hey][smartypants] track.save should return a SpotifyPaginationResponseObject")
)
}).await
}
pub async fn save(&mut self, ids: Vec<String>) -> anyhow::Result<()> {
let track_val = self.track_obj()?;
let track_object = track_val.as_object().ok_or(anyhow!("Not an object"))?;
let save_fn = track_object
.get(js_string!("save"), self.0)
.map_err(|e| anyhow!("JS error while accessing save: {}", e))?
.as_function()
.ok_or(anyhow!("save is not a function"))?;
let ids_val = utils::vec_string_to_js_array(ids, self.0)?;
let args = [ids_val.into()];
utils::js_call_to_void(save_fn.call(&track_val, &args, self.0), self.0).await
pub async fn unsave(&self, ids: Vec<String>) -> anyhow::Result<()> {
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<_, ()>(
ctx.clone(),
"track",
"unsave",
&[Value::Array(ids.into_iter().map(|id| Value::String(id)).collect())]
)
.await?
.expect("[hey][smartypants] track.unsave should return a SpotifyPaginationResponseObject")
)
}).await
}
pub async fn unsave(&mut self, ids: Vec<String>) -> anyhow::Result<()> {
let track_val = self.track_obj()?;
let track_object = track_val.as_object().ok_or(anyhow!("Not an object"))?;
let unsave_fn = track_object
.get(js_string!("unsave"), self.0)
.map_err(|e| anyhow!("JS error while accessing save: {}", e))?
.as_function()
.ok_or(anyhow!("save is not a function"))?;
let ids_val = utils::vec_string_to_js_array(ids, self.0)?;
let args = [ids_val.into()];
utils::js_call_to_void(unsave_fn.call(&track_val, &args, self.0), self.0).await
}
pub async fn radio(&mut self, id: String) -> anyhow::Result<Vec<SpotubeTrackObject>> {
let track_val = self.track_obj()?;
let track_object = track_val.as_object().ok_or(anyhow!("Not an object"))?;
let get_track_fn = track_object
.get(js_string!("radio"), self.0)
.map_err(|e| anyhow!("JS error while accessing radio: {}", e))?
.as_function()
.ok_or(anyhow!("radio is not a function"))?;
let args = [JsValue::from(js_string!(id))];
let res_json =
utils::js_call_to_json(get_track_fn.call(&track_val, &args, self.0), self.0).await?;
serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
pub async fn radio(&self, id: String) -> anyhow::Result<Vec<SpotubeTrackObject>> {
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<_, Vec<SpotubeTrackObject>>(
ctx.clone(),
"track",
"radio",
&[id],
)
.await?
.expect("[hey][smartypants] track.radio should return a SpotifyPaginationResponseObject")
)
}).await
}
}

View File

@ -1,107 +1,106 @@
use crate::api::plugin::models::pagination::SpotubePaginationResponseObject;
use crate::internal::utils;
use anyhow::anyhow;
use boa_engine::{js_string, Context, JsValue};
use flutter_rust_bridge::frb;
use crate::api::plugin::models::user::SpotubeUserObject;
use crate::internal::utils::js_invoke_async_method_to_json;
use flutter_rust_bridge::frb;
use rquickjs::{async_with, AsyncContext};
#[derive(Debug)]
pub struct PluginUserEndpoint<'a>(&'a mut Context);
pub struct PluginUserEndpoint<'a>(&'a AsyncContext);
impl<'a> PluginUserEndpoint<'a> {
#[frb(ignore)]
pub fn new(context: &'a mut Context) -> PluginUserEndpoint<'a> {
pub fn new(context: &'a AsyncContext) -> PluginUserEndpoint<'a> {
PluginUserEndpoint(context)
}
fn user_obj(&mut self) -> anyhow::Result<JsValue> {
let global = self.0.global_object();
let plugin_instance = global
.get(js_string!("pluginInstance"), self.0)
.map_err(|e| anyhow!("{}", e))
.and_then(|a| a.as_object().ok_or(anyhow!("Not an object")))?;
plugin_instance
.get(js_string!("user"), self.0)
.or_else(|e| Err(anyhow!("user not found: \n{}", e)))
}
pub async fn me(&mut self) -> anyhow::Result<SpotubeUserObject> {
let user_val = self.user_obj()?;
let user_object = user_val.as_object().ok_or(anyhow!("Not an object"))?;
let me_fn = user_object
.get(js_string!("me"), self.0)
.map_err(|e| anyhow!("JS error while accessing me: {}", e))?
.as_function()
.ok_or(anyhow!("me is not a function"))?;
let res_json = utils::js_call_to_json(me_fn.call(&user_val, &[], self.0), self.0).await?;
serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
}
async fn get_saved(
&mut self,
method: &str,
offset: Option<u32>,
limit: Option<u32>,
) -> anyhow::Result<SpotubePaginationResponseObject> {
let user_val = self.user_obj()?;
let user_object = user_val.as_object().ok_or(anyhow!("Not an object"))?;
let saved_fn = user_object
.get(js_string!(method), self.0)
.map_err(|e| anyhow!("JS error while accessing {}: {}", method, e))?
.as_function()
.ok_or(anyhow!("{} is not a function", method))?;
let args: [JsValue; 2] = [
match offset {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
match limit {
Some(o) => JsValue::from(o),
None => JsValue::undefined(),
},
];
let res_json = utils::js_call_to_json(saved_fn.call(&user_val, &args, self.0), self.0).await?;
serde_json::from_value(res_json).map_err(|e| anyhow!("{}", e))
pub async fn me(&self) -> anyhow::Result<SpotubeUserObject> {
async_with!(self.0 => |ctx| {
Ok(
js_invoke_async_method_to_json::<(), SpotubeUserObject>(
ctx.clone(),
"user",
"me",
&[],
)
.await?
.expect("[hey][smartypants] user.me should return a SpotifyUserObject")
)
})
.await
}
pub async fn saved_playlists(
&mut self,
&self,
offset: Option<u32>,
limit: Option<u32>,
) -> anyhow::Result<SpotubePaginationResponseObject> {
self.get_saved("savedPlaylists", offset, limit).await
async_with!(self.0 => |ctx| {
let res= js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
ctx,
"user",
"savedPlaylists",
&[serde_json::to_value(offset)?, serde_json::to_value(limit.unwrap())?]
)
.await?
.expect("[hey][smartypants] user.savedPlaylists should return a SpotifyPaginationResponseObject");
Ok(res)
}).await
}
pub async fn saved_tracks(
&mut self,
&self,
offset: Option<u32>,
limit: Option<u32>,
) -> anyhow::Result<SpotubePaginationResponseObject> {
self.get_saved("savedTracks", offset, limit).await
async_with!(self.0 => |ctx| {
let res= js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
ctx,
"user",
"savedTracks",
&[serde_json::to_value(offset)?, serde_json::to_value(limit.unwrap())?]
)
.await?
.expect("[hey][smartypants] user.savedTracks should return a SpotifyPaginationResponseObject");
Ok(res)
}).await
}
pub async fn saved_albums(
&mut self,
&self,
offset: Option<u32>,
limit: Option<u32>,
) -> anyhow::Result<SpotubePaginationResponseObject> {
self.get_saved("savedAlbums", offset, limit).await
async_with!(self.0 => |ctx| {
let res= js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
ctx,
"user",
"savedAlbums",
&[serde_json::to_value(offset)?, serde_json::to_value(limit.unwrap())?]
)
.await?
.expect("[hey][smartypants] user.savedAlbums should return a SpotifyPaginationResponseObject");
Ok(res)
}).await
}
pub async fn saved_artists(
&mut self,
&self,
offset: Option<u32>,
limit: Option<u32>,
) -> anyhow::Result<SpotubePaginationResponseObject> {
self.get_saved("savedArtists", offset, limit).await
async_with!(self.0 => |ctx| {
let res= js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
ctx,
"user",
"savedArtists",
&[serde_json::to_value(offset)?, serde_json::to_value(limit.unwrap())?]
)
.await?
.expect("[hey][smartypants] user.savedArtists should return a SpotifyPaginationResponseObject");
Ok(res)
}).await
}
}

View File

@ -1,116 +1,57 @@
use anyhow::anyhow;
use boa_engine::property::PropertyKey;
use boa_engine::{object::builtins::JsArray, Context, JsObject, JsResult, JsString, JsValue};
use rquickjs::function::Args;
use rquickjs::{Array, CatchResultExt, Ctx, Filter, FromJs, Function, IntoJs, Object, Promise};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::{Map, Value};
pub fn vec_string_to_js_array(
rust_vec: Vec<String>,
context: &mut Context,
) -> anyhow::Result<JsValue> {
let builder = JsArray::new(context);
for (index, rust_string) in rust_vec.into_iter().enumerate() {
let js_string_value = JsString::from(rust_string);
builder
.set(index as u32, js_string_value, true, context)
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
}
Ok(builder.into())
}
#[allow(dead_code)]
pub async fn js_call_to_string(
result: JsResult<JsValue>,
context: &mut Context,
) -> anyhow::Result<String> {
let res = result
.map_err(|e| anyhow!("{}", e))
.and_then(|f| f.as_promise().ok_or(anyhow!("Not a promise")))?
.into_js_future(context)
.await
.map_err(|e| anyhow!("{}", e))?
.as_string()
.ok_or(anyhow!("No response string returned"))?
.to_std_string()
.map_err(|e| anyhow!("{}", e))?;
Ok(res)
}
pub async fn js_call_to_json(
result: JsResult<JsValue>,
context: &mut Context,
) -> anyhow::Result<Value> {
let res = result
.map_err(|e| anyhow!("{}", e))
.and_then(|f| f.as_promise().ok_or(anyhow!("Not a promise")))?
.into_js_future(context)
.await
.map_err(|e| anyhow!("{}", e))?;
let ls = js_value_to_json(&res, context)?;
Ok(ls)
}
pub async fn js_call_to_void(
result: JsResult<JsValue>,
context: &mut Context,
) -> anyhow::Result<()> {
result
.map_err(|e| anyhow!("{}", e))
.and_then(|f| f.as_promise().ok_or(anyhow!("Not a promise")))?
.into_js_future(context)
.await
.map_err(|e| anyhow!("{}", e))?;
Ok(())
}
use std::collections::HashMap;
/// Convert a `serde_json::Value` into a Boa `JsValue`
pub fn json_value_to_js(value: &Value, ctx: &mut Context) -> JsResult<JsValue> {
pub fn json_value_to_js<'a>(value: &Value, ctx: Ctx<'a>) -> anyhow::Result<rquickjs::Value<'a>> {
match value {
Value::Null => Ok(JsValue::null()),
Value::Bool(b) => Ok(JsValue::from(*b)),
Value::Null => Ok(rquickjs::Value::new_null(ctx)),
Value::Bool(b) => Ok(rquickjs::Value::new_bool(ctx, *b)),
Value::Number(n) => {
if let Some(i) = n.as_i64() {
Ok(JsValue::new(i))
Ok(rquickjs::Value::new_int(ctx, i as i32))
} else if let Some(f) = n.as_f64() {
Ok(JsValue::new(f))
Ok(rquickjs::Value::new_float(ctx, f))
} else {
Ok(JsValue::null()) // fallback (rare)
Ok(rquickjs::Value::new_null(ctx)) // fallback (rare)
}
}
Value::String(s) => Ok(JsValue::from(JsString::from(s.as_str()))),
Value::String(s) => {
let sts = rquickjs::String::from_str(ctx, s.as_str())?;
Ok(rquickjs::Value::from_string(sts))
}
Value::Array(arr) => {
let js_arr = JsArray::new(ctx);
for (idx, item) in arr.iter().enumerate() {
let js_val = json_value_to_js(item, ctx)?;
js_arr.set(idx, js_val, false, ctx)?;
let mut js_arr = Vec::<rquickjs::Value>::with_capacity(arr.len());
for item in arr.iter() {
let js_val = json_value_to_js(item, ctx.clone())?;
js_arr.push(js_val);
}
Ok(JsValue::from(js_arr))
js_arr.into_js(&ctx).map_err(|e| anyhow!(e))
}
Value::Object(obj) => {
let js_obj = JsObject::with_null_proto();
let mut js_obj = HashMap::<String, rquickjs::Value>::with_capacity(obj.len());
for (key, val) in obj {
let js_val = json_value_to_js(val, ctx)?;
js_obj.set(JsString::from(key.as_str()), js_val, true, ctx)?;
let js_val = json_value_to_js(val, ctx.clone())?;
js_obj.insert(key.clone(), js_val);
}
Ok(JsValue::from(js_obj))
js_obj.into_js(&ctx).map_err(|e| anyhow!(e))
}
}
}
/// Convert a Boa `JsValue` into a `serde_json::Value`
pub fn js_value_to_json(value: &JsValue, ctx: &mut Context) -> 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() {
return Ok(Value::Null);
}
if let Some(b) = value.as_boolean() {
if let Some(b) = value.as_bool() {
return Ok(Value::Bool(b));
}
@ -120,27 +61,22 @@ pub fn js_value_to_json(value: &JsValue, ctx: &mut Context) -> anyhow::Result<Va
}
if let Some(s) = value.as_string() {
let s = s.to_std_string().map_err(|e| anyhow!("{}", e))?;
let s = s.to_string()?;
return Ok(Value::String(s));
}
if value.is_bigint() {
// BigInts are NOT JSON-compatible → store as string
return Ok(Value::String(value.display().to_string()));
}
if value.is_object() {
let obj = value.as_object().ok_or(anyhow!("Not an object"))?;
let obj = value.into_object().ok_or(anyhow!("Not an object"))?;
// Array?
if obj.is_array() {
let obj = JsArray::from_object(obj).map_err(|e| anyhow!("{}", e))?;
let length = obj.length(ctx).map_err(|e| anyhow!("{}", e))?;
let mut json_arr = Vec::<Value>::with_capacity(length as usize);
let obj: Array = Array::from_value(obj.into_value()).map_err(|e| anyhow!("{}", e))?;
let length = obj.len();
let mut json_arr = Vec::<Value>::with_capacity(length);
for i in 0..length {
let item = obj.get(i, ctx).unwrap_or(JsValue::null());
let item_json = js_value_to_json(&item, ctx)?;
let item = obj.get(i).unwrap_or(rquickjs::Value::new_null(ctx.clone()));
let item_json = js_value_to_json(item, ctx.clone())?;
json_arr.push(item_json);
}
@ -150,19 +86,14 @@ pub fn js_value_to_json(value: &JsValue, ctx: &mut Context) -> anyhow::Result<Va
// Regular Object
let mut map = Map::<String, Value>::new();
for key in obj.own_property_keys(ctx).map_err(|e| anyhow!("{}", e))? {
let key_val: Option<String> = match key.clone() {
PropertyKey::String(s) => Some(s.to_std_string().map_err(|e| anyhow!("{}", e))?),
PropertyKey::Index(i) => Some(serde_json::Number::from(i.get()).to_string()),
_ => None,
};
for key in obj.own_keys::<rquickjs::String>(Filter::default()) {
let key = key?;
let v_js = obj
.get(key.clone())
.unwrap_or(rquickjs::Value::new_null(ctx.clone()));
let v_json = js_value_to_json(v_js, ctx.clone())?;
let v_js = obj.get(key, ctx).unwrap_or(JsValue::null());
let v_json = js_value_to_json(&v_js, ctx)?;
if let Some(key_val) = key_val {
map.insert(key_val, v_json);
}
map.insert(key.clone().to_string()?, v_json);
}
return Ok(Value::Object(map));
@ -171,3 +102,79 @@ pub fn js_value_to_json(value: &JsValue, ctx: &mut Context) -> anyhow::Result<Va
// Fallback for unsupported JS types: functions, symbols, etc.
Ok(Value::Null)
}
pub async fn js_invoke_async_method_to_json<'b, T, R>(
ctx: Ctx<'b>,
endpoint_name: &'b str,
name: &'b str,
args: &[T],
) -> anyhow::Result<Option<R>>
where
T: Serialize,
R: DeserializeOwned,
{
let global = ctx.globals();
let plugin_instance: Object<'b> = global.get("pluginInstance").map_err(|e| anyhow!("{e}"))?;
let core_val: Object<'b> = plugin_instance
.get(endpoint_name)
.map_err(|e| anyhow!("{e}"))?;
let js_fn: Function<'b> = core_val.get(name).map_err(|e| anyhow!("{e}"))?;
let mut args_js = Args::new(ctx.clone(), args.len() as usize);
for (i, arg) in args.iter().enumerate() {
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}"))?;
args_js.push_arg(arg_js).map_err(|e| anyhow!("{e}"))?;
}
let result_promise: Promise = js_fn.call_arg(args_js).map_err(|e| anyhow!("{e}"))?;
let result_future: rquickjs::Value = result_promise
.into_future()
.await
.catch(&ctx)
.map_err(|e| anyhow!("{e}"))?;
let value = js_value_to_json(result_future, ctx.clone()).map_err(|e| anyhow!("{e}"))?;
if value.is_null() {
return Ok(None);
}
Ok(Some(
serde_json::from_value::<R>(value).map_err(|e| anyhow!("{e}"))?,
))
}
pub fn js_invoke_method_to_json<'b, T, R>(
ctx: Ctx<'b>,
endpoint_name: &'b str,
name: &'b str,
args: &[T],
) -> anyhow::Result<Option<R>>
where
T: Serialize,
R: DeserializeOwned,
{
let global = ctx.globals();
let plugin_instance: Object<'b> = global.get("pluginInstance").map_err(|e| anyhow!("{e}"))?;
let core_val: Object<'b> = plugin_instance
.get(endpoint_name)
.map_err(|e| anyhow!("{e}"))?;
let js_fn: Function<'b> = core_val.get(name).map_err(|e| anyhow!("{e}"))?;
let mut args_js = Args::new(ctx.clone(), args.len() as usize);
for (i, arg) in args.iter().enumerate() {
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}"))?;
args_js.push_arg(arg_js).map_err(|e| anyhow!("{e}"))?;
}
let result: rquickjs::Value = js_fn.call_arg(args_js).map_err(|e| anyhow!("{e}"))?;
let value = js_value_to_json(result, ctx.clone()).map_err(|e| anyhow!("{e}"))?;
if value.is_null() {
return Ok(None);
}
Ok(Some(
serde_json::from_value::<R>(value).map_err(|e| anyhow!("{e}"))?,
))
}

View File

@ -1,22 +1,24 @@
use rquickjs::function::Async;
use rquickjs::prelude::Func;
use rquickjs::{
async_with, AsyncContext, AsyncRuntime, CatchResultExt, CaughtError, Function, Object, Promise,
Result,
};
use std::time::Duration;
mod api;
mod internal;
use rquickjs::function::{Async, Func};
use rquickjs::{async_with, AsyncContext, AsyncRuntime, Function, Object, Promise};
use tokio::time::Instant;
use crate::api::plugin::models::core::{PluginAbility, PluginConfiguration};
use crate::api::plugin::plugin::SpotubePlugin;
async fn set_timeout(func: Function<'_>, timeout: u64) {
tokio::time::sleep(std::time::Duration::from_millis(timeout)).await;
func.call::<_, ()>(()).unwrap();
}
fn print(msg: String) {
println!("{}", msg);
}
async fn set_timeout<'js>(cb: Function<'js>, number: f64) -> Result<()> {
tokio::time::sleep(Duration::from_millis(number as u64)).await;
cb.call::<_, ()>(())
}
#[tokio::main]
async fn main() -> Result<()> {
async fn non_plugin() -> anyhow::Result<()> {
let start = Instant::now();
let rt = AsyncRuntime::new()?;
let ctx = AsyncContext::full(&rt).await?;
@ -30,23 +32,85 @@ async fn main() -> Result<()> {
Function::new(ctx.clone(), Async(set_timeout)).unwrap().with_name("setTimeout")
).unwrap();
if let Ok(function) = ctx.eval::<Function, _>(r#"
(function(){
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("hello world");
resolve();
}, 100);
let check_update_fn: Function = ctx.eval(r#"
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
(async function checkUpdate() {
console.log('Core checkUpdate');
await sleep(1000);
console.log('No update available');
})
"#)?;
let (r1, r2) = tokio::join!(
check_update_fn.call::<_, Promise>(()).unwrap().into_future::<()>(),
check_update_fn.call::<_, Promise>(()).unwrap().into_future::<()>()
);
r1?;
r2?;
Ok::<(), rquickjs::Error>(())
})
"#) {
let promise: Promise = function.call(()).unwrap();
if let Err(err) = promise.into_future::<()>().await.catch(&ctx) {
eprintln!("{:?}", err);
.await
.map_err(|e| anyhow::anyhow!(e))?;
let duration = start.elapsed();
println!("[NON-PLUGIN] Execution time: {:?}", duration);
Ok(())
}
const PLUGIN_JS: &str = "\
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
class Core {
async checkUpdate() {
console.log(globalThis);
}
support() {
return 'Metadata';
}
}
})
.await;
class TestingPlugin {
constructor() {
this.core = new Core();
}
}
";
async fn plugin() -> anyhow::Result<()> {
let start = Instant::now();
let plugin = SpotubePlugin::new();
let config = PluginConfiguration {
entry_point: "TestingPlugin".to_string(),
abilities: vec![PluginAbility::Metadata],
apis: vec![],
author: "KRTirtho".to_string(),
description: "Testing Plugin".to_string(),
name: "Testing Plugin".to_string(),
plugin_api_version: "2.0.0".to_string(),
repository: None,
version: "0.1.0".to_string(),
};
let sender = SpotubePlugin::new_context(PLUGIN_JS.to_string(), config.clone())?;
let (r1, r2) = tokio::join!(
plugin.core.check_update(sender.clone(), config.clone()),
plugin.core.check_update(sender.clone(), config.clone())
);
r1?;
r2?;
let duration = start.elapsed();
println!("[PLUGIN] Execution time: {:?}", duration);
Ok(())
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
non_plugin().await?;
plugin().await?;
Ok(())
}