mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-12-06 07:29:42 +00:00
feat: add rquickjs based JavaScript plugin system in Rust with common web API support
This commit is contained in:
parent
a1672594a2
commit
f3a809752a
@ -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 {}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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> {
|
||||
|
||||
10
lib/src/rust/internal/core.dart
Normal file
10
lib/src/rust/internal/core.dart
Normal 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
10
lib/src/rust/lib.dart
Normal 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
2374
rust/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -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)'] }
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -2,5 +2,4 @@ pub mod commands;
|
||||
pub mod plugin;
|
||||
pub mod executors;
|
||||
pub mod senders;
|
||||
pub mod models;
|
||||
mod event_loop;
|
||||
pub mod models;
|
||||
@ -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
|
||||
.map_err(|e| anyhow!("{}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
.expect("Unable to create async context");
|
||||
|
||||
// #[frb(ignore)]
|
||||
async_with!(context => |ctx| {
|
||||
global_attachment.attach(&ctx)?;
|
||||
Ok::<(), Error>(())
|
||||
})
|
||||
.await
|
||||
.map_err(|e| anyhow!("Failed to register globals: {}", e))?;
|
||||
|
||||
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 = {
|
||||
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 => {
|
||||
println!("JS Executor thread shutting down.");
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
}
|
||||
};
|
||||
while let Some(command) = rx.recv().await {
|
||||
println!("JS Executor thread received command: {:?}", command);
|
||||
if let PluginCommand::Shutdown = command {
|
||||
println!("JS Executor thread shutting down.");
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
|
||||
println!("JS executor command completed");
|
||||
return result;
|
||||
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!(),
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
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(&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 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(&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 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(&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,
|
||||
)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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}"))?,
|
||||
))
|
||||
}
|
||||
|
||||
122
rust/src/main.rs
122
rust/src/main.rs
@ -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 promise: Promise = function.call(()).unwrap();
|
||||
if let Err(err) = promise.into_future::<()>().await.catch(&ctx) {
|
||||
eprintln!("{:?}", err);
|
||||
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>(())
|
||||
})
|
||||
.await;
|
||||
.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';
|
||||
}
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user