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