moosync_edk/
handler.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 crate::api::Extension;
18use extensions_proto::moosync::types::extension_command::Event;
19use extensions_proto::moosync::types::extension_command_response::Response;
20use extensions_proto::moosync::types::*;
21use extism_pdk::FnResult;
22use std::{cell::RefCell, rc::Rc};
23
24thread_local!(
25    static EXTENSION: RefCell<Option<Rc<Box<dyn Extension>>>> = RefCell::new(None);
26);
27
28#[tracing::instrument(level = "debug", skip(extension))]
29pub fn register_extension(extension: Box<dyn Extension>) -> FnResult<()> {
30    EXTENSION.with(|ext| {
31        ext.borrow_mut().replace(Rc::new(extension));
32    });
33    Ok(())
34}
35
36#[derive(Debug)]
37pub enum MoosyncError {
38    String(String),
39}
40
41impl From<String> for MoosyncError {
42    fn from(e: String) -> Self {
43        MoosyncError::String(e)
44    }
45}
46impl From<&str> for MoosyncError {
47    fn from(s: &str) -> Self {
48        MoosyncError::String(s.to_string())
49    }
50}
51
52impl std::fmt::Display for MoosyncError {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        match self {
55            MoosyncError::String(s) => write!(f, "{}", s),
56        }
57    }
58}
59
60impl std::error::Error for MoosyncError {}
61
62// Using a macro for dispatch significantly simplifies the repetitive match arms.
63macro_rules! dispatch_command {
64    ($ext:expr, $event:expr, {
65        $($Variant:ident $(($($arg:pat),*))? => $method:ident $(($($param:expr),*))? => $res:ident in $RespVariant:ident $Body:tt),* $(,)?
66    }) => {
67        match $event {
68            $(
69                Event::$Variant($($($arg),*)?) => {
70                     let $res = $ext.$method($($($param),*)?)
71                        .map_err(|e| extism_pdk::Error::msg(format!("Error: {:?}", e)))?;
72
73                    Response::$Variant($RespVariant $Body)
74                }
75            )*
76             Event::GetRemoteUrl(_) => {
77                 return Err(extism_pdk::Error::msg("Not implemented"));
78             }
79        }
80    };
81}
82
83pub fn handle_command(
84    cmd: ExtensionCommand,
85) -> Result<ExtensionCommandResponse, extism_pdk::Error> {
86    EXTENSION.with(|ext| {
87        if let Some(ext) = ext.borrow().as_ref() {
88            let mut response = ExtensionCommandResponse { response: None };
89
90            if let Some(event) = cmd.event {
91                let resp = dispatch_command!(ext, event, {
92                    RequestedPlaylists(req) => get_playlists(req) => res in RequestedPlaylistsResponse { playlists: res },
93                    RequestedPlaylistSongs(req) => get_playlist_content(req) => res in RequestedPlaylistSongsResponse {
94                        songs: res.songs,
95                        next_page_token: res.next_page_token,
96                    },
97                    OauthCallback(req) => oauth_callback(req) => _res in OauthCallbackResponse {},
98                    SongQueueChanged(req) => on_queue_changed(req) => _res in SongQueueChangedResponse {},
99                    Seeked(req) => on_seeked(req) => _res in SeekedResponse {},
100                    VolumeChanged(req) => on_volume_changed(req) => _res in VolumeChangedResponse {},
101                    PlayerStateChanged(req) => on_player_state_changed(req) => _res in PlayerStateChangedResponse {},
102                    SongChanged(req) => on_song_changed(req) => _res in SongChangedResponse {},
103                    PreferenceChanged(req) => on_preferences_changed(req) => _res in PreferenceChangedResponse {},
104                    PlaybackDetailsRequested(req) => get_playback_details(req) => res in PlaybackDetailsRequestedResponse {
105                        duration: res.duration,
106                        url: res.url,
107                    },
108                    CustomRequest(req) => handle_custom_request(req) => res in CustomRequestResponse {
109                         mime_type: res.mime_type,
110                         data: res.data,
111                         redirect_url: res.redirect_url,
112                    },
113                    RequestedSongFromUrl(req) => get_song_from_url(req) => res in RequestedSongFromUrlResponse { song: res },
114                    RequestedPlaylistFromUrl(req) => get_playlist_from_url(req) => res in RequestedPlaylistFromUrlResponse {
115                         playlist: res,
116                         songs: vec![],
117                    },
118                    RequestedSearchResult(req) => search(req) => res in RequestedSearchResultResponse {
119                         songs: res.songs,
120                         playlists: res.playlists,
121                         artists: res.artists,
122                         albums: res.albums,
123                    },
124                    RequestedRecommendations(req) => get_recommendations(req) => res in RequestedRecommendationsResponse { songs: res },
125                    RequestedLyrics(req) => get_lyrics(req) => res in RequestedLyricsResponse { lyrics: res },
126                    RequestedArtistSongs(req) => get_artist_songs(req) => res in RequestedArtistSongsResponse {
127                         songs: res.songs,
128                         next_page_token: res.next_page_token,
129                    },
130                    RequestedAlbumSongs(req) => get_album_songs(req) => res in RequestedAlbumSongsResponse {
131                         songs: res.songs,
132                         next_page_token: res.next_page_token,
133                    },
134                    SongAdded(req) => on_song_added(req) => _res in SongAddedResponse {},
135                    SongRemoved(req) => on_song_removed(req) => _res in SongRemovedResponse {},
136                    PlaylistAdded(req) => on_playlist_added(req) => _res in PlaylistAddedResponse {},
137                    PlaylistRemoved(req) => on_playlist_removed(req) => _res in PlaylistRemovedResponse {},
138                    RequestedSongFromId(req) => get_song_from_id(req) => res in RequestedSongFromIdResponse { song: res },
139                    Scrobble(req) => scrobble(req) => _res in ScrobbleResponse {},
140                    RequestedSongContextMenu(req) => get_song_context_menu(req) => res in RequestedSongContextMenuResponse {
141                         menu: res.into_iter().next(),
142                    },
143                    RequestedPlaylistContextMenu(req) => get_playlist_context_menu(req) => res in RequestedPlaylistContextMenuResponse {
144                         menu: res.into_iter().next(),
145                    },
146                    ContextMenuAction(req) => on_context_menu_action(req) => _res in ContextMenuActionResponse {},
147                    GetProviderScopes(_) => get_provider_scopes() => res in GetProviderScopesResponse {
148                         scopes: res.into_iter().map(|s| s as i32).collect(),
149                    },
150                    GetAccounts(_) => get_accounts() => res in GetAccountsResponse { accounts: res },
151                    PerformAccountLogin(req) => perform_account_login(req) => res in PerformAccountLoginResponse { status: res },
152                });
153
154                response.response = Some(resp);
155            }
156
157            Ok(response)
158        } else {
159            Err(extism_pdk::Error::msg("No extension registered"))
160        }
161    })
162}