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';
|
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 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>>
|
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<PluginCommand>>
|
||||||
abstract class PluginCommand implements RustOpaqueInterface {}
|
abstract class PluginCommand implements RustOpaqueInterface {}
|
||||||
|
|||||||
@ -4,13 +4,13 @@
|
|||||||
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
|
// ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import
|
||||||
|
|
||||||
import '../../frb_generated.dart';
|
import '../../frb_generated.dart';
|
||||||
|
import '../../lib.dart';
|
||||||
import 'models/core.dart';
|
import 'models/core.dart';
|
||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
import 'senders.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 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>>
|
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<OpaqueSender>>
|
||||||
abstract class OpaqueSender implements RustOpaqueInterface {
|
abstract class OpaqueSender implements RustOpaqueInterface {
|
||||||
@ -19,9 +19,6 @@ abstract class OpaqueSender implements RustOpaqueInterface {
|
|||||||
set sender(SenderPluginCommand sender);
|
set sender(SenderPluginCommand sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rust type: RustOpaqueMoi<flutter_rust_bridge::for_generated::RustAutoOpaqueInner<Sender < PluginCommand >>>
|
|
||||||
abstract class SenderPluginCommand implements RustOpaqueInterface {}
|
|
||||||
|
|
||||||
class SpotubePlugin {
|
class SpotubePlugin {
|
||||||
final PluginArtistSender artist;
|
final PluginArtistSender artist;
|
||||||
final PluginAlbumSender album;
|
final PluginAlbumSender album;
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import 'dart:convert';
|
|||||||
import 'frb_generated.dart';
|
import 'frb_generated.dart';
|
||||||
import 'frb_generated.io.dart'
|
import 'frb_generated.io.dart'
|
||||||
if (dart.library.js_interop) 'frb_generated.web.dart';
|
if (dart.library.js_interop) 'frb_generated.web.dart';
|
||||||
|
import 'lib.dart';
|
||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart';
|
||||||
|
|
||||||
/// Main entrypoint of the Rust API
|
/// Main entrypoint of the Rust API
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import 'dart:async';
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:ffi' as ffi;
|
import 'dart:ffi' as ffi;
|
||||||
import 'frb_generated.dart';
|
import 'frb_generated.dart';
|
||||||
|
import 'lib.dart';
|
||||||
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated_io.dart';
|
||||||
|
|
||||||
abstract class RustLibApiImplPlatform extends BaseApiImpl<RustLibWire> {
|
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]
|
[dependencies]
|
||||||
flutter_rust_bridge = "=2.11.1"
|
flutter_rust_bridge = "=2.11.1"
|
||||||
boa_engine = "0.21.0"
|
|
||||||
boa_runtime = "0.21.0"
|
|
||||||
boa_gc = "0.21.0"
|
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
reqwest = { version = "0.12.x" }
|
|
||||||
http = { version = "1.3.1" }
|
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
rquickjs = { version = "0", features = ["chrono", "futures"] }
|
rquickjs = { version = "0", features = ["chrono", "futures"] }
|
||||||
tokio = { version = "1.48.0", features = ["full"] }
|
tokio = { version = "1.48.0", features = ["full"] }
|
||||||
heck = "0.5.0"
|
heck = "0.5.0"
|
||||||
futures-concurrency = "7.6.3"
|
llrt_modules = { git = "https://github.com/awslabs/llrt.git", rev = "7d749dd18cf26a2e51119094c3b945975ae57bd4", features = ["abort", "buffer", "console", "crypto", "events", "exceptions", "fetch", "navigator", "url", "timers"] }
|
||||||
futures-lite = "2.6.1"
|
|
||||||
|
|
||||||
|
[patch."https://github.com/DelSkayn/rquickjs"]
|
||||||
|
rquickjs = "0.10.0"
|
||||||
|
|
||||||
[lints.rust]
|
[lints.rust]
|
||||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(frb_expand)'] }
|
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::search::PluginSearchEndpoint;
|
||||||
use crate::internal::track::PluginTrackEndpoint;
|
use crate::internal::track::PluginTrackEndpoint;
|
||||||
use crate::internal::user::PluginUserEndpoint;
|
use crate::internal::user::PluginUserEndpoint;
|
||||||
use boa_engine::Context;
|
|
||||||
use flutter_rust_bridge::frb;
|
use flutter_rust_bridge::frb;
|
||||||
use std::fmt::Debug;
|
use std::fmt::Debug;
|
||||||
|
use rquickjs::AsyncContext;
|
||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
|
|
||||||
fn send_response<T>(tx: oneshot::Sender<T>, response: T) -> anyhow::Result<()>
|
fn send_response<T>(tx: oneshot::Sender<T>, response: T) -> anyhow::Result<()>
|
||||||
@ -26,8 +26,8 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[frb(ignore)]
|
#[frb(ignore)]
|
||||||
pub async fn execute_artists(command: ArtistCommands, context: &mut Context) -> anyhow::Result<()> {
|
pub async fn execute_artists(command: ArtistCommands, context: &AsyncContext) -> anyhow::Result<()> {
|
||||||
let mut artist = PluginArtistEndpoint::new(context);
|
let artist = PluginArtistEndpoint::new(context);
|
||||||
match command {
|
match command {
|
||||||
ArtistCommands::GetArtist { id, response_tx } => {
|
ArtistCommands::GetArtist { id, response_tx } => {
|
||||||
let artist = artist.get_artist(id).await;
|
let artist = artist.get_artist(id).await;
|
||||||
@ -72,8 +72,8 @@ pub async fn execute_artists(command: ArtistCommands, context: &mut Context) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[frb(ignore)]
|
#[frb(ignore)]
|
||||||
pub async fn execute_albums(command: AlbumCommands, context: &mut Context) -> anyhow::Result<()> {
|
pub async fn execute_albums(command: AlbumCommands, context: &AsyncContext) -> anyhow::Result<()> {
|
||||||
let mut album = PluginAlbumEndpoint::new(context);
|
let album = PluginAlbumEndpoint::new(context);
|
||||||
match command {
|
match command {
|
||||||
AlbumCommands::GetAlbum { id, response_tx } => {
|
AlbumCommands::GetAlbum { id, response_tx } => {
|
||||||
let album = album.get_album(id).await;
|
let album = album.get_album(id).await;
|
||||||
@ -110,9 +110,9 @@ pub async fn execute_albums(command: AlbumCommands, context: &mut Context) -> an
|
|||||||
#[frb(ignore)]
|
#[frb(ignore)]
|
||||||
pub async fn execute_audio_source(
|
pub async fn execute_audio_source(
|
||||||
command: AudioSourceCommands,
|
command: AudioSourceCommands,
|
||||||
context: &mut Context,
|
context: &AsyncContext,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let mut audio_source = PluginAudioSourceEndpoint::new(context);
|
let audio_source = PluginAudioSourceEndpoint::new(context);
|
||||||
match command {
|
match command {
|
||||||
AudioSourceCommands::Matches { track, response_tx } => {
|
AudioSourceCommands::Matches { track, response_tx } => {
|
||||||
let audio_source = audio_source.matches(track).await;
|
let audio_source = audio_source.matches(track).await;
|
||||||
@ -129,15 +129,15 @@ pub async fn execute_audio_source(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[frb(ignore)]
|
#[frb(ignore)]
|
||||||
pub async fn execute_auth(command: AuthCommands, context: &mut Context) -> anyhow::Result<()> {
|
pub async fn execute_auth(command: AuthCommands, context: &AsyncContext) -> anyhow::Result<()> {
|
||||||
let mut auth = PluginAuthEndpoint::new(context);
|
let auth = PluginAuthEndpoint::new(context);
|
||||||
match command {
|
match command {
|
||||||
AuthCommands::Authenticate { response_tx } => {
|
AuthCommands::Authenticate { response_tx } => {
|
||||||
let res = auth.authenticate().await;
|
let res = auth.authenticate().await;
|
||||||
send_response(response_tx, res)
|
send_response(response_tx, res)
|
||||||
}
|
}
|
||||||
AuthCommands::IsAuthenticated { response_tx } => {
|
AuthCommands::IsAuthenticated { response_tx } => {
|
||||||
let res = auth.is_authenticated();
|
let res = auth.is_authenticated().await;
|
||||||
send_response(response_tx, res)
|
send_response(response_tx, res)
|
||||||
}
|
}
|
||||||
AuthCommands::Logout { response_tx } => {
|
AuthCommands::Logout { response_tx } => {
|
||||||
@ -148,8 +148,8 @@ pub async fn execute_auth(command: AuthCommands, context: &mut Context) -> anyho
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[frb(ignore)]
|
#[frb(ignore)]
|
||||||
pub async fn execute_browse(command: BrowseCommands, context: &mut Context) -> anyhow::Result<()> {
|
pub async fn execute_browse(command: BrowseCommands, context: &AsyncContext) -> anyhow::Result<()> {
|
||||||
let mut browse = PluginBrowseEndpoint::new(context);
|
let browse = PluginBrowseEndpoint::new(context);
|
||||||
match command {
|
match command {
|
||||||
BrowseCommands::Sections {
|
BrowseCommands::Sections {
|
||||||
offset,
|
offset,
|
||||||
@ -172,8 +172,8 @@ pub async fn execute_browse(command: BrowseCommands, context: &mut Context) -> a
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[frb(ignore)]
|
#[frb(ignore)]
|
||||||
pub async fn execute_core(command: CoreCommands, context: &mut Context) -> anyhow::Result<()> {
|
pub async fn execute_core(command: CoreCommands, context: &AsyncContext) -> anyhow::Result<()> {
|
||||||
let mut core = PluginCoreEndpoint::new(context);
|
let core = PluginCoreEndpoint::new(context);
|
||||||
match command {
|
match command {
|
||||||
CoreCommands::CheckUpdate {
|
CoreCommands::CheckUpdate {
|
||||||
response_tx,
|
response_tx,
|
||||||
@ -190,7 +190,7 @@ pub async fn execute_core(command: CoreCommands, context: &mut Context) -> anyho
|
|||||||
send_response(response_tx, res)
|
send_response(response_tx, res)
|
||||||
}
|
}
|
||||||
CoreCommands::Support { response_tx } => {
|
CoreCommands::Support { response_tx } => {
|
||||||
let res = core.support();
|
let res = core.support().await;
|
||||||
send_response(response_tx, res)
|
send_response(response_tx, res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -199,9 +199,9 @@ pub async fn execute_core(command: CoreCommands, context: &mut Context) -> anyho
|
|||||||
#[frb(ignore)]
|
#[frb(ignore)]
|
||||||
pub async fn execute_playlist(
|
pub async fn execute_playlist(
|
||||||
command: PlaylistCommands,
|
command: PlaylistCommands,
|
||||||
context: &mut Context,
|
context: &AsyncContext,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let mut playlist = PluginPlaylistEndpoint::new(context);
|
let playlist = PluginPlaylistEndpoint::new(context);
|
||||||
match command {
|
match command {
|
||||||
PlaylistCommands::GetPlaylist { id, response_tx } => {
|
PlaylistCommands::GetPlaylist { id, response_tx } => {
|
||||||
let playlist = playlist.get_playlist(id).await;
|
let playlist = playlist.get_playlist(id).await;
|
||||||
@ -284,11 +284,11 @@ pub async fn execute_playlist(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[frb(ignore)]
|
#[frb(ignore)]
|
||||||
pub async fn execute_search(command: SearchCommands, context: &mut Context) -> anyhow::Result<()> {
|
pub async fn execute_search(command: SearchCommands, context: &AsyncContext) -> anyhow::Result<()> {
|
||||||
let mut search = PluginSearchEndpoint::new(context);
|
let search = PluginSearchEndpoint::new(context);
|
||||||
match command {
|
match command {
|
||||||
SearchCommands::Chips { response_tx } => {
|
SearchCommands::Chips { response_tx } => {
|
||||||
let chips = search.chips();
|
let chips = search.chips().await;
|
||||||
send_response(response_tx, chips)
|
send_response(response_tx, chips)
|
||||||
}
|
}
|
||||||
SearchCommands::All { query, response_tx } => {
|
SearchCommands::All { query, response_tx } => {
|
||||||
@ -335,8 +335,8 @@ pub async fn execute_search(command: SearchCommands, context: &mut Context) -> a
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[frb(ignore)]
|
#[frb(ignore)]
|
||||||
pub async fn execute_track(command: TrackCommands, context: &mut Context) -> anyhow::Result<()> {
|
pub async fn execute_track(command: TrackCommands, context: &AsyncContext) -> anyhow::Result<()> {
|
||||||
let mut track = PluginTrackEndpoint::new(context);
|
let track = PluginTrackEndpoint::new(context);
|
||||||
match command {
|
match command {
|
||||||
TrackCommands::GetTrack { id, response_tx } => {
|
TrackCommands::GetTrack { id, response_tx } => {
|
||||||
let res = track.get_track(id).await;
|
let res = track.get_track(id).await;
|
||||||
@ -358,8 +358,8 @@ pub async fn execute_track(command: TrackCommands, context: &mut Context) -> any
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[frb(ignore)]
|
#[frb(ignore)]
|
||||||
pub async fn execute_user(command: UserCommands, context: &mut Context) -> anyhow::Result<()> {
|
pub async fn execute_user(command: UserCommands, context: &AsyncContext) -> anyhow::Result<()> {
|
||||||
let mut user = PluginUserEndpoint::new(context);
|
let user = PluginUserEndpoint::new(context);
|
||||||
match command {
|
match command {
|
||||||
UserCommands::Me { response_tx } => {
|
UserCommands::Me { response_tx } => {
|
||||||
let me = user.me().await;
|
let me = user.me().await;
|
||||||
|
|||||||
@ -3,4 +3,3 @@ pub mod plugin;
|
|||||||
pub mod executors;
|
pub mod executors;
|
||||||
pub mod senders;
|
pub mod senders;
|
||||||
pub mod models;
|
pub mod models;
|
||||||
mod event_loop;
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
use crate::api::plugin::commands::PluginCommand;
|
use crate::api::plugin::commands::PluginCommand;
|
||||||
use crate::api::plugin::event_loop::Queue;
|
|
||||||
use crate::api::plugin::executors::{
|
use crate::api::plugin::executors::{
|
||||||
execute_albums, execute_artists, execute_audio_source, execute_auth, execute_browse,
|
execute_albums, execute_artists, execute_audio_source, execute_auth, execute_browse,
|
||||||
execute_core, execute_playlist, execute_search, execute_track, execute_user,
|
execute_core, execute_playlist, execute_search, execute_track, execute_user,
|
||||||
@ -10,122 +9,96 @@ use crate::api::plugin::senders::{
|
|||||||
PluginBrowseSender, PluginCoreSender, PluginPlaylistSender, PluginSearchSender,
|
PluginBrowseSender, PluginCoreSender, PluginPlaylistSender, PluginSearchSender,
|
||||||
PluginTrackSender, PluginUserSender,
|
PluginTrackSender, PluginUserSender,
|
||||||
};
|
};
|
||||||
use crate::internal::apis::fetcher::ReqwestFetcher;
|
|
||||||
use anyhow::anyhow;
|
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 flutter_rust_bridge::frb;
|
||||||
use std::cell::RefCell;
|
use llrt_modules::{abort, buffer, console, crypto, events, exceptions, fetch, navigator, timers, url, util};
|
||||||
use std::rc::Rc;
|
use llrt_modules::module_builder::ModuleBuilder;
|
||||||
use std::sync::Arc;
|
use rquickjs::{async_with, AsyncContext, AsyncRuntime, Error};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Duration;
|
use tokio::sync::mpsc;
|
||||||
use tokio::runtime::Runtime;
|
|
||||||
use tokio::sync::mpsc::Sender;
|
use tokio::sync::mpsc::Sender;
|
||||||
use tokio::sync::{mpsc, Mutex};
|
|
||||||
use tokio::task;
|
use tokio::task;
|
||||||
|
use tokio::task::LocalSet;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[frb(opaque)]
|
|
||||||
pub struct OpaqueSender {
|
pub struct OpaqueSender {
|
||||||
pub sender: Sender<PluginCommand>,
|
pub sender: Sender<PluginCommand>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn js_poller_thread(context: Arc<Mutex<Context>>, queue: Rc<Queue>) -> anyhow::Result<()> {
|
#[frb(ignore)]
|
||||||
let local_set = task::LocalSet::new();
|
async fn create_context() -> anyhow::Result<(AsyncContext, AsyncRuntime)> {
|
||||||
|
let runtime = AsyncRuntime::new().expect("Unable to create async runtime");
|
||||||
|
|
||||||
local_set
|
let mut module_builder = ModuleBuilder::new();
|
||||||
.run_until(async {
|
|
||||||
let mut ctx = context.lock().await;
|
module_builder = module_builder
|
||||||
queue.run_jobs_async(&RefCell::new(&mut *ctx)).await
|
.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
|
.await
|
||||||
.map_err(|e| anyhow!("{}", e))?;
|
.expect("Unable to create async context");
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// #[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(
|
async fn js_executor_thread(
|
||||||
rx: &mut mpsc::Receiver<PluginCommand>,
|
rx: &mut mpsc::Receiver<PluginCommand>,
|
||||||
context: Arc<Mutex<Context>>,
|
ctx: &AsyncContext,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
if let Some(command) = rx.recv().await {
|
while let Some(command) = rx.recv().await {
|
||||||
let result = {
|
println!("JS Executor thread received command: {:?}", command);
|
||||||
println!("JS Executor thread received command: {:?}", command);
|
if let PluginCommand::Shutdown = command {
|
||||||
match command {
|
println!("JS Executor thread shutting down.");
|
||||||
PluginCommand::Artist(commands) => {
|
return anyhow::Ok(());
|
||||||
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(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
println!("JS executor command completed");
|
let ctx = ctx.clone();
|
||||||
return result;
|
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(())
|
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 struct SpotubePlugin {
|
||||||
pub artist: PluginArtistSender,
|
pub artist: PluginArtistSender,
|
||||||
pub album: PluginAlbumSender,
|
pub album: PluginAlbumSender,
|
||||||
@ -156,7 +129,7 @@ impl SpotubePlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[frb(sync)]
|
#[frb(sync)]
|
||||||
pub fn new_context(
|
pub fn new_context(
|
||||||
plugin_script: String,
|
plugin_script: String,
|
||||||
plugin_config: PluginConfiguration,
|
plugin_config: PluginConfiguration,
|
||||||
@ -164,10 +137,13 @@ impl SpotubePlugin {
|
|||||||
let (command_tx, mut command_rx) = mpsc::channel(32);
|
let (command_tx, mut command_rx) = mpsc::channel(32);
|
||||||
|
|
||||||
let _thread_handle = thread::spawn(move || {
|
let _thread_handle = thread::spawn(move || {
|
||||||
let rt = Runtime::new().unwrap();
|
let rt = tokio::runtime::Builder::new_current_thread()
|
||||||
if let Err(e) = rt.block_on(async {
|
.enable_all()
|
||||||
let (context, queue) = create_context().await.unwrap();
|
.build()
|
||||||
let context_arc_mutex = Arc::new(Mutex::new(context));
|
.unwrap();
|
||||||
|
let local = LocalSet::new();
|
||||||
|
if let Err(e) = local.block_on(&rt, async {
|
||||||
|
let (ctx, runtime) = create_context().await?;
|
||||||
|
|
||||||
let injection = format!(
|
let injection = format!(
|
||||||
"globalThis.pluginInstance = new {}();",
|
"globalThis.pluginInstance = new {}();",
|
||||||
@ -175,38 +151,11 @@ impl SpotubePlugin {
|
|||||||
);
|
);
|
||||||
let script = format!("{}\n{}", plugin_script, injection);
|
let script = format!("{}\n{}", plugin_script, injection);
|
||||||
|
|
||||||
{
|
ctx.with(|cx| cx.eval::<(), _>(script.as_str())).await?;
|
||||||
let context_refcell = context_arc_mutex.clone();
|
|
||||||
|
|
||||||
context_refcell
|
if let Err(e) = js_executor_thread(&mut command_rx, &ctx).await {
|
||||||
.lock()
|
eprintln!("JS executor error: {}", e);
|
||||||
.await
|
|
||||||
.eval(Source::from_bytes(script.as_bytes()))
|
|
||||||
.map_err(|e| anyhow!("{}", 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(())
|
anyhow::Ok(())
|
||||||
}) {
|
}) {
|
||||||
eprintln!("JS Executor thread error: {}", e);
|
eprintln!("JS Executor thread error: {}", e);
|
||||||
|
|||||||
@ -27,6 +27,7 @@
|
|||||||
|
|
||||||
use crate::api::plugin::commands::*;
|
use crate::api::plugin::commands::*;
|
||||||
use crate::api::plugin::plugin::*;
|
use crate::api::plugin::plugin::*;
|
||||||
|
use crate::*;
|
||||||
use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt};
|
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::for_generated::{transform_result_dco, Lifetimeable, Lockable};
|
||||||
use flutter_rust_bridge::{Handler, IntoIntoDart};
|
use flutter_rust_bridge::{Handler, IntoIntoDart};
|
||||||
@ -5764,6 +5765,7 @@ mod io {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::api::plugin::commands::*;
|
use crate::api::plugin::commands::*;
|
||||||
use crate::api::plugin::plugin::*;
|
use crate::api::plugin::plugin::*;
|
||||||
|
use crate::*;
|
||||||
use flutter_rust_bridge::for_generated::byteorder::{
|
use flutter_rust_bridge::for_generated::byteorder::{
|
||||||
NativeEndian, ReadBytesExt, WriteBytesExt,
|
NativeEndian, ReadBytesExt, WriteBytesExt,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,148 +1,94 @@
|
|||||||
use crate::api::plugin::models::album::SpotubeFullAlbumObject;
|
use crate::api::plugin::models::album::SpotubeFullAlbumObject;
|
||||||
use crate::api::plugin::models::pagination::SpotubePaginationResponseObject;
|
use crate::api::plugin::models::pagination::SpotubePaginationResponseObject;
|
||||||
use crate::internal::utils;
|
use crate::internal::utils::js_invoke_async_method_to_json;
|
||||||
use anyhow::anyhow;
|
|
||||||
use boa_engine::{js_string, Context, JsValue};
|
|
||||||
use flutter_rust_bridge::frb;
|
use flutter_rust_bridge::frb;
|
||||||
|
use rquickjs::{async_with, AsyncContext};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
#[derive(Debug)]
|
pub struct PluginAlbumEndpoint<'a>(&'a AsyncContext);
|
||||||
pub struct PluginAlbumEndpoint<'a>(&'a mut Context);
|
|
||||||
|
|
||||||
impl<'a> PluginAlbumEndpoint<'a> {
|
impl<'a> PluginAlbumEndpoint<'a> {
|
||||||
#[frb(ignore)]
|
#[frb(ignore)]
|
||||||
pub fn new(context: &'a mut Context) -> PluginAlbumEndpoint<'a> {
|
pub fn new(context: &'a AsyncContext) -> PluginAlbumEndpoint<'a> {
|
||||||
PluginAlbumEndpoint(context)
|
PluginAlbumEndpoint(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn album_obj(&mut self) -> anyhow::Result<JsValue> {
|
pub async fn get_album(&self, id: String) -> anyhow::Result<SpotubeFullAlbumObject> {
|
||||||
let global = self.0.global_object();
|
async_with!(self.0 => |ctx| {
|
||||||
|
Ok(
|
||||||
let plugin_instance = global
|
js_invoke_async_method_to_json(ctx.clone(), "album", "getAlbum", &[id]).await
|
||||||
.get(js_string!("pluginInstance"), self.0)
|
?.expect("[hey][smartypants] album.getAlbum should return a SpotifyFullAlbumObject")
|
||||||
.map_err(|e| anyhow!("{}", e))
|
)
|
||||||
.and_then(|a| a.as_object().ok_or(anyhow!("Not an object")))?;
|
})
|
||||||
|
.await
|
||||||
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 tracks(
|
pub async fn tracks(
|
||||||
&mut self,
|
&self,
|
||||||
|
|
||||||
id: String,
|
id: String,
|
||||||
offset: Option<u32>,
|
offset: Option<u32>,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
||||||
let album_val = self.album_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let album_object = album_val.as_object().ok_or(anyhow!("Not an object"))?;
|
Ok(
|
||||||
|
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
|
||||||
let tracks_fn = album_object
|
ctx.clone(),
|
||||||
.get(js_string!("tracks"), self.0)
|
"album",
|
||||||
.map_err(|e| anyhow!("JS error while accessing tracks: {}", e))?
|
"tracks",
|
||||||
.as_function()
|
&[Value::String(id), serde_json::to_value(offset)?, serde_json::to_value(limit.unwrap())?]
|
||||||
.ok_or(anyhow!("tracks is not a function"))?;
|
)
|
||||||
|
.await?
|
||||||
let args: [JsValue; 3] = [
|
.expect("[hey][smartypants] album.tracks should return a SpotifyPaginationResponseObject")
|
||||||
JsValue::from(js_string!(id)),
|
)
|
||||||
match offset {
|
}).await
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn releases(
|
pub async fn releases(
|
||||||
&mut self,
|
&self,
|
||||||
|
|
||||||
offset: Option<u32>,
|
offset: Option<u32>,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
||||||
let album_val = self.album_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let album_object = album_val.as_object().ok_or(anyhow!("Not an object"))?;
|
Ok(
|
||||||
|
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
|
||||||
let releases_fn = album_object
|
ctx.clone(),
|
||||||
.get(js_string!("releases"), self.0)
|
"album",
|
||||||
.map_err(|e| anyhow!("JS error while accessing releases: {}", e))?
|
"releases",
|
||||||
.as_function()
|
&[serde_json::to_value(offset)?, serde_json::to_value(limit.unwrap())?]
|
||||||
.ok_or(anyhow!("releases is not a function"))?;
|
)
|
||||||
|
.await?
|
||||||
let args: [JsValue; 2] = [
|
.expect("[hey][smartypants] album.releases should return a SpotifyPaginationResponseObject")
|
||||||
match offset {
|
)
|
||||||
Some(o) => JsValue::from(o),
|
}).await
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn save(&mut self, ids: Vec<String>) -> anyhow::Result<()> {
|
pub async fn save(&self, ids: Vec<String>) -> anyhow::Result<()> {
|
||||||
let album_val = self.album_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let album_object = album_val.as_object().ok_or(anyhow!("Not an object"))?;
|
Ok(
|
||||||
|
js_invoke_async_method_to_json::<_, ()>(
|
||||||
let save_fn = album_object
|
ctx.clone(),
|
||||||
.get(js_string!("save"), self.0)
|
"album",
|
||||||
.map_err(|e| anyhow!("JS error while accessing save: {}", e))?
|
"save",
|
||||||
.as_function()
|
&[Value::Array(ids.into_iter().map(|id| Value::String(id)).collect())]
|
||||||
.ok_or(anyhow!("save is not a function"))?;
|
)
|
||||||
|
.await?
|
||||||
let ids_val = utils::vec_string_to_js_array(ids, self.0)?;
|
.expect("[hey][smartypants] album.save should return a SpotifyPaginationResponseObject")
|
||||||
let args = [ids_val.into()];
|
)
|
||||||
|
}).await
|
||||||
utils::js_call_to_void(save_fn.call(&album_val, &args, self.0), self.0).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unsave(&mut self, ids: Vec<String>) -> anyhow::Result<()> {
|
pub async fn unsave(&self, ids: Vec<String>) -> anyhow::Result<()> {
|
||||||
let album_val = self.album_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let album_object = album_val.as_object().ok_or(anyhow!("Not an object"))?;
|
Ok(
|
||||||
|
js_invoke_async_method_to_json::<_, ()>(
|
||||||
let unsave_fn = album_object
|
ctx.clone(),
|
||||||
.get(js_string!("unsave"), self.0)
|
"album",
|
||||||
.map_err(|e| anyhow!("JS error while accessing save: {}", e))?
|
"unsave",
|
||||||
.as_function()
|
&[Value::Array(ids.into_iter().map(|id| Value::String(id)).collect())]
|
||||||
.ok_or(anyhow!("save is not a function"))?;
|
)
|
||||||
|
.await?
|
||||||
let ids_val = utils::vec_string_to_js_array(ids, self.0)?;
|
.expect("[hey][smartypants] album.unsave should return a SpotifyPaginationResponseObject")
|
||||||
let args = [ids_val.into()];
|
)
|
||||||
|
}).await
|
||||||
utils::js_call_to_void(unsave_fn.call(&album_val, &args, self.0), self.0).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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::artist::SpotubeFullArtistObject;
|
||||||
use crate::api::plugin::models::pagination::SpotubePaginationResponseObject;
|
use crate::api::plugin::models::pagination::SpotubePaginationResponseObject;
|
||||||
use crate::internal::utils;
|
use crate::internal::utils::js_invoke_async_method_to_json;
|
||||||
use anyhow::anyhow;
|
use rquickjs::{async_with, AsyncContext};
|
||||||
use boa_engine::{js_string, Context, JsValue};
|
use serde_json::Value;
|
||||||
use flutter_rust_bridge::frb;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
pub struct PluginArtistEndpoint<'a>(&'a AsyncContext);
|
||||||
pub struct PluginArtistEndpoint<'a>(&'a mut Context);
|
|
||||||
|
|
||||||
impl<'a> PluginArtistEndpoint<'a> {
|
impl<'a> PluginArtistEndpoint<'a> {
|
||||||
#[frb(ignore)]
|
#[frb(ignore)]
|
||||||
pub fn new(context: &'a mut Context) -> PluginArtistEndpoint<'a> {
|
pub fn new(context: &'a AsyncContext) -> PluginArtistEndpoint<'a> {
|
||||||
PluginArtistEndpoint(context)
|
PluginArtistEndpoint(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn artist_obj(&mut self) -> anyhow::Result<JsValue> {
|
pub async fn get_artist(&self, id: String) -> anyhow::Result<SpotubeFullArtistObject> {
|
||||||
let global = self.0.global_object();
|
async_with!(self.0 => |ctx| {
|
||||||
|
Ok(
|
||||||
let plugin_instance = global
|
js_invoke_async_method_to_json(ctx.clone(), "artist", "getArtist", &[id]).await
|
||||||
.get(js_string!("pluginInstance"), self.0)
|
?.expect("[hey][smartypants] artist.getArtist should return a SpotifyFullArtistObject")
|
||||||
.map_err(|e| anyhow!("{}", e))
|
)
|
||||||
.and_then(|a| a.as_object().ok_or(anyhow!("Not an object")))?;
|
})
|
||||||
|
.await
|
||||||
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 top_tracks(
|
pub async fn top_tracks(
|
||||||
&mut self,
|
&self,
|
||||||
id: String,
|
id: String,
|
||||||
offset: Option<u32>,
|
offset: Option<u32>,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
||||||
let artist_val = self.artist_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let artist_object = artist_val.as_object().ok_or(anyhow!("Not an object"))?;
|
Ok(
|
||||||
|
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
|
||||||
let top_tracks_fn = artist_object
|
ctx.clone(),
|
||||||
.get(js_string!("topTracks"), self.0)
|
"artist",
|
||||||
.map_err(|e| anyhow!("JS error while accessing getArtist: {}", e))?
|
"topTracks",
|
||||||
.as_function()
|
&[Value::String(id), serde_json::to_value(offset)?, serde_json::to_value(limit.unwrap())?]
|
||||||
.ok_or(anyhow!("getArtist is not a function"))?;
|
)
|
||||||
|
.await?
|
||||||
let args: [JsValue; 3] = [
|
.expect("[hey][smartypants] album.tracks should return a SpotifyPaginationResponseObject")
|
||||||
JsValue::from(js_string!(id)),
|
)
|
||||||
match offset {
|
}).await
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn albums(
|
pub async fn albums(
|
||||||
&mut self,
|
&self,
|
||||||
id: String,
|
id: String,
|
||||||
offset: Option<u32>,
|
offset: Option<u32>,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
||||||
let artist_val = self.artist_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let artist_object = artist_val.as_object().ok_or(anyhow!("Not an object"))?;
|
Ok(
|
||||||
|
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
|
||||||
let albums_fn = artist_object
|
ctx.clone(),
|
||||||
.get(js_string!("albums"), self.0)
|
"artist",
|
||||||
.map_err(|e| anyhow!("JS error while accessing albums: {}", e))?
|
"albums",
|
||||||
.as_function()
|
&[Value::String(id), serde_json::to_value(offset)?, serde_json::to_value(limit.unwrap())?]
|
||||||
.ok_or(anyhow!("albums is not a function"))?;
|
)
|
||||||
|
.await?
|
||||||
let args: [JsValue; 3] = [
|
.expect("[hey][smartypants] artist.albums should return a SpotifyPaginationResponseObject")
|
||||||
JsValue::from(js_string!(id)),
|
)
|
||||||
match offset {
|
}).await
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn related(
|
pub async fn related(
|
||||||
&mut self,
|
&self,
|
||||||
id: String,
|
id: String,
|
||||||
offset: Option<u32>,
|
offset: Option<u32>,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
||||||
let artist_val = self.artist_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let artist_object = artist_val.as_object().ok_or(anyhow!("Not an object"))?;
|
Ok(
|
||||||
|
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
|
||||||
let related_fn = artist_object
|
ctx.clone(),
|
||||||
.get(js_string!("related"), self.0)
|
"artist",
|
||||||
.map_err(|e| anyhow!("JS error while accessing related: {}", e))?
|
"related",
|
||||||
.as_function()
|
&[Value::String(id), serde_json::to_value(offset)?, serde_json::to_value(limit.unwrap())?]
|
||||||
.ok_or(anyhow!("related is not a function"))?;
|
)
|
||||||
|
.await?
|
||||||
let args = [
|
.expect("[hey][smartypants] artist.related should return a SpotifyPaginationResponseObject")
|
||||||
JsValue::from(js_string!(id)),
|
)
|
||||||
match offset {
|
}).await
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn save(&mut self, ids: Vec<String>) -> anyhow::Result<()> {
|
pub async fn save(&self, ids: Vec<String>) -> anyhow::Result<()> {
|
||||||
let artist_val = self.artist_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let artist_object = artist_val.as_object().ok_or(anyhow!("Not an object"))?;
|
Ok(
|
||||||
|
js_invoke_async_method_to_json::<_, ()>(
|
||||||
let save_fn = artist_object
|
ctx.clone(),
|
||||||
.get(js_string!("save"), self.0)
|
"artist",
|
||||||
.map_err(|e| anyhow!("JS error while accessing save: {}", e))?
|
"save",
|
||||||
.as_function()
|
&[Value::Array(ids.into_iter().map(|id| Value::String(id)).collect())]
|
||||||
.ok_or(anyhow!("save is not a function"))?;
|
)
|
||||||
|
.await?
|
||||||
let ids_val = utils::vec_string_to_js_array(ids, self.0)?;
|
.expect("[hey][smartypants] artist.save should return a SpotifyPaginationResponseObject")
|
||||||
let args = [ids_val.into()];
|
)
|
||||||
|
}).await
|
||||||
utils::js_call_to_void(save_fn.call(&artist_val, &args, self.0), self.0).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unsave(&mut self, ids: Vec<String>) -> anyhow::Result<()> {
|
pub async fn unsave(&self, ids: Vec<String>) -> anyhow::Result<()> {
|
||||||
let artist_val = self.artist_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let artist_object = artist_val.as_object().ok_or(anyhow!("Not an object"))?;
|
Ok(
|
||||||
|
js_invoke_async_method_to_json::<_, ()>(
|
||||||
let unsave_fn = artist_object
|
ctx.clone(),
|
||||||
.get(js_string!("unsave"), self.0)
|
"artist",
|
||||||
.map_err(|e| anyhow!("JS error while accessing save: {}", e))?
|
"unsave",
|
||||||
.as_function()
|
&[Value::Array(ids.into_iter().map(|id| Value::String(id)).collect())]
|
||||||
.ok_or(anyhow!("save is not a function"))?;
|
)
|
||||||
|
.await?
|
||||||
let ids_val = utils::vec_string_to_js_array(ids, self.0)?;
|
.expect("[hey][smartypants] artist.unsave should return a SpotifyPaginationResponseObject")
|
||||||
let args = [ids_val.into()];
|
)
|
||||||
|
}).await
|
||||||
utils::js_call_to_void(unsave_fn.call(&artist_val, &args, self.0), self.0).await
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,80 +2,51 @@ use crate::api::plugin::models::audio_source::{
|
|||||||
SpotubeAudioSourceMatchObject, SpotubeAudioSourceStreamObject,
|
SpotubeAudioSourceMatchObject, SpotubeAudioSourceStreamObject,
|
||||||
};
|
};
|
||||||
use crate::api::plugin::models::track::SpotubeTrackObject;
|
use crate::api::plugin::models::track::SpotubeTrackObject;
|
||||||
use crate::internal::utils;
|
use crate::internal::utils::js_invoke_async_method_to_json;
|
||||||
use anyhow::anyhow;
|
|
||||||
use boa_engine::{js_string, Context, JsValue};
|
|
||||||
use flutter_rust_bridge::frb;
|
use flutter_rust_bridge::frb;
|
||||||
|
use rquickjs::{async_with, AsyncContext};
|
||||||
|
|
||||||
#[derive(Debug)]
|
pub struct PluginAudioSourceEndpoint<'a>(&'a AsyncContext);
|
||||||
pub struct PluginAudioSourceEndpoint<'a>(&'a mut Context);
|
|
||||||
|
|
||||||
impl<'a> PluginAudioSourceEndpoint<'a> {
|
impl<'a> PluginAudioSourceEndpoint<'a> {
|
||||||
#[frb(ignore)]
|
#[frb(ignore)]
|
||||||
pub fn new(context: &'a mut Context) -> PluginAudioSourceEndpoint<'a> {
|
pub fn new(context: &'a AsyncContext) -> PluginAudioSourceEndpoint<'a> {
|
||||||
PluginAudioSourceEndpoint(context)
|
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(
|
pub async fn matches(
|
||||||
&mut self,
|
&self,
|
||||||
track: SpotubeTrackObject,
|
track: SpotubeTrackObject,
|
||||||
) -> anyhow::Result<Vec<SpotubeAudioSourceMatchObject>> {
|
) -> anyhow::Result<Vec<SpotubeAudioSourceMatchObject>> {
|
||||||
let audio_source_val = self.audio_source_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let audio_source_object = audio_source_val
|
Ok(
|
||||||
.as_object()
|
js_invoke_async_method_to_json(
|
||||||
.ok_or(anyhow!("Not an object"))?;
|
ctx.clone(),
|
||||||
|
"audioSource",
|
||||||
let matches_fn = audio_source_object
|
"matches",
|
||||||
.get(js_string!("matches"), self.0)
|
&[track]
|
||||||
.map_err(|e| anyhow!("JS error while accessing matches: {}", e))?
|
)
|
||||||
.as_function()
|
.await?
|
||||||
.ok_or(anyhow!("matches is not a function"))?;
|
.expect("[hey][smartypants] album.tracks should return a SpotifyPaginationResponseObject")
|
||||||
|
)
|
||||||
let value = serde_json::to_value(track)?;
|
}).await
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn streams(
|
pub async fn streams(
|
||||||
&mut self,
|
&self,
|
||||||
matched: SpotubeAudioSourceMatchObject,
|
matched: SpotubeAudioSourceMatchObject,
|
||||||
) -> anyhow::Result<Vec<SpotubeAudioSourceStreamObject>> {
|
) -> anyhow::Result<Vec<SpotubeAudioSourceStreamObject>> {
|
||||||
let audio_source_val = self.audio_source_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let audio_source_object = audio_source_val
|
Ok(
|
||||||
.as_object()
|
js_invoke_async_method_to_json(
|
||||||
.ok_or(anyhow!("Not an object"))?;
|
ctx.clone(),
|
||||||
|
"audioSource",
|
||||||
let matches_fn = audio_source_object
|
"streams",
|
||||||
.get(js_string!("streams"), self.0)
|
&[matched]
|
||||||
.map_err(|e| anyhow!("JS error while accessing matches: {}", e))?
|
)
|
||||||
.as_function()
|
.await?
|
||||||
.ok_or(anyhow!("matches is not a function"))?;
|
.expect("[hey][smartypants] audioSource.streams should return a SpotifyPaginationResponseObject")
|
||||||
|
)
|
||||||
let value = serde_json::to_value(matched)?;
|
}).await
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,74 +1,60 @@
|
|||||||
use crate::internal::utils;
|
use crate::internal::utils::js_invoke_async_method_to_json;
|
||||||
use anyhow::anyhow;
|
|
||||||
use boa_engine::{js_string, Context, JsValue};
|
|
||||||
use flutter_rust_bridge::frb;
|
use flutter_rust_bridge::frb;
|
||||||
|
use rquickjs::{async_with, AsyncContext};
|
||||||
|
|
||||||
#[derive(Debug)]
|
pub struct PluginAuthEndpoint<'a>(&'a AsyncContext);
|
||||||
pub struct PluginAuthEndpoint<'a>(&'a mut Context);
|
|
||||||
|
|
||||||
impl<'a> PluginAuthEndpoint<'a> {
|
impl<'a> PluginAuthEndpoint<'a> {
|
||||||
#[frb(ignore)]
|
#[frb(ignore)]
|
||||||
pub fn new(context: &'a mut Context) -> PluginAuthEndpoint<'a> {
|
pub fn new(context: &'a AsyncContext) -> PluginAuthEndpoint<'a> {
|
||||||
PluginAuthEndpoint(context)
|
PluginAuthEndpoint(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn auth_obj(&mut self) -> anyhow::Result<JsValue> {
|
pub async fn authenticate(&self) -> anyhow::Result<()> {
|
||||||
let global = self.0.global_object();
|
async_with!(self.0 => |ctx| {
|
||||||
|
Ok(
|
||||||
let plugin_instance = global
|
js_invoke_async_method_to_json::<(), ()>(
|
||||||
.get(js_string!("pluginInstance"), self.0)
|
ctx.clone(),
|
||||||
.map_err(|e| anyhow!("{}", e))
|
"auth",
|
||||||
.and_then(|a| a.as_object().ok_or(anyhow!("Not an object")))?;
|
"authenticate",
|
||||||
|
&[]
|
||||||
return plugin_instance
|
)
|
||||||
.get(js_string!("auth"), self.0)
|
.await?
|
||||||
.or_else(|e| Err(anyhow!("auth not found:\n{}", e)));
|
.expect("[hey][smartypants] auth.authenticate should return a void")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn authenticate(&mut self) -> anyhow::Result<()> {
|
pub async fn is_authenticated(&self) -> anyhow::Result<bool> {
|
||||||
let auth_val = self.auth_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let auth_object = auth_val.as_object().ok_or(anyhow!("Not an object"))?;
|
Ok(
|
||||||
|
js_invoke_async_method_to_json::<(), bool>(
|
||||||
let authenticate_fn = auth_object
|
ctx.clone(),
|
||||||
.get(js_string!("authenticate"), self.0)
|
"auth",
|
||||||
.map_err(|e| anyhow!("JS error while accessing authenticate: {}", e))?
|
"is_authenticated",
|
||||||
.as_function()
|
&[]
|
||||||
.ok_or(anyhow!("authenticate is not a function"))?;
|
)
|
||||||
|
.await?
|
||||||
let args = [];
|
.expect("[hey][smartypants] auth.is_authenticated should return a boolean")
|
||||||
|
)
|
||||||
utils::js_call_to_void(authenticate_fn.call(&auth_val, &args, self.0), self.0).await
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_authenticated(&mut self) -> anyhow::Result<bool> {
|
pub async fn logout(&self) -> anyhow::Result<()> {
|
||||||
let auth_val = self.auth_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let auth_object = auth_val.as_object().ok_or(anyhow!("Not an object"))?;
|
Ok(
|
||||||
|
js_invoke_async_method_to_json::<(), ()>(
|
||||||
let authenticate_fn = auth_object
|
ctx.clone(),
|
||||||
.get(js_string!("is_authenticated"), self.0)
|
"auth",
|
||||||
.map_err(|e| anyhow!("JS error while accessing authenticate: {}", e))?
|
"logout",
|
||||||
.as_function()
|
&[]
|
||||||
.ok_or(anyhow!("authenticate is not a function"))?;
|
)
|
||||||
|
.await?
|
||||||
authenticate_fn
|
.expect("[hey][smartypants] auth.logout should return a void")
|
||||||
.call(&auth_val, &[], self.0)
|
)
|
||||||
.map_err(|e| anyhow!("{}", e))?
|
})
|
||||||
.as_boolean()
|
.await
|
||||||
.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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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::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 AsyncContext);
|
||||||
pub struct PluginBrowseEndpoint<'a>(&'a mut Context);
|
|
||||||
|
|
||||||
impl<'a> PluginBrowseEndpoint<'a> {
|
impl<'a> PluginBrowseEndpoint<'a> {
|
||||||
#[frb(ignore)]
|
#[frb(ignore)]
|
||||||
pub fn new(context: &'a mut Context) -> PluginBrowseEndpoint<'a> {
|
pub fn new(context: &'a AsyncContext) -> PluginBrowseEndpoint<'a> {
|
||||||
PluginBrowseEndpoint(context)
|
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(
|
pub async fn sections(
|
||||||
&mut self,
|
&self,
|
||||||
offset: Option<u32>,
|
offset: Option<u32>,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
||||||
let browse_val = self.browse_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let browse_object = browse_val.as_object().ok_or(anyhow!("Not an object"))?;
|
Ok(
|
||||||
|
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
|
||||||
let sections_fn = browse_object
|
ctx.clone(),
|
||||||
.get(js_string!("sections"), self.0)
|
"browse",
|
||||||
.map_err(|e| anyhow!("JS error while accessing sections: {}", e))?
|
"sections",
|
||||||
.as_function()
|
&[serde_json::to_value(offset)?, serde_json::to_value(limit)?]
|
||||||
.ok_or(anyhow!("sections is not a function"))?;
|
)
|
||||||
|
.await?
|
||||||
let args = [
|
.expect("[hey][smartypants] browse.sections should return a SpotifyPaginationResponseObject")
|
||||||
match offset {
|
)
|
||||||
Some(o) => JsValue::from(o),
|
}).await
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn section_items(
|
pub async fn section_items(
|
||||||
&mut self,
|
&self,
|
||||||
id: String,
|
id: String,
|
||||||
offset: Option<u32>,
|
offset: Option<u32>,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
||||||
let browse_val = self.browse_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let browse_object = browse_val.as_object().ok_or(anyhow!("Not an object"))?;
|
Ok(
|
||||||
|
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
|
||||||
let section_items_fn = browse_object
|
ctx.clone(),
|
||||||
.get(js_string!("sectionItems"), self.0)
|
"browse",
|
||||||
.map_err(|e| anyhow!("JS error while accessing sectionItems: {}", e))?
|
"sectionItems",
|
||||||
.as_function()
|
&[
|
||||||
.ok_or(anyhow!("sectionItems is not a function"))?;
|
serde_json::to_value(id)?,
|
||||||
|
serde_json::to_value(offset)?,
|
||||||
let args = [
|
serde_json::to_value(limit)?,
|
||||||
JsValue::from(js_string!(id)),
|
]
|
||||||
match offset {
|
)
|
||||||
Some(o) => JsValue::from(o),
|
.await?
|
||||||
None => JsValue::undefined(),
|
.expect("[hey][smartypants] browse.sectionItems should return a SpotifyPaginationResponseObject")
|
||||||
},
|
)
|
||||||
match limit {
|
}).await
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,85 +1,49 @@
|
|||||||
use crate::api::plugin::models::core::{PluginConfiguration, PluginUpdateAvailable, ScrobbleDetails};
|
use crate::api::plugin::models::core::{
|
||||||
use crate::internal::utils;
|
PluginConfiguration, PluginUpdateAvailable, ScrobbleDetails,
|
||||||
|
};
|
||||||
|
use crate::internal::utils::{js_invoke_async_method_to_json, js_invoke_method_to_json};
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use boa_engine::{js_string, Context, JsValue};
|
|
||||||
use flutter_rust_bridge::frb;
|
use flutter_rust_bridge::frb;
|
||||||
|
use rquickjs::{async_with, AsyncContext};
|
||||||
|
|
||||||
#[derive(Debug)]
|
pub struct PluginCoreEndpoint<'a>(&'a AsyncContext);
|
||||||
pub struct PluginCoreEndpoint<'a>(&'a mut Context);
|
|
||||||
|
|
||||||
impl<'a> PluginCoreEndpoint<'a> {
|
impl<'a> PluginCoreEndpoint<'a> {
|
||||||
#[frb(ignore)]
|
#[frb(ignore)]
|
||||||
pub fn new(context: &'a mut Context) -> PluginCoreEndpoint<'a> {
|
pub fn new(context: &'a AsyncContext) -> PluginCoreEndpoint<'a> {
|
||||||
PluginCoreEndpoint(context)
|
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(
|
pub async fn check_update(
|
||||||
&mut self,
|
&self,
|
||||||
plugin_config: PluginConfiguration,
|
plugin_config: PluginConfiguration,
|
||||||
) -> anyhow::Result<Option<PluginUpdateAvailable>> {
|
) -> anyhow::Result<Option<PluginUpdateAvailable>> {
|
||||||
let core_val = self.core_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let core_object = core_val.as_object().ok_or(anyhow!("Not an object"))?;
|
js_invoke_async_method_to_json(ctx.clone(), "core", "checkUpdate", &[plugin_config]).await
|
||||||
|
}).await
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn support(&mut self) -> anyhow::Result<String> {
|
pub async fn support(&self) -> anyhow::Result<String> {
|
||||||
let core_val = self.core_obj()?;
|
self.0
|
||||||
let core_object = core_val.as_object().ok_or(anyhow!("Not an object"))?;
|
.with(|ctx| {
|
||||||
|
anyhow::Ok(
|
||||||
let support_val = core_object
|
js_invoke_method_to_json::<String, String>(
|
||||||
.get(js_string!("support"), self.0)
|
ctx.clone(),
|
||||||
.map_err(|e| anyhow!("JS error while accessing support: {}", e))?;
|
"core",
|
||||||
|
"support",
|
||||||
support_val
|
&[],
|
||||||
.as_string()
|
)?
|
||||||
.ok_or(anyhow!("support is not a string"))?
|
.expect("[hey][smartypants] core.support should return a string"),
|
||||||
.to_std_string()
|
)
|
||||||
.map_err(|e| anyhow!("{}", e))
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn scrobble(&mut self, details: ScrobbleDetails) -> anyhow::Result<()> {
|
pub async fn scrobble(&self, details: ScrobbleDetails) -> anyhow::Result<()> {
|
||||||
let core_val = self.core_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let core_object = core_val.as_object().ok_or(anyhow!("Not an object"))?;
|
js_invoke_async_method_to_json::<_, ()>(ctx.clone(), "core", "scrobble", &[details]).await?;
|
||||||
|
anyhow::Ok(())
|
||||||
let scrobble_fn = core_object
|
})
|
||||||
.get(js_string!("scrobble"), self.0)
|
.await
|
||||||
.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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,134 +1,82 @@
|
|||||||
use crate::api::plugin::models::pagination::SpotubePaginationResponseObject;
|
use crate::api::plugin::models::pagination::SpotubePaginationResponseObject;
|
||||||
use crate::api::plugin::models::playlist::SpotubeFullPlaylistObject;
|
use crate::api::plugin::models::playlist::SpotubeFullPlaylistObject;
|
||||||
use crate::internal::utils;
|
use crate::internal::utils::js_invoke_async_method_to_json;
|
||||||
use anyhow::anyhow;
|
|
||||||
use boa_engine::{js_string, Context, JsValue};
|
|
||||||
use flutter_rust_bridge::frb;
|
use flutter_rust_bridge::frb;
|
||||||
|
use rquickjs::{async_with, AsyncContext};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
#[derive(Debug)]
|
pub struct PluginPlaylistEndpoint<'a>(&'a AsyncContext);
|
||||||
pub struct PluginPlaylistEndpoint<'a>(&'a mut Context);
|
|
||||||
|
|
||||||
impl<'a> PluginPlaylistEndpoint<'a> {
|
impl<'a> PluginPlaylistEndpoint<'a> {
|
||||||
#[frb(ignore)]
|
#[frb(ignore)]
|
||||||
pub fn new(context: &'a mut Context) -> PluginPlaylistEndpoint<'a> {
|
pub fn new(context: &'a AsyncContext) -> PluginPlaylistEndpoint<'a> {
|
||||||
PluginPlaylistEndpoint(context)
|
PluginPlaylistEndpoint(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn playlist_obj(&mut self) -> anyhow::Result<JsValue> {
|
pub async fn get_playlist(&self, id: String) -> anyhow::Result<SpotubeFullPlaylistObject> {
|
||||||
let global = self.0.global_object();
|
async_with!(self.0 => |ctx| {
|
||||||
|
Ok(
|
||||||
let plugin_instance = global
|
js_invoke_async_method_to_json(
|
||||||
.get(js_string!("pluginInstance"), self.0)
|
ctx.clone(),
|
||||||
.map_err(|e| anyhow!("{}", e))
|
"playlist",
|
||||||
.and_then(|a| a.as_object().ok_or(anyhow!("Not an object")))?;
|
"getPlaylist",
|
||||||
|
&[id]
|
||||||
plugin_instance
|
)
|
||||||
.get(js_string!("playlist"), self.0)
|
.await?
|
||||||
.or_else(|e| Err(anyhow!("playlist not found: \n{}", e)))
|
.expect("[hey][smartypants] playlist.getPlaylist should return a SpotifyFullPlaylistObject")
|
||||||
}
|
)
|
||||||
|
}).await
|
||||||
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 tracks(
|
pub async fn tracks(
|
||||||
&mut self,
|
&self,
|
||||||
|
|
||||||
id: String,
|
id: String,
|
||||||
offset: Option<u32>,
|
offset: Option<u32>,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
||||||
let playlist_val = self.playlist_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let playlist_object = playlist_val.as_object().ok_or(anyhow!("Not an object"))?;
|
Ok(
|
||||||
|
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
|
||||||
let tracks_fn = playlist_object
|
ctx.clone(),
|
||||||
.get(js_string!("tracks"), self.0)
|
"playlist",
|
||||||
.map_err(|e| anyhow!("{}", e))?
|
"tracks",
|
||||||
.as_function()
|
&[Value::String(id), serde_json::to_value(offset)?, serde_json::to_value(limit.unwrap())?]
|
||||||
.ok_or(anyhow!("tracks is not a function"))?;
|
)
|
||||||
|
.await?
|
||||||
let args: [JsValue; 3] = [
|
.expect("[hey][smartypants] artist.related should return a SpotifyPaginationResponseObject")
|
||||||
JsValue::from(js_string!(id)),
|
)
|
||||||
match offset {
|
}).await
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create(
|
pub async fn create(
|
||||||
&mut self,
|
&self,
|
||||||
|
|
||||||
user_id: String,
|
user_id: String,
|
||||||
name: String,
|
name: String,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
public: Option<bool>,
|
public: Option<bool>,
|
||||||
collaborative: Option<bool>,
|
collaborative: Option<bool>,
|
||||||
) -> anyhow::Result<Option<SpotubeFullPlaylistObject>> {
|
) -> anyhow::Result<Option<SpotubeFullPlaylistObject>> {
|
||||||
let playlist_val = self.playlist_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let playlist_object = playlist_val.as_object().ok_or(anyhow!("Not an object"))?;
|
js_invoke_async_method_to_json(
|
||||||
|
ctx.clone(),
|
||||||
let create_fn = playlist_object
|
"playlist",
|
||||||
.get(js_string!("create"), self.0)
|
"create",
|
||||||
.map_err(|e| anyhow!("{}", e))?
|
&[
|
||||||
.as_function()
|
Value::String(user_id),
|
||||||
.ok_or(anyhow!("create is not a function"))?;
|
Value::String(name),
|
||||||
|
Value::String(description.unwrap_or_default()),
|
||||||
let args = [
|
serde_json::to_value(public.unwrap_or_default())?,
|
||||||
JsValue::from(js_string!(user_id)),
|
serde_json::to_value(collaborative.unwrap_or_default())?,
|
||||||
JsValue::from(js_string!(name)),
|
]
|
||||||
match description {
|
)
|
||||||
Some(o) => JsValue::from(js_string!(o)),
|
.await
|
||||||
None => JsValue::undefined(),
|
})
|
||||||
},
|
.await
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update(
|
pub async fn update(
|
||||||
&mut self,
|
&self,
|
||||||
|
|
||||||
playlist_id: String,
|
playlist_id: String,
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
@ -136,135 +84,104 @@ impl<'a> PluginPlaylistEndpoint<'a> {
|
|||||||
public: Option<bool>,
|
public: Option<bool>,
|
||||||
collaborative: Option<bool>,
|
collaborative: Option<bool>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let playlist_val = self.playlist_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let playlist_object = playlist_val.as_object().ok_or(anyhow!("Not an object"))?;
|
js_invoke_async_method_to_json::<_, ()>(
|
||||||
|
ctx.clone(),
|
||||||
let update_fn = playlist_object
|
"playlist",
|
||||||
.get(js_string!("update"), self.0)
|
"update",
|
||||||
.map_err(|e| anyhow!("{}", e))?
|
&[
|
||||||
.as_function()
|
Value::String(playlist_id),
|
||||||
.ok_or(anyhow!("update is not a function"))?;
|
serde_json::to_value(name)?,
|
||||||
|
Value::String(description.unwrap_or_default()),
|
||||||
let args = [
|
serde_json::to_value(public.unwrap_or_default())?,
|
||||||
JsValue::from(js_string!(playlist_id)),
|
serde_json::to_value(collaborative.unwrap_or_default())?,
|
||||||
match name {
|
]
|
||||||
Some(o) => JsValue::from(js_string!(o)),
|
)
|
||||||
None => JsValue::undefined(),
|
.await.and_then(|_| Ok(()))
|
||||||
},
|
})
|
||||||
match description {
|
.await
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn add_tracks(
|
pub async fn add_tracks(
|
||||||
&mut self,
|
&self,
|
||||||
|
|
||||||
playlist_id: String,
|
playlist_id: String,
|
||||||
track_ids: Vec<String>,
|
track_ids: Vec<String>,
|
||||||
position: Option<u32>,
|
position: Option<u32>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let playlist_val = self.playlist_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let playlist_object = playlist_val.as_object().ok_or(anyhow!("Not an object"))?;
|
js_invoke_async_method_to_json::<_, ()>(
|
||||||
|
ctx.clone(),
|
||||||
let add_tracks_fn = playlist_object
|
"playlist",
|
||||||
.get(js_string!("addTracks"), self.0)
|
"addTracks",
|
||||||
.map_err(|e| anyhow!("{}", e))?
|
&[
|
||||||
.as_function()
|
Value::String(playlist_id),
|
||||||
.ok_or(anyhow!("addTracks is not a function"))?;
|
Value::Array(track_ids.into_iter().map(|id| Value::String(id)).collect()),
|
||||||
|
serde_json::to_value(position)?,
|
||||||
let args = [
|
]
|
||||||
JsValue::from(js_string!(playlist_id)),
|
)
|
||||||
utils::vec_string_to_js_array(track_ids, self.0)?,
|
.await.and_then(|_| Ok(()))
|
||||||
match position {
|
})
|
||||||
Some(o) => JsValue::from(o),
|
.await
|
||||||
None => JsValue::undefined(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
utils::js_call_to_void(add_tracks_fn.call(&playlist_val, &args, self.0), self.0).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn remove_tracks(
|
pub async fn remove_tracks(
|
||||||
&mut self,
|
&self,
|
||||||
|
|
||||||
playlist_id: String,
|
playlist_id: String,
|
||||||
track_ids: Vec<String>,
|
track_ids: Vec<String>,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let playlist_val = self.playlist_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let playlist_object = playlist_val.as_object().ok_or(anyhow!("Not an object"))?;
|
js_invoke_async_method_to_json::<_, ()>(
|
||||||
|
ctx.clone(),
|
||||||
let remove_tracks_fn = playlist_object
|
"playlist",
|
||||||
.get(js_string!("removeTracks"), self.0)
|
"removeTracks",
|
||||||
.map_err(|e| anyhow!("{}", e))?
|
&[
|
||||||
.as_function()
|
Value::String(playlist_id),
|
||||||
.ok_or(anyhow!("removeTracks is not a function"))?;
|
Value::Array(track_ids.into_iter().map(|id| Value::String(id)).collect()),
|
||||||
|
]
|
||||||
let args = [
|
)
|
||||||
JsValue::from(js_string!(playlist_id)),
|
.await.and_then(|_| Ok(()))
|
||||||
utils::vec_string_to_js_array(track_ids, self.0)?,
|
})
|
||||||
];
|
.await
|
||||||
|
|
||||||
utils::js_call_to_void(remove_tracks_fn.call(&playlist_val, &args, self.0), self.0).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn save(&mut self, playlist_id: String) -> anyhow::Result<()> {
|
pub async fn save(&self, playlist_id: String) -> anyhow::Result<()> {
|
||||||
let playlist_val = self.playlist_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let playlist_object = playlist_val.as_object().ok_or(anyhow!("Not an object"))?;
|
js_invoke_async_method_to_json::<_, ()>(
|
||||||
|
ctx.clone(),
|
||||||
let save_fn = playlist_object
|
"playlist",
|
||||||
.get(js_string!("save"), self.0)
|
"save",
|
||||||
.map_err(|e| anyhow!("{}", e))?
|
&[Value::String(playlist_id)]
|
||||||
.as_function()
|
)
|
||||||
.ok_or(anyhow!("save is not a function"))?;
|
.await.and_then(|_| Ok(()))
|
||||||
|
})
|
||||||
let args = [JsValue::from(js_string!(playlist_id))];
|
.await
|
||||||
|
|
||||||
utils::js_call_to_void(save_fn.call(&playlist_val, &args, self.0), self.0).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unsave(&mut self, playlist_id: String) -> anyhow::Result<()> {
|
pub async fn unsave(&self, playlist_id: String) -> anyhow::Result<()> {
|
||||||
let playlist_val = self.playlist_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let playlist_object = playlist_val.as_object().ok_or(anyhow!("Not an object"))?;
|
js_invoke_async_method_to_json::<_, ()>(
|
||||||
|
ctx.clone(),
|
||||||
let unsave_fn = playlist_object
|
"playlist",
|
||||||
.get(js_string!("unsave"), self.0)
|
"unsave",
|
||||||
.map_err(|e| anyhow!("{}", e))?
|
&[Value::String(playlist_id)]
|
||||||
.as_function()
|
)
|
||||||
.ok_or(anyhow!("unsave is not a function"))?;
|
.await.and_then(|_| Ok(()))
|
||||||
|
})
|
||||||
let args = [JsValue::from(js_string!(playlist_id))];
|
.await
|
||||||
|
|
||||||
utils::js_call_to_void(unsave_fn.call(&playlist_val, &args, self.0), self.0).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_playlist(&mut self, playlist_id: String) -> anyhow::Result<()> {
|
pub async fn delete_playlist(&self, playlist_id: String) -> anyhow::Result<()> {
|
||||||
let playlist_val = self.playlist_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let playlist_object = playlist_val.as_object().ok_or(anyhow!("Not an object"))?;
|
js_invoke_async_method_to_json::<_, ()>(
|
||||||
|
ctx.clone(),
|
||||||
let delete_playlist_fn = playlist_object
|
"playlist",
|
||||||
.get(js_string!("deletePlaylist"), self.0)
|
"deletePlaylist",
|
||||||
.map_err(|e| anyhow!("{}", e))?
|
&[Value::String(playlist_id)]
|
||||||
.as_function()
|
)
|
||||||
.ok_or(anyhow!("deletePlaylist is not a function"))?;
|
.await.and_then(|_| Ok(()))
|
||||||
|
})
|
||||||
let args = [JsValue::from(js_string!(playlist_id))];
|
|
||||||
|
|
||||||
utils::js_call_to_void(
|
|
||||||
delete_playlist_fn.call(&playlist_val, &args, self.0),
|
|
||||||
self.0,
|
|
||||||
)
|
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,201 +1,149 @@
|
|||||||
use crate::api::plugin::models::pagination::SpotubePaginationResponseObject;
|
use crate::api::plugin::models::pagination::SpotubePaginationResponseObject;
|
||||||
use crate::api::plugin::models::search::SpotubeSearchResponseObject;
|
use crate::api::plugin::models::search::SpotubeSearchResponseObject;
|
||||||
use crate::internal::utils;
|
use crate::internal::utils::{js_invoke_async_method_to_json, js_invoke_method_to_json};
|
||||||
use anyhow::anyhow;
|
|
||||||
use boa_engine::object::builtins::JsArray;
|
|
||||||
use boa_engine::{js_string, Context, JsValue};
|
|
||||||
use flutter_rust_bridge::frb;
|
use flutter_rust_bridge::frb;
|
||||||
|
use rquickjs::{async_with, AsyncContext};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
#[derive(Debug)]
|
pub struct PluginSearchEndpoint<'a>(&'a AsyncContext);
|
||||||
pub struct PluginSearchEndpoint<'a>(&'a mut Context);
|
|
||||||
|
|
||||||
impl<'a> PluginSearchEndpoint<'a> {
|
impl<'a> PluginSearchEndpoint<'a> {
|
||||||
#[frb(ignore)]
|
#[frb(ignore)]
|
||||||
pub fn new(context: &'a mut Context) -> PluginSearchEndpoint<'a> {
|
pub fn new(context: &'a AsyncContext) -> PluginSearchEndpoint<'a> {
|
||||||
PluginSearchEndpoint(context)
|
PluginSearchEndpoint(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn search_obj(&mut self) -> anyhow::Result<JsValue> {
|
|
||||||
let global = self.0.global_object();
|
|
||||||
|
|
||||||
let plugin_instance = global
|
pub async fn chips(&self) -> anyhow::Result<Vec<String>> {
|
||||||
.get(js_string!("pluginInstance"), self.0)
|
self.0
|
||||||
.map_err(|e| anyhow!("{}", e))
|
.with(|ctx| {
|
||||||
.and_then(|a| a.as_object().ok_or(anyhow!("Not an object")))?;
|
anyhow::Ok(
|
||||||
|
js_invoke_method_to_json::<(), Vec<String>>(
|
||||||
plugin_instance
|
ctx.clone(),
|
||||||
.get(js_string!("search"), self.0)
|
"search",
|
||||||
.or_else(|e| Err(anyhow!("search not found: \n{}", e)))
|
"chips",
|
||||||
|
&[],
|
||||||
|
)?
|
||||||
|
.expect("[hey][smartypants] search.chips should return a string"),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn chips(&mut self) -> anyhow::Result<Vec<String>> {
|
pub async fn all(&self, query: String) -> anyhow::Result<SpotubeSearchResponseObject> {
|
||||||
let search_val = self.search_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let search_object = search_val.as_object().ok_or(anyhow!("Not an object"))?;
|
Ok(
|
||||||
|
js_invoke_async_method_to_json::<_, SpotubeSearchResponseObject>(
|
||||||
let chips_val = search_object
|
ctx.clone(),
|
||||||
.get(js_string!("chips"), self.0)
|
"search",
|
||||||
.map_err(|e| anyhow!("{}", e))?;
|
"all",
|
||||||
let chips_obj = chips_val.as_object().ok_or(anyhow!("Not an object"))?;
|
&[query],
|
||||||
|
)
|
||||||
if !chips_obj.is_array() {
|
.await?
|
||||||
return Err(anyhow!("chips is not an array"));
|
.expect("[hey][smartypants] search.all should return a SpotifySearchResponseObject")
|
||||||
}
|
)
|
||||||
|
})
|
||||||
let chips_array = JsArray::from_object(chips_obj.clone()).map_err(|e| anyhow!("{}", e))?;
|
.await
|
||||||
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 albums(
|
pub async fn albums(
|
||||||
&mut self,
|
&self,
|
||||||
query: String,
|
query: String,
|
||||||
offset: Option<u32>,
|
offset: Option<u32>,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
||||||
let search_val = self.search_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let search_object = search_val.as_object().ok_or(anyhow!("Not an object"))?;
|
Ok(
|
||||||
|
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
|
||||||
let albums_fn = search_object
|
ctx.clone(),
|
||||||
.get(js_string!("albums"), self.0)
|
"search",
|
||||||
.map_err(|e| anyhow!("{}", e))?
|
"albums",
|
||||||
.as_function()
|
&[
|
||||||
.ok_or(anyhow!("albums is not a function"))?;
|
Value::String(query),
|
||||||
|
serde_json::to_value(offset)?,
|
||||||
let args: [JsValue; 3] = [
|
serde_json::to_value(limit)?
|
||||||
JsValue::from(js_string!(query)),
|
],
|
||||||
match offset {
|
)
|
||||||
Some(o) => JsValue::from(o),
|
.await?
|
||||||
None => JsValue::undefined(),
|
.expect("[hey][smartypants] search.albums should return a SpotifyPaginationResponseObject")
|
||||||
},
|
)
|
||||||
match limit {
|
})
|
||||||
Some(o) => JsValue::from(o),
|
.await
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
pub async fn artists(
|
pub async fn artists(
|
||||||
&mut self,
|
&self,
|
||||||
query: String,
|
query: String,
|
||||||
offset: Option<u32>,
|
offset: Option<u32>,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
||||||
let search_val = self.search_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let search_object = search_val.as_object().ok_or(anyhow!("Not an object"))?;
|
Ok(
|
||||||
|
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
|
||||||
let artists_fn = search_object
|
ctx.clone(),
|
||||||
.get(js_string!("artists"), self.0)
|
"search",
|
||||||
.map_err(|e| anyhow!("{}", e))?
|
"artists",
|
||||||
.as_function()
|
&[
|
||||||
.ok_or(anyhow!("artists is not a function"))?;
|
Value::String(query),
|
||||||
|
serde_json::to_value(offset)?,
|
||||||
let args: [JsValue; 3] = [
|
serde_json::to_value(limit)?
|
||||||
JsValue::from(js_string!(query)),
|
],
|
||||||
match offset {
|
)
|
||||||
Some(o) => JsValue::from(o),
|
.await?
|
||||||
None => JsValue::undefined(),
|
.expect("[hey][smartypants] search.artists should return a SpotifyPaginationResponseObject")
|
||||||
},
|
)
|
||||||
match limit {
|
})
|
||||||
Some(o) => JsValue::from(o),
|
.await
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn playlists(
|
pub async fn playlists(
|
||||||
&mut self,
|
&self,
|
||||||
query: String,
|
query: String,
|
||||||
offset: Option<u32>,
|
offset: Option<u32>,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
||||||
let search_val = self.search_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let search_object = search_val.as_object().ok_or(anyhow!("Not an object"))?;
|
Ok(
|
||||||
|
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
|
||||||
let playlists_fn = search_object
|
ctx.clone(),
|
||||||
.get(js_string!("playlists"), self.0)
|
"search",
|
||||||
.map_err(|e| anyhow!("{}", e))?
|
"playlists",
|
||||||
.as_function()
|
&[
|
||||||
.ok_or(anyhow!("playlists is not a function"))?;
|
Value::String(query),
|
||||||
|
serde_json::to_value(offset)?,
|
||||||
let args: [JsValue; 3] = [
|
serde_json::to_value(limit)?
|
||||||
JsValue::from(js_string!(query)),
|
],
|
||||||
match offset {
|
)
|
||||||
Some(o) => JsValue::from(o),
|
.await?
|
||||||
None => JsValue::undefined(),
|
.expect("[hey][smartypants] search.playlists should return a SpotifyPaginationResponseObject")
|
||||||
},
|
)
|
||||||
match limit {
|
})
|
||||||
Some(o) => JsValue::from(o),
|
.await
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn tracks(
|
pub async fn tracks(
|
||||||
&mut self,
|
&self,
|
||||||
query: String,
|
query: String,
|
||||||
offset: Option<u32>,
|
offset: Option<u32>,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
||||||
let search_val = self.search_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let search_object = search_val.as_object().ok_or(anyhow!("Not an object"))?;
|
Ok(
|
||||||
|
js_invoke_async_method_to_json::<_, SpotubePaginationResponseObject>(
|
||||||
let tracks_fn = search_object
|
ctx.clone(),
|
||||||
.get(js_string!("tracks"), self.0)
|
"search",
|
||||||
.map_err(|e| anyhow!("{}", e))?
|
"tracks",
|
||||||
.as_function()
|
&[
|
||||||
.ok_or(anyhow!("tracks is not a function"))?;
|
Value::String(query),
|
||||||
|
serde_json::to_value(offset)?,
|
||||||
let args: [JsValue; 3] = [
|
serde_json::to_value(limit)?
|
||||||
JsValue::from(js_string!(query)),
|
],
|
||||||
match offset {
|
)
|
||||||
Some(o) => JsValue::from(o),
|
.await?
|
||||||
None => JsValue::undefined(),
|
.expect("[hey][smartypants] search.tracks should return a SpotifyPaginationResponseObject")
|
||||||
},
|
)
|
||||||
match limit {
|
})
|
||||||
Some(o) => JsValue::from(o),
|
.await
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,96 +1,77 @@
|
|||||||
use crate::api::plugin::models::track::SpotubeTrackObject;
|
use crate::api::plugin::models::track::SpotubeTrackObject;
|
||||||
use crate::internal::utils;
|
use crate::internal::utils::js_invoke_async_method_to_json;
|
||||||
use anyhow::anyhow;
|
|
||||||
use boa_engine::{js_string, Context, JsValue};
|
|
||||||
use flutter_rust_bridge::frb;
|
use flutter_rust_bridge::frb;
|
||||||
|
use rquickjs::{async_with, AsyncContext};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
#[derive(Debug)]
|
pub struct PluginTrackEndpoint<'a>(&'a AsyncContext);
|
||||||
pub struct PluginTrackEndpoint<'a>(&'a mut Context);
|
|
||||||
|
|
||||||
impl<'a> PluginTrackEndpoint<'a> {
|
impl<'a> PluginTrackEndpoint<'a> {
|
||||||
#[frb(ignore)]
|
#[frb(ignore)]
|
||||||
pub fn new(context: &'a mut Context) -> PluginTrackEndpoint<'a> {
|
pub fn new(context: &'a AsyncContext) -> PluginTrackEndpoint<'a> {
|
||||||
PluginTrackEndpoint(context)
|
PluginTrackEndpoint(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn track_obj(&mut self) -> anyhow::Result<JsValue> {
|
pub async fn get_track(&self, id: String) -> anyhow::Result<SpotubeTrackObject> {
|
||||||
let global = self.0.global_object();
|
async_with!(self.0 => |ctx| {
|
||||||
|
Ok(
|
||||||
let plugin_instance = global
|
js_invoke_async_method_to_json(
|
||||||
.get(js_string!("pluginInstance"), self.0)
|
ctx.clone(),
|
||||||
.map_err(|e| anyhow!("{}", e))
|
"track",
|
||||||
.and_then(|a| a.as_object().ok_or(anyhow!("Not an object")))?;
|
"getTrack",
|
||||||
|
&[
|
||||||
plugin_instance
|
id
|
||||||
.get(js_string!("track"), self.0)
|
],
|
||||||
.or_else(|e| Err(anyhow!("track not found: \n{}", e)))
|
)
|
||||||
|
.await?
|
||||||
|
.expect("[hey][smartypants] track.getTrack should return a SpotifyTrackObject")
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_track(&mut self, id: String) -> anyhow::Result<SpotubeTrackObject> {
|
pub async fn save(&self, ids: Vec<String>) -> anyhow::Result<()> {
|
||||||
let track_val = self.track_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let track_object = track_val.as_object().ok_or(anyhow!("Not an object"))?;
|
Ok(
|
||||||
|
js_invoke_async_method_to_json::<_, ()>(
|
||||||
let get_track_fn = track_object
|
ctx.clone(),
|
||||||
.get(js_string!("getTrack"), self.0)
|
"track",
|
||||||
.map_err(|e| anyhow!("JS error while accessing getTrack: {}", e))?
|
"save",
|
||||||
.as_function()
|
&[Value::Array(ids.into_iter().map(|id| Value::String(id)).collect())]
|
||||||
.ok_or(anyhow!("getTrack is not a function"))?;
|
)
|
||||||
|
.await?
|
||||||
let args = [JsValue::from(js_string!(id))];
|
.expect("[hey][smartypants] track.save should return a SpotifyPaginationResponseObject")
|
||||||
|
)
|
||||||
let res_json =
|
}).await
|
||||||
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(&mut self, ids: Vec<String>) -> anyhow::Result<()> {
|
pub async fn unsave(&self, ids: Vec<String>) -> anyhow::Result<()> {
|
||||||
let track_val = self.track_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let track_object = track_val.as_object().ok_or(anyhow!("Not an object"))?;
|
Ok(
|
||||||
|
js_invoke_async_method_to_json::<_, ()>(
|
||||||
let save_fn = track_object
|
ctx.clone(),
|
||||||
.get(js_string!("save"), self.0)
|
"track",
|
||||||
.map_err(|e| anyhow!("JS error while accessing save: {}", e))?
|
"unsave",
|
||||||
.as_function()
|
&[Value::Array(ids.into_iter().map(|id| Value::String(id)).collect())]
|
||||||
.ok_or(anyhow!("save is not a function"))?;
|
)
|
||||||
|
.await?
|
||||||
let ids_val = utils::vec_string_to_js_array(ids, self.0)?;
|
.expect("[hey][smartypants] track.unsave should return a SpotifyPaginationResponseObject")
|
||||||
let args = [ids_val.into()];
|
)
|
||||||
|
}).await
|
||||||
utils::js_call_to_void(save_fn.call(&track_val, &args, self.0), self.0).await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn unsave(&mut self, ids: Vec<String>) -> anyhow::Result<()> {
|
pub async fn radio(&self, id: String) -> anyhow::Result<Vec<SpotubeTrackObject>> {
|
||||||
let track_val = self.track_obj()?;
|
async_with!(self.0 => |ctx| {
|
||||||
let track_object = track_val.as_object().ok_or(anyhow!("Not an object"))?;
|
Ok(
|
||||||
|
js_invoke_async_method_to_json::<_, Vec<SpotubeTrackObject>>(
|
||||||
let unsave_fn = track_object
|
ctx.clone(),
|
||||||
.get(js_string!("unsave"), self.0)
|
"track",
|
||||||
.map_err(|e| anyhow!("JS error while accessing save: {}", e))?
|
"radio",
|
||||||
.as_function()
|
&[id],
|
||||||
.ok_or(anyhow!("save is not a function"))?;
|
)
|
||||||
|
.await?
|
||||||
let ids_val = utils::vec_string_to_js_array(ids, self.0)?;
|
.expect("[hey][smartypants] track.radio should return a SpotifyPaginationResponseObject")
|
||||||
let args = [ids_val.into()];
|
)
|
||||||
|
}).await
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,107 +1,106 @@
|
|||||||
use crate::api::plugin::models::pagination::SpotubePaginationResponseObject;
|
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::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 AsyncContext);
|
||||||
pub struct PluginUserEndpoint<'a>(&'a mut Context);
|
|
||||||
|
|
||||||
impl<'a> PluginUserEndpoint<'a> {
|
impl<'a> PluginUserEndpoint<'a> {
|
||||||
#[frb(ignore)]
|
#[frb(ignore)]
|
||||||
pub fn new(context: &'a mut Context) -> PluginUserEndpoint<'a> {
|
pub fn new(context: &'a AsyncContext) -> PluginUserEndpoint<'a> {
|
||||||
PluginUserEndpoint(context)
|
PluginUserEndpoint(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn user_obj(&mut self) -> anyhow::Result<JsValue> {
|
pub async fn me(&self) -> anyhow::Result<SpotubeUserObject> {
|
||||||
let global = self.0.global_object();
|
async_with!(self.0 => |ctx| {
|
||||||
|
Ok(
|
||||||
let plugin_instance = global
|
js_invoke_async_method_to_json::<(), SpotubeUserObject>(
|
||||||
.get(js_string!("pluginInstance"), self.0)
|
ctx.clone(),
|
||||||
.map_err(|e| anyhow!("{}", e))
|
"user",
|
||||||
.and_then(|a| a.as_object().ok_or(anyhow!("Not an object")))?;
|
"me",
|
||||||
|
&[],
|
||||||
plugin_instance
|
)
|
||||||
.get(js_string!("user"), self.0)
|
.await?
|
||||||
.or_else(|e| Err(anyhow!("user not found: \n{}", e)))
|
.expect("[hey][smartypants] user.me should return a SpotifyUserObject")
|
||||||
}
|
)
|
||||||
|
})
|
||||||
pub async fn me(&mut self) -> anyhow::Result<SpotubeUserObject> {
|
.await
|
||||||
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 saved_playlists(
|
pub async fn saved_playlists(
|
||||||
&mut self,
|
&self,
|
||||||
offset: Option<u32>,
|
offset: Option<u32>,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
) -> 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(
|
pub async fn saved_tracks(
|
||||||
&mut self,
|
&self,
|
||||||
offset: Option<u32>,
|
offset: Option<u32>,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
) -> 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(
|
pub async fn saved_albums(
|
||||||
&mut self,
|
&self,
|
||||||
offset: Option<u32>,
|
offset: Option<u32>,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
) -> 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(
|
pub async fn saved_artists(
|
||||||
&mut self,
|
&self,
|
||||||
offset: Option<u32>,
|
offset: Option<u32>,
|
||||||
limit: Option<u32>,
|
limit: Option<u32>,
|
||||||
) -> anyhow::Result<SpotubePaginationResponseObject> {
|
) -> 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 anyhow::anyhow;
|
||||||
use boa_engine::property::PropertyKey;
|
use rquickjs::function::Args;
|
||||||
use boa_engine::{object::builtins::JsArray, Context, JsObject, JsResult, JsString, JsValue};
|
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};
|
use serde_json::{Map, Value};
|
||||||
|
use std::collections::HashMap;
|
||||||
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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a `serde_json::Value` into a Boa `JsValue`
|
/// 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 {
|
match value {
|
||||||
Value::Null => Ok(JsValue::null()),
|
Value::Null => Ok(rquickjs::Value::new_null(ctx)),
|
||||||
Value::Bool(b) => Ok(JsValue::from(*b)),
|
Value::Bool(b) => Ok(rquickjs::Value::new_bool(ctx, *b)),
|
||||||
Value::Number(n) => {
|
Value::Number(n) => {
|
||||||
if let Some(i) = n.as_i64() {
|
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() {
|
} else if let Some(f) = n.as_f64() {
|
||||||
Ok(JsValue::new(f))
|
Ok(rquickjs::Value::new_float(ctx, f))
|
||||||
} else {
|
} 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) => {
|
Value::Array(arr) => {
|
||||||
let js_arr = JsArray::new(ctx);
|
let mut js_arr = Vec::<rquickjs::Value>::with_capacity(arr.len());
|
||||||
for (idx, item) in arr.iter().enumerate() {
|
for item in arr.iter() {
|
||||||
let js_val = json_value_to_js(item, ctx)?;
|
let js_val = json_value_to_js(item, ctx.clone())?;
|
||||||
js_arr.set(idx, js_val, false, ctx)?;
|
js_arr.push(js_val);
|
||||||
}
|
}
|
||||||
Ok(JsValue::from(js_arr))
|
js_arr.into_js(&ctx).map_err(|e| anyhow!(e))
|
||||||
}
|
}
|
||||||
|
|
||||||
Value::Object(obj) => {
|
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 {
|
for (key, val) in obj {
|
||||||
let js_val = json_value_to_js(val, ctx)?;
|
let js_val = json_value_to_js(val, ctx.clone())?;
|
||||||
js_obj.set(JsString::from(key.as_str()), js_val, true, ctx)?;
|
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`
|
/// 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() {
|
if value.is_null() || value.is_undefined() {
|
||||||
return Ok(Value::Null);
|
return Ok(Value::Null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(b) = value.as_boolean() {
|
if let Some(b) = value.as_bool() {
|
||||||
return Ok(Value::Bool(b));
|
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() {
|
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));
|
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() {
|
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?
|
// Array?
|
||||||
if obj.is_array() {
|
if obj.is_array() {
|
||||||
let obj = JsArray::from_object(obj).map_err(|e| anyhow!("{}", e))?;
|
let obj: Array = Array::from_value(obj.into_value()).map_err(|e| anyhow!("{}", e))?;
|
||||||
let length = obj.length(ctx).map_err(|e| anyhow!("{}", e))?;
|
let length = obj.len();
|
||||||
let mut json_arr = Vec::<Value>::with_capacity(length as usize);
|
let mut json_arr = Vec::<Value>::with_capacity(length);
|
||||||
|
|
||||||
for i in 0..length {
|
for i in 0..length {
|
||||||
let item = obj.get(i, ctx).unwrap_or(JsValue::null());
|
let item = obj.get(i).unwrap_or(rquickjs::Value::new_null(ctx.clone()));
|
||||||
let item_json = js_value_to_json(&item, ctx)?;
|
let item_json = js_value_to_json(item, ctx.clone())?;
|
||||||
json_arr.push(item_json);
|
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
|
// Regular Object
|
||||||
let mut map = Map::<String, Value>::new();
|
let mut map = Map::<String, Value>::new();
|
||||||
|
|
||||||
for key in obj.own_property_keys(ctx).map_err(|e| anyhow!("{}", e))? {
|
for key in obj.own_keys::<rquickjs::String>(Filter::default()) {
|
||||||
let key_val: Option<String> = match key.clone() {
|
let key = key?;
|
||||||
PropertyKey::String(s) => Some(s.to_std_string().map_err(|e| anyhow!("{}", e))?),
|
let v_js = obj
|
||||||
PropertyKey::Index(i) => Some(serde_json::Number::from(i.get()).to_string()),
|
.get(key.clone())
|
||||||
_ => None,
|
.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());
|
map.insert(key.clone().to_string()?, v_json);
|
||||||
let v_json = js_value_to_json(&v_js, ctx)?;
|
|
||||||
|
|
||||||
if let Some(key_val) = key_val {
|
|
||||||
map.insert(key_val, v_json);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(Value::Object(map));
|
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.
|
// Fallback for unsupported JS types: functions, symbols, etc.
|
||||||
Ok(Value::Null)
|
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;
|
mod api;
|
||||||
use rquickjs::prelude::Func;
|
mod internal;
|
||||||
use rquickjs::{
|
|
||||||
async_with, AsyncContext, AsyncRuntime, CatchResultExt, CaughtError, Function, Object, Promise,
|
use rquickjs::function::{Async, Func};
|
||||||
Result,
|
use rquickjs::{async_with, AsyncContext, AsyncRuntime, Function, Object, Promise};
|
||||||
};
|
use tokio::time::Instant;
|
||||||
use std::time::Duration;
|
|
||||||
|
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) {
|
fn print(msg: String) {
|
||||||
println!("{}", msg);
|
println!("{}", msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn set_timeout<'js>(cb: Function<'js>, number: f64) -> Result<()> {
|
async fn non_plugin() -> anyhow::Result<()> {
|
||||||
tokio::time::sleep(Duration::from_millis(number as u64)).await;
|
let start = Instant::now();
|
||||||
cb.call::<_, ()>(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() -> Result<()> {
|
|
||||||
let rt = AsyncRuntime::new()?;
|
let rt = AsyncRuntime::new()?;
|
||||||
let ctx = AsyncContext::full(&rt).await?;
|
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")
|
Function::new(ctx.clone(), Async(set_timeout)).unwrap().with_name("setTimeout")
|
||||||
).unwrap();
|
).unwrap();
|
||||||
|
|
||||||
if let Ok(function) = ctx.eval::<Function, _>(r#"
|
let check_update_fn: Function = ctx.eval(r#"
|
||||||
(function(){
|
function sleep(ms) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
(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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
non_plugin().await?;
|
||||||
|
plugin().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user