moosync_edk/
api.rs

1// Moosync
2// Copyright (C) 2024, 2025  Moosync <support@moosync.app>
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <http://www.gnu.org/licenses/>.
16
17use extensions_proto::struct_proto::google::protobuf::Struct as ProtoStruct;
18use extism_pdk::{Prost, host_fn};
19
20pub use extensions_proto::moosync::types::{
21    AddPlaylistRequest, AddSongsRequest, AddToPlaylistRequest, ContextMenuActionRequest,
22    ContextMenuReturnType, CustomRequest, ExtensionAccountDetail, ExtensionProviderScope,
23    GetCurrentSongRequest, GetEntityRequest, GetPlayerStateRequest, GetPreferenceRequest,
24    GetQueueRequest, GetSecureRequest, GetSongRequest, GetTimeRequest, GetVolumeRequest,
25    MainCommand, MainCommandResponse, OauthCallbackRequest, OpenExternalUrlRequest,
26    PerformAccountLoginRequest, PlaybackDetailsRequestedRequest, PlayerState,
27    PlayerStateChangedRequest, PlaylistAddedRequest, PlaylistRemovedRequest,
28    PreferenceChangedRequest, PreferenceData, RegisterOauthRequest, RegisterUserPreferenceRequest,
29    RemoveSongRequest, RequestedAlbumSongsRequest, RequestedArtistSongsRequest,
30    RequestedLyricsRequest, RequestedPlaylistContextMenuRequest, RequestedPlaylistFromUrlRequest,
31    RequestedPlaylistSongsRequest, RequestedPlaylistsRequest, RequestedRecommendationsRequest,
32    RequestedSearchResultRequest, RequestedSongContextMenuRequest, RequestedSongFromIdRequest,
33    RequestedSongFromUrlRequest, ScrobbleRequest, SeekedRequest, SetAccountRequest,
34    SetPreferenceRequest, SetSecureRequest, SongAddedRequest, SongChangedRequest,
35    SongQueueChangedRequest, SongRemovedRequest, UnregisterUserPreferenceRequest, UpdateSongRequest,
36    VolumeChangedRequest,
37};
38use preferences_proto::moosync::types::PreferenceItem;
39use songs_proto::moosync::types::{EntityResult, Lyrics, Playlist, SearchResult, Song};
40
41pub type MoosyncResult<T> = Result<T, crate::handler::MoosyncError>;
42pub type AccountLoginArgs = PerformAccountLoginRequest;
43
44#[allow(unused_variables)]
45/// Trait for handling account-related events.
46pub trait Accounts {
47    /// Called when the main app requests to perform an account login.
48    fn perform_account_login(&self, req: PerformAccountLoginRequest) -> MoosyncResult<String> {
49        Err("Not implemented".into())
50    }
51
52    /// Called when the main app provides an OAuth callback code.
53    fn oauth_callback(&self, req: OauthCallbackRequest) -> MoosyncResult<()> {
54        Err("Not implemented".into())
55    }
56}
57
58#[allow(unused_variables)]
59/// Trait for handling database-related events.
60pub trait DatabaseEvents {
61    /// Called when a song is added to the database.
62    fn on_song_added(&self, req: SongAddedRequest) -> MoosyncResult<()> {
63        Err("Not implemented".into())
64    }
65
66    /// Called when a song is removed from the database.
67    fn on_song_removed(&self, req: SongRemovedRequest) -> MoosyncResult<()> {
68        Err("Not implemented".into())
69    }
70
71    /// Called when a playlist is added to the database.
72    fn on_playlist_added(&self, req: PlaylistAddedRequest) -> MoosyncResult<()> {
73        Err("Not implemented".into())
74    }
75
76    /// Called when a playlist is removed from the database.
77    fn on_playlist_removed(&self, req: PlaylistRemovedRequest) -> MoosyncResult<()> {
78        Err("Not implemented".into())
79    }
80}
81
82#[allow(unused_variables)]
83/// Trait for handling preference-related events.
84pub trait PreferenceEvents {
85    /// Called when preferences are changed.
86    fn on_preferences_changed(&self, req: PreferenceChangedRequest) -> MoosyncResult<()> {
87        Err("Not implemented".into())
88    }
89}
90
91#[allow(unused_variables)]
92/// Trait for handling player-related events.
93pub trait PlayerEvents {
94    /// Called when the queue is changed.
95    fn on_queue_changed(&self, req: SongQueueChangedRequest) -> MoosyncResult<()> {
96        Err("Not implemented".into())
97    }
98
99    /// Called when the volume is changed.
100    fn on_volume_changed(&self, req: VolumeChangedRequest) -> MoosyncResult<()> {
101        Err("Not implemented".into())
102    }
103
104    /// Called when the player state is changed.
105    fn on_player_state_changed(&self, req: PlayerStateChangedRequest) -> MoosyncResult<()> {
106        Err("Not implemented".into())
107    }
108
109    /// Called when the song is changed.
110    fn on_song_changed(&self, req: SongChangedRequest) -> MoosyncResult<()> {
111        Err("Not implemented".into())
112    }
113
114    /// Called when the player is seeked to a specific time.
115    fn on_seeked(&self, req: SeekedRequest) -> MoosyncResult<()> {
116        Err("Not implemented".into())
117    }
118}
119
120#[allow(unused_variables)]
121/// Trait for handling provider-related events.
122pub trait Provider {
123    /// Called when the main app requests the provider scopes.
124    fn get_provider_scopes(&self) -> MoosyncResult<Vec<ExtensionProviderScope>>;
125
126    /// Called when the main app requests the list of playlists.
127    fn get_playlists(&self, req: RequestedPlaylistsRequest) -> MoosyncResult<Vec<Playlist>> {
128        Err("Not implemented".into())
129    }
130
131    /// Called when the main app requests the content of a specific playlist.
132    fn get_playlist_content(
133        &self,
134        req: RequestedPlaylistSongsRequest,
135    ) -> MoosyncResult<SongsWithPageTokenReturnType> {
136        Err("Not implemented".into())
137    }
138
139    /// Called when the main app requests a playlist from a URL.
140    fn get_playlist_from_url(
141        &self,
142        req: RequestedPlaylistFromUrlRequest,
143    ) -> MoosyncResult<Option<Playlist>> {
144        Err("Not implemented".into())
145    }
146
147    /// Called when the main app requests playback details for a song.
148    fn get_playback_details(
149        &self,
150        req: PlaybackDetailsRequestedRequest,
151    ) -> MoosyncResult<PlaybackDetailsReturnType> {
152        Err("Not implemented".into())
153    }
154
155    /// Called when the main app performs a search.
156    fn search(&self, req: RequestedSearchResultRequest) -> MoosyncResult<SearchResult> {
157        Err("Not implemented".into())
158    }
159
160    /// Called when the main app requests recommendations.
161    fn get_recommendations(
162        &self,
163        req: RequestedRecommendationsRequest,
164    ) -> MoosyncResult<Vec<Song>> {
165        Err("Not implemented".into())
166    }
167
168    /// Called when the main app requests a song from a URL.
169    fn get_song_from_url(&self, req: RequestedSongFromUrlRequest) -> MoosyncResult<Option<Song>> {
170        Err("Not implemented".into())
171    }
172
173    /// Called when the main app handles a custom request.
174    fn handle_custom_request(&self, req: CustomRequest) -> MoosyncResult<CustomRequestReturnType> {
175        Err("Not implemented".into())
176    }
177
178    /// Called when the main app requests songs of a specific artist.
179    fn get_artist_songs(
180        &self,
181        req: RequestedArtistSongsRequest,
182    ) -> MoosyncResult<SongsWithPageTokenReturnType> {
183        Err("Not implemented".into())
184    }
185
186    /// Called when the main app requests songs of a specific album.
187    fn get_album_songs(
188        &self,
189        req: RequestedAlbumSongsRequest,
190    ) -> MoosyncResult<SongsWithPageTokenReturnType> {
191        Err("Not implemented".into())
192    }
193
194    /// Called when the main app requests a song from an ID.
195    fn get_song_from_id(&self, req: RequestedSongFromIdRequest) -> MoosyncResult<Option<Song>> {
196        Err("Not implemented".into())
197    }
198
199    /// Called when the main app requests to scrobble a song.
200    fn scrobble(&self, req: ScrobbleRequest) -> MoosyncResult<()> {
201        Err("Not implemented".into())
202    }
203
204    /// Called when the main app requests lyrics for a song.
205    fn get_lyrics(&self, req: RequestedLyricsRequest) -> MoosyncResult<Option<Lyrics>> {
206        Err("Not implemented".into())
207    }
208}
209
210#[allow(unused_variables)]
211/// Trait for handling context menu-related events.
212pub trait ContextMenu {
213    /// Called when the main app requests the context menu for songs.
214    fn get_song_context_menu(
215        &self,
216        req: RequestedSongContextMenuRequest,
217    ) -> MoosyncResult<Vec<ContextMenuReturnType>> {
218        Err("Not implemented".into())
219    }
220
221    /// Called when the main app requests the context menu for a playlist.
222    fn get_playlist_context_menu(
223        &self,
224        req: RequestedPlaylistContextMenuRequest,
225    ) -> MoosyncResult<Vec<ContextMenuReturnType>> {
226        Err("Not implemented".into())
227    }
228
229    /// Called when the main app performs an action from the context menu.
230    fn on_context_menu_action(&self, req: ContextMenuActionRequest) -> MoosyncResult<()> {
231        Err("Not implemented".into())
232    }
233}
234
235/// Trait that combines all other traits for the extension.
236pub trait Extension:
237    Provider + PlayerEvents + PreferenceEvents + DatabaseEvents + Accounts + ContextMenu
238{
239}
240
241#[derive(Debug)]
242pub struct PlaybackDetailsReturnType {
243    pub duration: u32,
244    pub url: String,
245}
246
247#[derive(Debug)]
248pub struct SongsWithPageTokenReturnType {
249    pub songs: Vec<Song>,
250    pub next_page_token: Option<String>,
251}
252
253#[derive(Debug)]
254pub struct ContextMenuReturnTypeWrapper(pub ContextMenuReturnType);
255
256#[derive(Debug)]
257pub struct CustomRequestReturnType {
258    pub mime_type: Option<String>,
259    pub data: Option<Vec<u8>>,
260    pub redirect_url: Option<String>,
261}
262
263#[derive(Debug)]
264pub struct EntityInfo {} // Dummy definition just in case, wait, not needed.
265
266#[host_fn]
267extern "ExtismHost" {
268    fn send_main_command(command: Prost<MainCommand>) -> Prost<MainCommandResponse>;
269    fn system_time() -> u64;
270    fn open_clientfd(path: String) -> i64;
271    fn write_sock(sock_id: i64, buf: Vec<u8>) -> i64;
272    fn read_sock(sock_id: i64, read_len: u64) -> Vec<u8>;
273    fn hash(hash_type: String, data: Vec<u8>) -> Vec<u8>;
274}
275
276pub mod extension_api {
277    use super::*;
278    use crate::handler::MoosyncError;
279    use crate::response_utils::Extract;
280    use extensions_proto::moosync::types::main_command::Command as MainCommandEnum;
281    use extensions_proto::moosync::types::main_command_response::Response as MainCommandResponseEnum;
282    use songs_proto::moosync::types::{GetEntityOptions, GetSongOptions}; // Needed
283
284    use super::{
285        hash, open_clientfd, read_sock as read_sock_ext, send_main_command, system_time,
286        write_sock as write_sock_ext,
287    };
288
289    macro_rules! create_api_fn {
290        ($(
291            $(#[doc = $doc:literal])*
292            $fn_name:ident (
293                $Variant:ident,
294                $ReqType:ident,
295                $RespType:ident
296                $(, $arg_name:ident : $arg_type:ty )*
297            ) -> $ret_type:ty
298        );* $(;)?) => {
299            $(
300                $(#[doc = $doc])*
301                pub fn $fn_name($( $arg_name: $arg_type ),*) -> MoosyncResult<$ret_type> {
302                    unsafe {
303                        let request = $ReqType {
304                            $( $arg_name: Some($arg_name.into()) ),*
305                        };
306                        let cmd_enum = MainCommandEnum::$Variant(request);
307                        let cmd = MainCommand { command: Some(cmd_enum) };
308
309                        let extism_pdk::Prost(res) = send_main_command(extism_pdk::Prost(cmd)).unwrap();
310
311                        if let Some(MainCommandResponseEnum::Error(e)) = res.response.as_ref() {
312                            return Err(MoosyncError::String(e.message.clone()));
313                        }
314
315                        if let Some(MainCommandResponseEnum::$Variant(data)) = res.response {
316                            return Ok(data.extract());
317                        }
318
319                        Err(MoosyncError::String("Host returned invalid response".into()))
320                    }
321                }
322            )*
323        };
324    }
325
326    macro_rules! create_api_fn_no_resp {
327        ($(
328            $(#[doc = $doc:literal])*
329            $fn_name:ident (
330                $Variant:ident,
331                $ReqType:ident
332                $(, $arg_name:ident : $arg_type:ty )*
333            ) -> $ret_type:ty
334        );* $(;)?) => {
335            $(
336                $(#[doc = $doc])*
337                pub fn $fn_name($( $arg_name: $arg_type ),*) -> MoosyncResult<$ret_type> {
338                    unsafe {
339                        let request = $ReqType {
340                            $( $arg_name: Some($arg_name.into()) ),*
341                        };
342                         let cmd_enum = MainCommandEnum::$Variant(request);
343                        let cmd = MainCommand { command: Some(cmd_enum) };
344
345                        let extism_pdk::Prost(res) = send_main_command(extism_pdk::Prost(cmd)).unwrap();
346
347                        if let Some(MainCommandResponseEnum::Error(e)) = res.response.as_ref() {
348                            return Err(MoosyncError::String(e.message.clone()));
349                        }
350
351                        if let Some(MainCommandResponseEnum::$Variant(_)) = res.response {
352                            return Ok(());
353                        }
354
355                        Err(MoosyncError::String("Host returned invalid response".into()))
356                    }
357                }
358            )*
359        };
360    }
361
362    // Special macro for repeated fields or non-optional ones if pattern differs
363    macro_rules! create_api_fn_repeated {
364        ($(
365            $(#[doc = $doc:literal])*
366            $fn_name:ident (
367                $Variant:ident,
368                $ReqType:ident,
369                $field:ident,
370                $arg_name:ident : $arg_type:ty
371            ) -> $ret_type:ty
372        );* $(;)?) => {
373            $(
374                $(#[doc = $doc])*
375                pub fn $fn_name( $arg_name: $arg_type ) -> MoosyncResult<$ret_type> {
376                    unsafe {
377                        let request = $ReqType {
378                            $field: $arg_name, // Direct assignment for repeated
379                        };
380                         let cmd_enum = MainCommandEnum::$Variant(request);
381                        let cmd = MainCommand { command: Some(cmd_enum) };
382
383                        let extism_pdk::Prost(res) = send_main_command(extism_pdk::Prost(cmd)).unwrap();
384
385                        if let Some(MainCommandResponseEnum::Error(e)) = res.response.as_ref() {
386                            return Err(MoosyncError::String(e.message.clone()));
387                        }
388
389                        if let Some(MainCommandResponseEnum::$Variant(_)) = res.response {
390                            return Ok(());
391                        }
392
393                         Err(MoosyncError::String("Host returned invalid response".into()))
394                    }
395                }
396            )*
397        };
398    }
399
400    create_api_fn! {
401        /// Retrieves a list of songs based on the provided options.
402        get_song(GetSong, GetSongRequest, GetSongResponse, options: GetSongOptions) -> Vec<Song>;
403
404        /// Retrieves the current song being played.
405        get_current_song(GetCurrentSong, GetCurrentSongRequest, GetCurrentSongResponse) -> Option<Song>;
406
407        get_entity(GetEntity, GetEntityRequest, GetEntityResponse, options: GetEntityOptions) -> Option<EntityResult>;
408
409        /// Retrieves the current state of the player.
410        get_player_state(GetPlayerState, GetPlayerStateRequest, GetPlayerStateResponse) -> PlayerState;
411
412        /// Retrieves the current volume level.
413        get_volume(GetVolume, GetVolumeRequest, GetVolumeResponse) -> f64;
414
415        /// Retrieves the current playback time.
416        get_time(GetTime, GetTimeRequest, GetTimeResponse) -> f64;
417
418        /// Retrieves the current playback queue.
419        get_queue(GetQueue, GetQueueRequest, GetQueueResponse) -> Option<ProtoStruct>;
420
421        /// Retrieves a preference value based on the provided data.
422        get_preference(GetPreference, GetPreferenceRequest, GetPreferenceResponse, data: PreferenceData) -> PreferenceData;
423
424        /// Retrieves a secure preference value based on the provided data.
425        get_secure(GetSecure, GetSecureRequest, GetSecureResponse, data: PreferenceData) -> PreferenceData;
426
427        /// Adds a new playlist to the main app.
428        add_playlist(AddPlaylist, AddPlaylistRequest, AddPlaylistResponse, playlist: Playlist) -> String;
429    }
430
431    create_api_fn_no_resp! {
432        /// Sets a preference value based on the provided data.
433        set_preference(SetPreference, SetPreferenceRequest, data: PreferenceData) -> ();
434
435        /// Sets a secure preference value based on the provided data.
436        set_secure(SetSecure, SetSecureRequest, data: PreferenceData) -> ();
437
438        /// Removes a song from the main app.
439        remove_song(RemoveSong, RemoveSongRequest, song: Song) -> ();
440
441        /// Updates a song in the main app.
442        update_song(UpdateSong, UpdateSongRequest, song: Song) -> ();
443    }
444
445    /// Registers or updates an account in the main app.
446    pub fn set_account(account: ExtensionAccountDetail) -> MoosyncResult<()> {
447        unsafe {
448            let request = SetAccountRequest {
449                account: Some(account),
450            };
451            let cmd_enum = MainCommandEnum::SetAccount(request);
452            let cmd = MainCommand {
453                command: Some(cmd_enum),
454            };
455
456            let extism_pdk::Prost(res) = send_main_command(extism_pdk::Prost(cmd)).unwrap();
457
458            if let Some(MainCommandResponseEnum::Error(e)) = res.response.as_ref() {
459                return Err(MoosyncError::String(e.message.clone()));
460            }
461
462            if let Some(MainCommandResponseEnum::SetAccount(_)) = res.response {
463                return Ok(());
464            }
465
466            Err(MoosyncError::String(
467                "Host returned invalid response".into(),
468            ))
469        }
470    }
471    // If I pass the struct directly, I don't need to construct it.
472    // I need a special macro for "Pass Through Request".
473
474    // Pass-through request (argument IS the request)
475    macro_rules! create_api_fn_pass_through {
476        ($(
477            $(#[doc = $doc:literal])*
478            $fn_name:ident (
479                $Variant:ident,
480                $ReqType:ident,
481                $arg_name:ident : $arg_type:ty
482            ) -> $ret_type:ty
483        );* $(;)?) => {
484            $(
485                $(#[doc = $doc])*
486                pub fn $fn_name( $arg_name: $arg_type ) -> MoosyncResult<$ret_type> {
487                    unsafe {
488                        // Argument is the request itself
489                         let cmd_enum = MainCommandEnum::$Variant($arg_name);
490                        let cmd = MainCommand { command: Some(cmd_enum) };
491
492                        let extism_pdk::Prost(res) = send_main_command(extism_pdk::Prost(cmd)).unwrap();
493
494                        if let Some(MainCommandResponseEnum::Error(e)) = res.response.as_ref() {
495                            return Err(MoosyncError::String(e.message.clone()));
496                        }
497
498                        if let Some(MainCommandResponseEnum::$Variant(_)) = res.response {
499                            return Ok(());
500                        }
501
502                        Err(MoosyncError::String("Host returned invalid response".into()))
503                    }
504                }
505            )*
506        };
507    }
508
509    create_api_fn_pass_through! {
510         /// Adds a song to a playlist.
511        add_to_playlist(AddToPlaylist, AddToPlaylistRequest, request: AddToPlaylistRequest) -> ();
512    }
513
514    create_api_fn_repeated! {
515         /// Adds a list of songs to the main app.
516        add_songs(AddSongs, AddSongsRequest, songs, songs: Vec<Song>) -> ();
517
518        // RegisterUserPreferenceRequest has 'prefs' field (repeated).
519        /// Registers user preferences with the main app.
520        register_user_preferences(RegisterUserPreference, RegisterUserPreferenceRequest, prefs, prefs: Vec<PreferenceItem>) -> ();
521
522        // UnregisterUserPreferenceRequest has 'keys' field.
523        /// Unregisters user preferences from the main app.
524        unregister_user_preferences(UnregisterUserPreference, UnregisterUserPreferenceRequest, keys, keys: Vec<String>) -> ();
525    }
526
527    // RegisterOAuth: `url`. Request field `url`.
528    // OpenExternalUrl: `url`. Request field `url`.
529
530    pub fn register_oauth(url: String) -> MoosyncResult<()> {
531        unsafe {
532            let request = RegisterOauthRequest { url };
533            let cmd_enum = MainCommandEnum::RegisterOauth(request);
534            let cmd = MainCommand {
535                command: Some(cmd_enum),
536            };
537            let extism_pdk::Prost(res) = send_main_command(extism_pdk::Prost(cmd)).unwrap();
538
539            if let Some(MainCommandResponseEnum::Error(e)) = res.response.as_ref() {
540                return Err(MoosyncError::String(e.message.clone()));
541            }
542            if let Some(MainCommandResponseEnum::RegisterOauth(_)) = res.response {
543                return Ok(());
544            }
545            // Ignore other responses or treat as success if no error?
546            // Better to return Ok only on matching response or if we don't care about specific return
547            Ok(())
548        }
549    }
550
551    pub fn open_external_url(url: String) -> MoosyncResult<()> {
552        unsafe {
553            let request = OpenExternalUrlRequest { url };
554            let cmd_enum = MainCommandEnum::OpenExternalUrl(request);
555            let cmd = MainCommand {
556                command: Some(cmd_enum),
557            };
558            let extism_pdk::Prost(res) = send_main_command(extism_pdk::Prost(cmd)).unwrap();
559
560            if let Some(MainCommandResponseEnum::Error(e)) = res.response.as_ref() {
561                return Err(MoosyncError::String(e.message.clone()));
562            }
563            // OpenExternalUrlResponse
564            Ok(())
565        }
566    }
567
568    // update_accounts needs rename in signature or macro usage?
569    // Macro assumes arg matches field.
570    // I will rename signature arg to 'account'.
571
572    pub fn get_system_time() -> u64 {
573        unsafe {
574            if let Ok(time) = system_time() {
575                return time;
576            }
577            0u64
578        }
579    }
580
581    pub fn open_sock(path: String) -> MoosyncResult<i64> {
582        let res = unsafe { open_clientfd(path) };
583        res.map_err(|e| MoosyncError::String(e.to_string()))
584    }
585
586    pub fn write_sock(sock_id: i64, buf: Vec<u8>) -> MoosyncResult<i64> {
587        let res = unsafe { write_sock_ext(sock_id, buf) };
588        res.map_err(|e| MoosyncError::String(e.to_string()))
589    }
590
591    pub fn read_sock(sock_id: i64, read_len: u64) -> MoosyncResult<Vec<u8>> {
592        let res = unsafe { read_sock_ext(sock_id, read_len) };
593        res.map_err(|e| MoosyncError::String(e.to_string()))
594    }
595
596    pub fn gen_hash(hash_type: String, data: Vec<u8>) -> MoosyncResult<Vec<u8>> {
597        let res = unsafe { hash(hash_type, data) };
598        res.map_err(|e| MoosyncError::String(e.to_string()))
599    }
600}