moosync_edk
1from enum import Enum 2import sys 3from datetime import timedelta 4from typing import Any, Dict, List, Optional, Sequence, Union, cast 5 6import extism 7from core.types.protos import extensions_pb2, preferences_pb2, songs_pb2, themes_pb2, ui_pb2 8 9# Re-export protos 10from core.types.protos.extensions_pb2 import * 11from core.types.protos.preferences_pb2 import * 12from core.types.protos.songs_pb2 import * 13from core.types.protos.ui_pb2 import * 14from google.protobuf.duration_pb2 import Duration 15 16# Fix module split issue when running as entry point 17if "moosync_edk" not in sys.modules: 18 sys.modules["moosync_edk"] = sys.modules[__name__] 19 20 21@extism.import_fn("extism:host/user", "open_clientfd") 22def open_sock(path: str) -> int: ... 23 24 25@extism.import_fn("extism:host/user", "system_time") 26def system_time() -> int: ... 27 28 29@extism.import_fn("extism:host/user", "write_sock") 30def write_sock(sock_id: int, buf: bytes) -> int: ... 31 32 33@extism.import_fn("extism:host/user", "read_sock") 34def read_sock(sock_id: int, read_len: int) -> bytes: ... 35 36 37@extism.import_fn("extism:host/user", "hash") 38def hash(hash_type: str, data: bytes) -> bytes: ... 39 40 41@extism.import_fn("extism:host/user", "send_main_command") 42def send_main_command(data: int) -> int: ... 43 44 45@extism.import_fn("extism:host/user", "batch_http_request") 46def batch_http_request_host(data: int) -> int: ... 47 48 49class CustomPrint: 50 buf = "" 51 52 def write(self, text): 53 self.buf += text 54 self.flush() 55 56 def flush(self): 57 extism.log(extism.LogLevel.Debug, self.buf) 58 self.buf = "" 59 60 61class HttpRequest: 62 def __init__( 63 self, 64 url: str, 65 method: str = "GET", 66 headers: Optional[Dict[str, str]] = None, 67 body: Optional[Union[bytes, str]] = None, 68 timeout_ms: Optional[int] = None, 69 ): 70 self.url = url 71 self.method = method 72 self.headers = headers or {} 73 if isinstance(body, str): 74 self.body = body.encode("utf-8") 75 else: 76 self.body = body 77 self.timeout_ms = timeout_ms 78 79 def to_proto(self) -> extensions_pb2.HttpRequest: 80 return extensions_pb2.HttpRequest( 81 url=self.url, 82 method=self.method.upper(), 83 headers=self.headers, 84 body=self.body, 85 timeout_ms=self.timeout_ms, 86 ) 87 88 89class HttpResponse: 90 def __init__(self, proto: extensions_pb2.HttpResponse): 91 self.status_code: int = proto.status_code 92 self.status_text: str = proto.status_text 93 self.headers: dict = dict(proto.headers) 94 self.body: bytes = proto.body 95 96 @property 97 def ok(self) -> bool: 98 return 200 <= self.status_code < 300 99 100 @property 101 def text(self) -> str: 102 return self.body.decode("utf-8", errors="replace") 103 104 def json(self) -> Any: 105 import json 106 return json.loads(self.text) 107 108 109class HttpResult: 110 def __init__(self, proto: extensions_pb2.HttpResult): 111 which = proto.WhichOneof("result") 112 if which == "response": 113 self.response: Optional[HttpResponse] = HttpResponse(proto.response) 114 self.error: Optional[str] = None 115 elif which == "error": 116 self.response = None 117 self.error = proto.error 118 else: 119 self.response = None 120 self.error = "Empty HTTP result" 121 122 123def http_batch_request(requests: Sequence[Union[HttpRequest, Dict[str, Any]]]) -> List[HttpResponse]: 124 if not requests: 125 return [] 126 proto_reqs = [] 127 for r in requests: 128 if isinstance(r, HttpRequest): 129 proto_reqs.append(r.to_proto()) 130 elif isinstance(r, dict): 131 req_obj = HttpRequest(**r) 132 proto_reqs.append(req_obj.to_proto()) 133 else: 134 raise ValueError(f"Invalid request type: {type(r)}") 135 136 batch = extensions_pb2.BatchHttpRequest(requests=proto_reqs) 137 data = batch.SerializeToString() 138 mem = extism.memory.alloc(data) 139 if mem is None: 140 raise Exception("Failed to allocate memory for HTTP batch request") 141 res_offset = batch_http_request_host(mem.offset) 142 143 res_mem_handle = extism.memory.find(res_offset) 144 if res_mem_handle is None: 145 raise Exception(f"Failed to find memory for batch HTTP response at offset {res_offset}") 146 147 res_bytes = extism.memory.bytes(res_mem_handle) 148 extism.memory.free(mem) 149 150 batch_resp = extensions_pb2.BatchHttpResponse() 151 batch_resp.ParseFromString(res_bytes) 152 153 if batch_resp.HasField("error") and batch_resp.error: 154 raise Exception(batch_resp.error) 155 156 responses = [] 157 for idx, item in enumerate(batch_resp.responses): 158 which = item.WhichOneof("result") 159 if which == "response": 160 responses.append(HttpResponse(item.response)) 161 elif which == "error": 162 raise Exception(f"Request #{idx} failed: {item.error}") 163 else: 164 raise Exception(f"Request #{idx} returned empty HTTP result") 165 return responses 166 167 168def http_batch_get( 169 urls: List[str], 170 headers: Optional[Dict[str, str]] = None, 171 timeout_ms: Optional[int] = None, 172) -> List[HttpResponse]: 173 reqs = [HttpRequest(url=u, method="GET", headers=headers, timeout_ms=timeout_ms) for u in urls] 174 return http_batch_request(reqs) 175 176 177def http_request( 178 url: str, 179 method: str = "GET", 180 body: Optional[Union[bytes, str]] = None, 181 headers: Optional[Dict[str, str]] = None, 182 timeout_ms: Optional[int] = None, 183) -> HttpResponse: 184 req = HttpRequest( 185 url=url, 186 method=method, 187 headers=headers, 188 body=body, 189 timeout_ms=timeout_ms, 190 ) 191 resps = http_batch_request([req]) 192 if not resps: 193 raise Exception("Host runner returned empty response") 194 return resps[0] 195 196 197def http_get( 198 url: str, 199 headers: Optional[Dict[str, str]] = None, 200 timeout_ms: Optional[int] = None, 201) -> HttpResponse: 202 return http_request(url=url, method="GET", headers=headers, timeout_ms=timeout_ms) 203 204 205def send_main_command_( 206 cmd: extensions_pb2.MainCommand, 207) -> extensions_pb2.MainCommandResponse: 208 data = cmd.SerializeToString() 209 mem = extism.memory.alloc(data) 210 if mem is None: 211 raise Exception("Failed to allocate memory for main command") 212 res_offset = send_main_command(mem.offset) 213 214 res_mem_handle = extism.memory.find(res_offset) 215 if res_mem_handle is None: 216 raise Exception(f"Failed to find memory for response at offset {res_offset}") 217 218 res_bytes = extism.memory.bytes(res_mem_handle) 219 220 extism.memory.free(mem) 221 222 resp = extensions_pb2.MainCommandResponse() 223 resp.ParseFromString(res_bytes) 224 return resp 225 226 227class Api: 228 def get_song(self, options: songs_pb2.GetSongOptions) -> List[songs_pb2.Song]: 229 req = extensions_pb2.MainCommand( 230 get_song=extensions_pb2.GetSongRequest(options=options) 231 ) 232 resp = send_main_command_(req) 233 return list(resp.get_song.songs) 234 235 def get_current_song(self) -> Optional[songs_pb2.Song]: 236 req = extensions_pb2.MainCommand( 237 get_current_song=extensions_pb2.GetCurrentSongRequest() 238 ) 239 resp = send_main_command_(req) 240 if resp.HasField("get_current_song"): 241 if resp.get_current_song.HasField("song"): 242 return resp.get_current_song.song 243 return None 244 245 def get_player_state(self) -> extensions_pb2.PlayerState: 246 req = extensions_pb2.MainCommand( 247 get_player_state=extensions_pb2.GetPlayerStateRequest() 248 ) 249 resp = send_main_command_(req) 250 return resp.get_player_state.state 251 252 def get_volume(self) -> float: 253 req = extensions_pb2.MainCommand(get_volume=extensions_pb2.GetVolumeRequest()) 254 resp = send_main_command_(req) 255 return resp.get_volume.volume 256 257 def get_time(self) -> float: 258 req = extensions_pb2.MainCommand(get_time=extensions_pb2.GetTimeRequest()) 259 resp = send_main_command_(req) 260 return resp.get_time.time 261 262 def get_queue(self) -> Any: # Returns Struct 263 req = extensions_pb2.MainCommand(get_queue=extensions_pb2.GetQueueRequest()) 264 resp = send_main_command_(req) 265 return resp.get_queue.queue 266 267 def get_preference( 268 self, data: extensions_pb2.PreferenceData 269 ) -> extensions_pb2.PreferenceData: 270 req = extensions_pb2.MainCommand( 271 get_preference=extensions_pb2.GetPreferenceRequest(data=data) 272 ) 273 resp = send_main_command_(req) 274 return resp.get_preference.data 275 276 def get_secure( 277 self, data: extensions_pb2.PreferenceData 278 ) -> extensions_pb2.PreferenceData: 279 req = extensions_pb2.MainCommand( 280 get_secure=extensions_pb2.GetSecureRequest(data=data) 281 ) 282 resp = send_main_command_(req) 283 return resp.get_secure.data 284 285 def set_preference(self, data: extensions_pb2.PreferenceData) -> bool: 286 req = extensions_pb2.MainCommand( 287 set_preference=extensions_pb2.SetPreferenceRequest(data=data) 288 ) 289 resp = send_main_command_(req) 290 return resp.set_preference.success 291 292 def set_secure(self, data: extensions_pb2.PreferenceData) -> bool: 293 req = extensions_pb2.MainCommand( 294 set_secure=extensions_pb2.SetSecureRequest(data=data) 295 ) 296 resp = send_main_command_(req) 297 return resp.set_secure.success 298 299 def add_songs(self, songs: List[songs_pb2.Song]) -> List[songs_pb2.Song]: 300 req = extensions_pb2.MainCommand( 301 add_songs=extensions_pb2.AddSongsRequest(songs=songs) 302 ) 303 resp = send_main_command_(req) 304 return list(resp.add_songs.songs) 305 306 def remove_song(self, song: songs_pb2.Song) -> bool: 307 req = extensions_pb2.MainCommand( 308 remove_song=extensions_pb2.RemoveSongRequest(song=song) 309 ) 310 resp = send_main_command_(req) 311 return resp.remove_song.success 312 313 def update_song(self, song: songs_pb2.Song) -> songs_pb2.Song: 314 req = extensions_pb2.MainCommand( 315 update_song=extensions_pb2.UpdateSongRequest(song=song) 316 ) 317 resp = send_main_command_(req) 318 return resp.update_song.song 319 320 def add_playlist(self, playlist: songs_pb2.Playlist) -> str: 321 req = extensions_pb2.MainCommand( 322 add_playlist=extensions_pb2.AddPlaylistRequest(playlist=playlist) 323 ) 324 resp = send_main_command_(req) 325 return resp.add_playlist.playlist_id 326 327 def add_to_playlist(self, playlist_id: str, songs: List[songs_pb2.Song]) -> bool: 328 req = extensions_pb2.MainCommand( 329 add_to_playlist=extensions_pb2.AddToPlaylistRequest( 330 playlist_id=playlist_id, songs=songs 331 ) 332 ) 333 resp = send_main_command_(req) 334 return resp.add_to_playlist.success 335 336 def register_oauth(self, url: str) -> bool: 337 req = extensions_pb2.MainCommand( 338 register_oauth=extensions_pb2.RegisterOauthRequest(url=url) 339 ) 340 resp = send_main_command_(req) 341 return resp.register_oauth.success 342 343 def open_external_url(self, url: str) -> bool: 344 req = extensions_pb2.MainCommand( 345 open_external_url=extensions_pb2.OpenExternalUrlRequest(url=url) 346 ) 347 resp = send_main_command_(req) 348 return resp.open_external_url.success 349 350 def set_account(self, account: extensions_pb2.ExtensionAccountDetail) -> None: 351 req = extensions_pb2.MainCommand( 352 set_account=extensions_pb2.SetAccountRequest(account=account) 353 ) 354 resp = send_main_command_(req) 355 if not resp.HasField("set_account"): 356 raise Exception("Failed to set account") 357 358 def register_user_preferences(self, prefs: List[preferences_pb2.PreferenceItem]) -> bool: 359 req = extensions_pb2.MainCommand( 360 register_user_preference=extensions_pb2.RegisterUserPreferenceRequest( 361 prefs=prefs 362 ) 363 ) 364 resp = send_main_command_(req) 365 return resp.register_user_preference.success 366 367 def unregister_user_preferences(self, keys: List[str]) -> bool: 368 req = extensions_pb2.MainCommand( 369 unregister_user_preference=extensions_pb2.UnregisterUserPreferenceRequest( 370 keys=keys 371 ) 372 ) 373 resp = send_main_command_(req) 374 return resp.unregister_user_preference.success 375 376 377class Extension: 378 api = Api() 379 380 def get_provider_scopes( 381 self, _: extensions_pb2.GetProviderScopesRequest 382 ) -> extensions_pb2.GetProviderScopesResponse: 383 return extensions_pb2.GetProviderScopesResponse() 384 385 def get_playlists( 386 self, req: extensions_pb2.RequestedPlaylistsRequest 387 ) -> extensions_pb2.RequestedPlaylistsResponse: 388 raise NotImplementedError() 389 390 def get_playlist_content( 391 self, req: extensions_pb2.RequestedPlaylistSongsRequest 392 ) -> extensions_pb2.RequestedPlaylistSongsResponse: 393 raise NotImplementedError() 394 395 def get_playlist_from_url( 396 self, req: extensions_pb2.RequestedPlaylistFromUrlRequest 397 ) -> extensions_pb2.RequestedPlaylistFromUrlResponse: 398 raise NotImplementedError() 399 400 def get_playback_details( 401 self, req: extensions_pb2.PlaybackDetailsRequestedRequest 402 ) -> extensions_pb2.PlaybackDetailsRequestedResponse: 403 raise NotImplementedError() 404 405 def get_search( 406 self, req: extensions_pb2.RequestedSearchResultRequest 407 ) -> extensions_pb2.RequestedSearchResultResponse: 408 raise NotImplementedError() 409 410 def get_recommendations( 411 self, req: extensions_pb2.RequestedRecommendationsRequest 412 ) -> extensions_pb2.RequestedRecommendationsResponse: 413 raise NotImplementedError() 414 415 def get_song_from_url( 416 self, req: extensions_pb2.RequestedSongFromUrlRequest 417 ) -> extensions_pb2.RequestedSongFromUrlResponse: 418 raise NotImplementedError() 419 420 def handle_custom_request( 421 self, req: extensions_pb2.CustomRequest 422 ) -> extensions_pb2.CustomRequestResponse: 423 raise NotImplementedError() 424 425 def get_artist_songs( 426 self, req: extensions_pb2.RequestedArtistSongsRequest 427 ) -> extensions_pb2.RequestedArtistSongsResponse: 428 raise NotImplementedError() 429 430 def get_album_songs( 431 self, req: extensions_pb2.RequestedAlbumSongsRequest 432 ) -> extensions_pb2.RequestedAlbumSongsResponse: 433 raise NotImplementedError() 434 435 def get_song_from_id( 436 self, req: extensions_pb2.RequestedSongFromIdRequest 437 ) -> extensions_pb2.RequestedSongFromIdResponse: 438 raise NotImplementedError() 439 440 def on_queue_changed( 441 self, req: extensions_pb2.SongQueueChangedRequest 442 ) -> extensions_pb2.SongQueueChangedResponse: 443 raise NotImplementedError() 444 445 def on_volume_changed( 446 self, req: extensions_pb2.VolumeChangedRequest 447 ) -> extensions_pb2.VolumeChangedResponse: 448 raise NotImplementedError() 449 450 def on_player_state_changed( 451 self, req: extensions_pb2.PlayerStateChangedRequest 452 ) -> extensions_pb2.PlayerStateChangedResponse: 453 raise NotImplementedError() 454 455 def on_song_changed( 456 self, req: extensions_pb2.SongChangedRequest 457 ) -> extensions_pb2.SongChangedResponse: 458 raise NotImplementedError() 459 460 def on_seeked( 461 self, req: extensions_pb2.SeekedRequest 462 ) -> extensions_pb2.SeekedResponse: 463 raise NotImplementedError() 464 465 def on_preferences_changed( 466 self, req: extensions_pb2.PreferenceChangedRequest 467 ) -> extensions_pb2.PreferenceChangedResponse: 468 raise NotImplementedError() 469 470 def on_song_added( 471 self, req: extensions_pb2.SongAddedRequest 472 ) -> extensions_pb2.SongAddedResponse: 473 raise NotImplementedError() 474 475 def on_song_removed( 476 self, req: extensions_pb2.SongRemovedRequest 477 ) -> extensions_pb2.SongRemovedResponse: 478 raise NotImplementedError() 479 480 def on_playlist_added( 481 self, req: extensions_pb2.PlaylistAddedRequest 482 ) -> extensions_pb2.PlaylistAddedResponse: 483 raise NotImplementedError() 484 485 def on_playlist_removed( 486 self, req: extensions_pb2.PlaylistRemovedRequest 487 ) -> extensions_pb2.PlaylistRemovedResponse: 488 raise NotImplementedError() 489 490 def perform_account_login( 491 self, req: extensions_pb2.PerformAccountLoginRequest 492 ) -> extensions_pb2.PerformAccountLoginResponse: 493 raise NotImplementedError() 494 495 def scrobble( 496 self, req: extensions_pb2.ScrobbleRequest 497 ) -> extensions_pb2.ScrobbleResponse: 498 raise NotImplementedError() 499 500 def oauth_callback( 501 self, req: extensions_pb2.OauthCallbackRequest 502 ) -> extensions_pb2.OauthCallbackResponse: 503 raise NotImplementedError() 504 505 def get_song_context_menu( 506 self, req: extensions_pb2.RequestedSongContextMenuRequest 507 ) -> extensions_pb2.RequestedSongContextMenuResponse: 508 raise NotImplementedError() 509 510 def get_playlist_context_menu( 511 self, req: extensions_pb2.RequestedPlaylistContextMenuRequest 512 ) -> extensions_pb2.RequestedPlaylistContextMenuResponse: 513 raise NotImplementedError() 514 515 def on_context_menu_action( 516 self, req: extensions_pb2.ContextMenuActionRequest 517 ) -> extensions_pb2.ContextMenuActionResponse: 518 raise NotImplementedError() 519 520 def get_lyrics( 521 self, req: extensions_pb2.RequestedLyricsRequest 522 ) -> extensions_pb2.RequestedLyricsResponse: 523 raise NotImplementedError() 524 525 526extension_instance: Optional[Extension] = None 527 528 529def register_extension(extension: Extension): 530 global extension_instance 531 extension_instance = extension 532 533 534def ensure_extension_instance() -> Extension: 535 if extension_instance is None: 536 raise Exception("Extension instance is not initialized") 537 return extension_instance 538 539 540@extism.plugin_fn 541def handle_extension_command(): 542 input_data = extism.input_bytes() 543 cmd = extensions_pb2.ExtensionCommand() 544 cmd.ParseFromString(input_data) 545 546 instance = ensure_extension_instance() 547 response = extensions_pb2.ExtensionCommandResponse() 548 549 which = cmd.WhichOneof("event") 550 if which is None: 551 return 0 552 553 field_descriptor = cmd.DESCRIPTOR.fields_by_name[which] 554 number = field_descriptor.number 555 556 if number == extensions_pb2.ExtensionCommand.GET_PROVIDER_SCOPES_FIELD_NUMBER: 557 response.get_provider_scopes.CopyFrom( 558 instance.get_provider_scopes(cmd.get_provider_scopes) 559 ) 560 elif number == extensions_pb2.ExtensionCommand.REQUESTED_PLAYLISTS_FIELD_NUMBER: 561 response.requested_playlists.CopyFrom( 562 instance.get_playlists(cmd.requested_playlists) 563 ) 564 elif ( 565 number == extensions_pb2.ExtensionCommand.REQUESTED_PLAYLIST_SONGS_FIELD_NUMBER 566 ): 567 response.requested_playlist_songs.CopyFrom( 568 instance.get_playlist_content(cmd.requested_playlist_songs) 569 ) 570 elif number == extensions_pb2.ExtensionCommand.OAUTH_CALLBACK_FIELD_NUMBER: 571 response.oauth_callback.CopyFrom(instance.oauth_callback(cmd.oauth_callback)) 572 elif number == extensions_pb2.ExtensionCommand.SONG_QUEUE_CHANGED_FIELD_NUMBER: 573 response.song_queue_changed.CopyFrom( 574 instance.on_queue_changed(cmd.song_queue_changed) 575 ) 576 elif number == extensions_pb2.ExtensionCommand.SEEKED_FIELD_NUMBER: 577 response.seeked.CopyFrom(instance.on_seeked(cmd.seeked)) 578 elif number == extensions_pb2.ExtensionCommand.VOLUME_CHANGED_FIELD_NUMBER: 579 response.volume_changed.CopyFrom(instance.on_volume_changed(cmd.volume_changed)) 580 elif number == extensions_pb2.ExtensionCommand.PLAYER_STATE_CHANGED_FIELD_NUMBER: 581 response.player_state_changed.CopyFrom( 582 instance.on_player_state_changed(cmd.player_state_changed) 583 ) 584 elif number == extensions_pb2.ExtensionCommand.SONG_CHANGED_FIELD_NUMBER: 585 response.song_changed.CopyFrom(instance.on_song_changed(cmd.song_changed)) 586 elif number == extensions_pb2.ExtensionCommand.PREFERENCE_CHANGED_FIELD_NUMBER: 587 response.preference_changed.CopyFrom( 588 instance.on_preferences_changed(cmd.preference_changed) 589 ) 590 elif ( 591 number 592 == extensions_pb2.ExtensionCommand.PLAYBACK_DETAILS_REQUESTED_FIELD_NUMBER 593 ): 594 response.playback_details_requested.CopyFrom( 595 instance.get_playback_details(cmd.playback_details_requested) 596 ) 597 elif number == extensions_pb2.ExtensionCommand.CUSTOM_REQUEST_FIELD_NUMBER: 598 response.custom_request.CopyFrom( 599 instance.handle_custom_request(cmd.custom_request) 600 ) 601 elif number == extensions_pb2.ExtensionCommand.REQUESTED_SONG_FROM_URL_FIELD_NUMBER: 602 response.requested_song_from_url.CopyFrom( 603 instance.get_song_from_url(cmd.requested_song_from_url) 604 ) 605 elif ( 606 number 607 == extensions_pb2.ExtensionCommand.REQUESTED_PLAYLIST_FROM_URL_FIELD_NUMBER 608 ): 609 response.requested_playlist_from_url.CopyFrom( 610 instance.get_playlist_from_url(cmd.requested_playlist_from_url) 611 ) 612 elif number == extensions_pb2.ExtensionCommand.REQUESTED_SEARCH_RESULT_FIELD_NUMBER: 613 response.requested_search_result.CopyFrom( 614 instance.get_search(cmd.requested_search_result) 615 ) 616 elif ( 617 number == extensions_pb2.ExtensionCommand.REQUESTED_RECOMMENDATIONS_FIELD_NUMBER 618 ): 619 response.requested_recommendations.CopyFrom( 620 instance.get_recommendations(cmd.requested_recommendations) 621 ) 622 elif number == extensions_pb2.ExtensionCommand.REQUESTED_LYRICS_FIELD_NUMBER: 623 response.requested_lyrics.CopyFrom(instance.get_lyrics(cmd.requested_lyrics)) 624 elif number == extensions_pb2.ExtensionCommand.REQUESTED_ARTIST_SONGS_FIELD_NUMBER: 625 response.requested_artist_songs.CopyFrom( 626 instance.get_artist_songs(cmd.requested_artist_songs) 627 ) 628 elif number == extensions_pb2.ExtensionCommand.REQUESTED_ALBUM_SONGS_FIELD_NUMBER: 629 response.requested_album_songs.CopyFrom( 630 instance.get_album_songs(cmd.requested_album_songs) 631 ) 632 elif number == extensions_pb2.ExtensionCommand.SONG_ADDED_FIELD_NUMBER: 633 response.song_added.CopyFrom(instance.on_song_added(cmd.song_added)) 634 elif number == extensions_pb2.ExtensionCommand.SONG_REMOVED_FIELD_NUMBER: 635 response.song_removed.CopyFrom(instance.on_song_removed(cmd.song_removed)) 636 elif number == extensions_pb2.ExtensionCommand.PLAYLIST_ADDED_FIELD_NUMBER: 637 response.playlist_added.CopyFrom(instance.on_playlist_added(cmd.playlist_added)) 638 elif number == extensions_pb2.ExtensionCommand.PLAYLIST_REMOVED_FIELD_NUMBER: 639 response.playlist_removed.CopyFrom( 640 instance.on_playlist_removed(cmd.playlist_removed) 641 ) 642 elif number == extensions_pb2.ExtensionCommand.REQUESTED_SONG_FROM_ID_FIELD_NUMBER: 643 response.requested_song_from_id.CopyFrom( 644 instance.get_song_from_id(cmd.requested_song_from_id) 645 ) 646 elif number == extensions_pb2.ExtensionCommand.SCROBBLE_FIELD_NUMBER: 647 response.scrobble.CopyFrom(instance.scrobble(cmd.scrobble)) 648 elif ( 649 number 650 == extensions_pb2.ExtensionCommand.REQUESTED_SONG_CONTEXT_MENU_FIELD_NUMBER 651 ): 652 response.requested_song_context_menu.CopyFrom( 653 instance.get_song_context_menu(cmd.requested_song_context_menu) 654 ) 655 elif ( 656 number 657 == extensions_pb2.ExtensionCommand.REQUESTED_PLAYLIST_CONTEXT_MENU_FIELD_NUMBER 658 ): 659 response.requested_playlist_context_menu.CopyFrom( 660 instance.get_playlist_context_menu(cmd.requested_playlist_context_menu) 661 ) 662 elif number == extensions_pb2.ExtensionCommand.CONTEXT_MENU_ACTION_FIELD_NUMBER: 663 response.context_menu_action.CopyFrom( 664 instance.on_context_menu_action(cmd.context_menu_action) 665 ) 666 elif number == extensions_pb2.ExtensionCommand.PERFORM_ACCOUNT_LOGIN_FIELD_NUMBER: 667 response.perform_account_login.CopyFrom( 668 instance.perform_account_login(cmd.perform_account_login) 669 ) 670 671 extism.output_bytes(response.SerializeToString()) 672 673 674# Import extension module at the end to avoid circular dependency issues 675# and ensure SDK classes are defined before extension tries to use them. 676# The extension module is expected to be named 'main' (main.py). 677extension_module = None 678try: 679 import sdk_main as extension_module 680except ImportError as e: 681 print(f"WARNING: Could not import 'main' module: {e}") 682 683 684@extism.plugin_fn 685def entry(): 686 sys.stdout = CustomPrint() 687 print("PYTHON: entry() called") 688 if extension_module: 689 if hasattr(extension_module, "entry"): 690 print("PYTHON: Calling extension_module.entry()") 691 extension_module.entry() 692 else: 693 print("WARNING: 'main' module does not have an 'entry' function") 694 else: 695 raise Exception("Extension module not loaded") 696 697 698def duration_to_proto(td: timedelta) -> Duration: 699 """Convert datetime.timedelta to google.protobuf.Duration.""" 700 duration = Duration() 701 duration.seconds = int(td.total_seconds()) 702 duration.nanos = int((td.microseconds % 1000000) * 1000) 703 return duration
@extism.import_fn('extism:host/user', 'open_clientfd')
def
open_sock(path: str) -> int:
@extism.import_fn('extism:host/user', 'system_time')
def
system_time() -> int:
@extism.import_fn('extism:host/user', 'write_sock')
def
write_sock(sock_id: int, buf: bytes) -> int:
@extism.import_fn('extism:host/user', 'read_sock')
def
read_sock(sock_id: int, read_len: int) -> bytes:
@extism.import_fn('extism:host/user', 'hash')
def
hash(hash_type: str, data: bytes) -> bytes:
@extism.import_fn('extism:host/user', 'send_main_command')
def
send_main_command(data: int) -> int:
@extism.import_fn('extism:host/user', 'batch_http_request')
def
batch_http_request_host(data: int) -> int:
class
CustomPrint:
class
HttpRequest:
62class HttpRequest: 63 def __init__( 64 self, 65 url: str, 66 method: str = "GET", 67 headers: Optional[Dict[str, str]] = None, 68 body: Optional[Union[bytes, str]] = None, 69 timeout_ms: Optional[int] = None, 70 ): 71 self.url = url 72 self.method = method 73 self.headers = headers or {} 74 if isinstance(body, str): 75 self.body = body.encode("utf-8") 76 else: 77 self.body = body 78 self.timeout_ms = timeout_ms 79 80 def to_proto(self) -> extensions_pb2.HttpRequest: 81 return extensions_pb2.HttpRequest( 82 url=self.url, 83 method=self.method.upper(), 84 headers=self.headers, 85 body=self.body, 86 timeout_ms=self.timeout_ms, 87 )
HttpRequest( url: str, method: str = 'GET', headers: Optional[Dict[str, str]] = None, body: Union[bytes, str, NoneType] = None, timeout_ms: Optional[int] = None)
63 def __init__( 64 self, 65 url: str, 66 method: str = "GET", 67 headers: Optional[Dict[str, str]] = None, 68 body: Optional[Union[bytes, str]] = None, 69 timeout_ms: Optional[int] = None, 70 ): 71 self.url = url 72 self.method = method 73 self.headers = headers or {} 74 if isinstance(body, str): 75 self.body = body.encode("utf-8") 76 else: 77 self.body = body 78 self.timeout_ms = timeout_ms
class
HttpResponse:
90class HttpResponse: 91 def __init__(self, proto: extensions_pb2.HttpResponse): 92 self.status_code: int = proto.status_code 93 self.status_text: str = proto.status_text 94 self.headers: dict = dict(proto.headers) 95 self.body: bytes = proto.body 96 97 @property 98 def ok(self) -> bool: 99 return 200 <= self.status_code < 300 100 101 @property 102 def text(self) -> str: 103 return self.body.decode("utf-8", errors="replace") 104 105 def json(self) -> Any: 106 import json 107 return json.loads(self.text)
class
HttpResult:
110class HttpResult: 111 def __init__(self, proto: extensions_pb2.HttpResult): 112 which = proto.WhichOneof("result") 113 if which == "response": 114 self.response: Optional[HttpResponse] = HttpResponse(proto.response) 115 self.error: Optional[str] = None 116 elif which == "error": 117 self.response = None 118 self.error = proto.error 119 else: 120 self.response = None 121 self.error = "Empty HTTP result"
HttpResult(proto: core.types.protos.extensions_pb2.HttpResult)
111 def __init__(self, proto: extensions_pb2.HttpResult): 112 which = proto.WhichOneof("result") 113 if which == "response": 114 self.response: Optional[HttpResponse] = HttpResponse(proto.response) 115 self.error: Optional[str] = None 116 elif which == "error": 117 self.response = None 118 self.error = proto.error 119 else: 120 self.response = None 121 self.error = "Empty HTTP result"
def
http_batch_request( requests: Sequence[Union[HttpRequest, Dict[str, Any]]]) -> List[HttpResponse]:
124def http_batch_request(requests: Sequence[Union[HttpRequest, Dict[str, Any]]]) -> List[HttpResponse]: 125 if not requests: 126 return [] 127 proto_reqs = [] 128 for r in requests: 129 if isinstance(r, HttpRequest): 130 proto_reqs.append(r.to_proto()) 131 elif isinstance(r, dict): 132 req_obj = HttpRequest(**r) 133 proto_reqs.append(req_obj.to_proto()) 134 else: 135 raise ValueError(f"Invalid request type: {type(r)}") 136 137 batch = extensions_pb2.BatchHttpRequest(requests=proto_reqs) 138 data = batch.SerializeToString() 139 mem = extism.memory.alloc(data) 140 if mem is None: 141 raise Exception("Failed to allocate memory for HTTP batch request") 142 res_offset = batch_http_request_host(mem.offset) 143 144 res_mem_handle = extism.memory.find(res_offset) 145 if res_mem_handle is None: 146 raise Exception(f"Failed to find memory for batch HTTP response at offset {res_offset}") 147 148 res_bytes = extism.memory.bytes(res_mem_handle) 149 extism.memory.free(mem) 150 151 batch_resp = extensions_pb2.BatchHttpResponse() 152 batch_resp.ParseFromString(res_bytes) 153 154 if batch_resp.HasField("error") and batch_resp.error: 155 raise Exception(batch_resp.error) 156 157 responses = [] 158 for idx, item in enumerate(batch_resp.responses): 159 which = item.WhichOneof("result") 160 if which == "response": 161 responses.append(HttpResponse(item.response)) 162 elif which == "error": 163 raise Exception(f"Request #{idx} failed: {item.error}") 164 else: 165 raise Exception(f"Request #{idx} returned empty HTTP result") 166 return responses
def
http_batch_get( urls: List[str], headers: Optional[Dict[str, str]] = None, timeout_ms: Optional[int] = None) -> List[HttpResponse]:
def
http_request( url: str, method: str = 'GET', body: Union[bytes, str, NoneType] = None, headers: Optional[Dict[str, str]] = None, timeout_ms: Optional[int] = None) -> HttpResponse:
178def http_request( 179 url: str, 180 method: str = "GET", 181 body: Optional[Union[bytes, str]] = None, 182 headers: Optional[Dict[str, str]] = None, 183 timeout_ms: Optional[int] = None, 184) -> HttpResponse: 185 req = HttpRequest( 186 url=url, 187 method=method, 188 headers=headers, 189 body=body, 190 timeout_ms=timeout_ms, 191 ) 192 resps = http_batch_request([req]) 193 if not resps: 194 raise Exception("Host runner returned empty response") 195 return resps[0]
def
http_get( url: str, headers: Optional[Dict[str, str]] = None, timeout_ms: Optional[int] = None) -> HttpResponse:
def
send_main_command_( cmd: core.types.protos.extensions_pb2.MainCommand) -> core.types.protos.extensions_pb2.MainCommandResponse:
206def send_main_command_( 207 cmd: extensions_pb2.MainCommand, 208) -> extensions_pb2.MainCommandResponse: 209 data = cmd.SerializeToString() 210 mem = extism.memory.alloc(data) 211 if mem is None: 212 raise Exception("Failed to allocate memory for main command") 213 res_offset = send_main_command(mem.offset) 214 215 res_mem_handle = extism.memory.find(res_offset) 216 if res_mem_handle is None: 217 raise Exception(f"Failed to find memory for response at offset {res_offset}") 218 219 res_bytes = extism.memory.bytes(res_mem_handle) 220 221 extism.memory.free(mem) 222 223 resp = extensions_pb2.MainCommandResponse() 224 resp.ParseFromString(res_bytes) 225 return resp
class
Api:
228class Api: 229 def get_song(self, options: songs_pb2.GetSongOptions) -> List[songs_pb2.Song]: 230 req = extensions_pb2.MainCommand( 231 get_song=extensions_pb2.GetSongRequest(options=options) 232 ) 233 resp = send_main_command_(req) 234 return list(resp.get_song.songs) 235 236 def get_current_song(self) -> Optional[songs_pb2.Song]: 237 req = extensions_pb2.MainCommand( 238 get_current_song=extensions_pb2.GetCurrentSongRequest() 239 ) 240 resp = send_main_command_(req) 241 if resp.HasField("get_current_song"): 242 if resp.get_current_song.HasField("song"): 243 return resp.get_current_song.song 244 return None 245 246 def get_player_state(self) -> extensions_pb2.PlayerState: 247 req = extensions_pb2.MainCommand( 248 get_player_state=extensions_pb2.GetPlayerStateRequest() 249 ) 250 resp = send_main_command_(req) 251 return resp.get_player_state.state 252 253 def get_volume(self) -> float: 254 req = extensions_pb2.MainCommand(get_volume=extensions_pb2.GetVolumeRequest()) 255 resp = send_main_command_(req) 256 return resp.get_volume.volume 257 258 def get_time(self) -> float: 259 req = extensions_pb2.MainCommand(get_time=extensions_pb2.GetTimeRequest()) 260 resp = send_main_command_(req) 261 return resp.get_time.time 262 263 def get_queue(self) -> Any: # Returns Struct 264 req = extensions_pb2.MainCommand(get_queue=extensions_pb2.GetQueueRequest()) 265 resp = send_main_command_(req) 266 return resp.get_queue.queue 267 268 def get_preference( 269 self, data: extensions_pb2.PreferenceData 270 ) -> extensions_pb2.PreferenceData: 271 req = extensions_pb2.MainCommand( 272 get_preference=extensions_pb2.GetPreferenceRequest(data=data) 273 ) 274 resp = send_main_command_(req) 275 return resp.get_preference.data 276 277 def get_secure( 278 self, data: extensions_pb2.PreferenceData 279 ) -> extensions_pb2.PreferenceData: 280 req = extensions_pb2.MainCommand( 281 get_secure=extensions_pb2.GetSecureRequest(data=data) 282 ) 283 resp = send_main_command_(req) 284 return resp.get_secure.data 285 286 def set_preference(self, data: extensions_pb2.PreferenceData) -> bool: 287 req = extensions_pb2.MainCommand( 288 set_preference=extensions_pb2.SetPreferenceRequest(data=data) 289 ) 290 resp = send_main_command_(req) 291 return resp.set_preference.success 292 293 def set_secure(self, data: extensions_pb2.PreferenceData) -> bool: 294 req = extensions_pb2.MainCommand( 295 set_secure=extensions_pb2.SetSecureRequest(data=data) 296 ) 297 resp = send_main_command_(req) 298 return resp.set_secure.success 299 300 def add_songs(self, songs: List[songs_pb2.Song]) -> List[songs_pb2.Song]: 301 req = extensions_pb2.MainCommand( 302 add_songs=extensions_pb2.AddSongsRequest(songs=songs) 303 ) 304 resp = send_main_command_(req) 305 return list(resp.add_songs.songs) 306 307 def remove_song(self, song: songs_pb2.Song) -> bool: 308 req = extensions_pb2.MainCommand( 309 remove_song=extensions_pb2.RemoveSongRequest(song=song) 310 ) 311 resp = send_main_command_(req) 312 return resp.remove_song.success 313 314 def update_song(self, song: songs_pb2.Song) -> songs_pb2.Song: 315 req = extensions_pb2.MainCommand( 316 update_song=extensions_pb2.UpdateSongRequest(song=song) 317 ) 318 resp = send_main_command_(req) 319 return resp.update_song.song 320 321 def add_playlist(self, playlist: songs_pb2.Playlist) -> str: 322 req = extensions_pb2.MainCommand( 323 add_playlist=extensions_pb2.AddPlaylistRequest(playlist=playlist) 324 ) 325 resp = send_main_command_(req) 326 return resp.add_playlist.playlist_id 327 328 def add_to_playlist(self, playlist_id: str, songs: List[songs_pb2.Song]) -> bool: 329 req = extensions_pb2.MainCommand( 330 add_to_playlist=extensions_pb2.AddToPlaylistRequest( 331 playlist_id=playlist_id, songs=songs 332 ) 333 ) 334 resp = send_main_command_(req) 335 return resp.add_to_playlist.success 336 337 def register_oauth(self, url: str) -> bool: 338 req = extensions_pb2.MainCommand( 339 register_oauth=extensions_pb2.RegisterOauthRequest(url=url) 340 ) 341 resp = send_main_command_(req) 342 return resp.register_oauth.success 343 344 def open_external_url(self, url: str) -> bool: 345 req = extensions_pb2.MainCommand( 346 open_external_url=extensions_pb2.OpenExternalUrlRequest(url=url) 347 ) 348 resp = send_main_command_(req) 349 return resp.open_external_url.success 350 351 def set_account(self, account: extensions_pb2.ExtensionAccountDetail) -> None: 352 req = extensions_pb2.MainCommand( 353 set_account=extensions_pb2.SetAccountRequest(account=account) 354 ) 355 resp = send_main_command_(req) 356 if not resp.HasField("set_account"): 357 raise Exception("Failed to set account") 358 359 def register_user_preferences(self, prefs: List[preferences_pb2.PreferenceItem]) -> bool: 360 req = extensions_pb2.MainCommand( 361 register_user_preference=extensions_pb2.RegisterUserPreferenceRequest( 362 prefs=prefs 363 ) 364 ) 365 resp = send_main_command_(req) 366 return resp.register_user_preference.success 367 368 def unregister_user_preferences(self, keys: List[str]) -> bool: 369 req = extensions_pb2.MainCommand( 370 unregister_user_preference=extensions_pb2.UnregisterUserPreferenceRequest( 371 keys=keys 372 ) 373 ) 374 resp = send_main_command_(req) 375 return resp.unregister_user_preference.success
def
get_song( self, options: core.types.protos.songs_pb2.GetSongOptions) -> List[core.types.protos.songs_pb2.Song]:
def
get_current_song(self) -> Optional[core.types.protos.songs_pb2.Song]:
236 def get_current_song(self) -> Optional[songs_pb2.Song]: 237 req = extensions_pb2.MainCommand( 238 get_current_song=extensions_pb2.GetCurrentSongRequest() 239 ) 240 resp = send_main_command_(req) 241 if resp.HasField("get_current_song"): 242 if resp.get_current_song.HasField("song"): 243 return resp.get_current_song.song 244 return None
def
get_player_state( self) -> <google.protobuf.internal.enum_type_wrapper.EnumTypeWrapper object at 0x7ff21271ef90>:
def
get_preference( self, data: core.types.protos.extensions_pb2.PreferenceData) -> core.types.protos.extensions_pb2.PreferenceData:
def
get_secure( self, data: core.types.protos.extensions_pb2.PreferenceData) -> core.types.protos.extensions_pb2.PreferenceData:
def
add_songs( self, songs: List[core.types.protos.songs_pb2.Song]) -> List[core.types.protos.songs_pb2.Song]:
def
add_to_playlist( self, playlist_id: str, songs: List[core.types.protos.songs_pb2.Song]) -> bool:
328 def add_to_playlist(self, playlist_id: str, songs: List[songs_pb2.Song]) -> bool: 329 req = extensions_pb2.MainCommand( 330 add_to_playlist=extensions_pb2.AddToPlaylistRequest( 331 playlist_id=playlist_id, songs=songs 332 ) 333 ) 334 resp = send_main_command_(req) 335 return resp.add_to_playlist.success
def
set_account( self, account: core.types.protos.extensions_pb2.ExtensionAccountDetail) -> None:
351 def set_account(self, account: extensions_pb2.ExtensionAccountDetail) -> None: 352 req = extensions_pb2.MainCommand( 353 set_account=extensions_pb2.SetAccountRequest(account=account) 354 ) 355 resp = send_main_command_(req) 356 if not resp.HasField("set_account"): 357 raise Exception("Failed to set account")
def
register_user_preferences( self, prefs: List[core.types.protos.preferences_pb2.PreferenceItem]) -> bool:
359 def register_user_preferences(self, prefs: List[preferences_pb2.PreferenceItem]) -> bool: 360 req = extensions_pb2.MainCommand( 361 register_user_preference=extensions_pb2.RegisterUserPreferenceRequest( 362 prefs=prefs 363 ) 364 ) 365 resp = send_main_command_(req) 366 return resp.register_user_preference.success
class
Extension:
378class Extension: 379 api = Api() 380 381 def get_provider_scopes( 382 self, _: extensions_pb2.GetProviderScopesRequest 383 ) -> extensions_pb2.GetProviderScopesResponse: 384 return extensions_pb2.GetProviderScopesResponse() 385 386 def get_playlists( 387 self, req: extensions_pb2.RequestedPlaylistsRequest 388 ) -> extensions_pb2.RequestedPlaylistsResponse: 389 raise NotImplementedError() 390 391 def get_playlist_content( 392 self, req: extensions_pb2.RequestedPlaylistSongsRequest 393 ) -> extensions_pb2.RequestedPlaylistSongsResponse: 394 raise NotImplementedError() 395 396 def get_playlist_from_url( 397 self, req: extensions_pb2.RequestedPlaylistFromUrlRequest 398 ) -> extensions_pb2.RequestedPlaylistFromUrlResponse: 399 raise NotImplementedError() 400 401 def get_playback_details( 402 self, req: extensions_pb2.PlaybackDetailsRequestedRequest 403 ) -> extensions_pb2.PlaybackDetailsRequestedResponse: 404 raise NotImplementedError() 405 406 def get_search( 407 self, req: extensions_pb2.RequestedSearchResultRequest 408 ) -> extensions_pb2.RequestedSearchResultResponse: 409 raise NotImplementedError() 410 411 def get_recommendations( 412 self, req: extensions_pb2.RequestedRecommendationsRequest 413 ) -> extensions_pb2.RequestedRecommendationsResponse: 414 raise NotImplementedError() 415 416 def get_song_from_url( 417 self, req: extensions_pb2.RequestedSongFromUrlRequest 418 ) -> extensions_pb2.RequestedSongFromUrlResponse: 419 raise NotImplementedError() 420 421 def handle_custom_request( 422 self, req: extensions_pb2.CustomRequest 423 ) -> extensions_pb2.CustomRequestResponse: 424 raise NotImplementedError() 425 426 def get_artist_songs( 427 self, req: extensions_pb2.RequestedArtistSongsRequest 428 ) -> extensions_pb2.RequestedArtistSongsResponse: 429 raise NotImplementedError() 430 431 def get_album_songs( 432 self, req: extensions_pb2.RequestedAlbumSongsRequest 433 ) -> extensions_pb2.RequestedAlbumSongsResponse: 434 raise NotImplementedError() 435 436 def get_song_from_id( 437 self, req: extensions_pb2.RequestedSongFromIdRequest 438 ) -> extensions_pb2.RequestedSongFromIdResponse: 439 raise NotImplementedError() 440 441 def on_queue_changed( 442 self, req: extensions_pb2.SongQueueChangedRequest 443 ) -> extensions_pb2.SongQueueChangedResponse: 444 raise NotImplementedError() 445 446 def on_volume_changed( 447 self, req: extensions_pb2.VolumeChangedRequest 448 ) -> extensions_pb2.VolumeChangedResponse: 449 raise NotImplementedError() 450 451 def on_player_state_changed( 452 self, req: extensions_pb2.PlayerStateChangedRequest 453 ) -> extensions_pb2.PlayerStateChangedResponse: 454 raise NotImplementedError() 455 456 def on_song_changed( 457 self, req: extensions_pb2.SongChangedRequest 458 ) -> extensions_pb2.SongChangedResponse: 459 raise NotImplementedError() 460 461 def on_seeked( 462 self, req: extensions_pb2.SeekedRequest 463 ) -> extensions_pb2.SeekedResponse: 464 raise NotImplementedError() 465 466 def on_preferences_changed( 467 self, req: extensions_pb2.PreferenceChangedRequest 468 ) -> extensions_pb2.PreferenceChangedResponse: 469 raise NotImplementedError() 470 471 def on_song_added( 472 self, req: extensions_pb2.SongAddedRequest 473 ) -> extensions_pb2.SongAddedResponse: 474 raise NotImplementedError() 475 476 def on_song_removed( 477 self, req: extensions_pb2.SongRemovedRequest 478 ) -> extensions_pb2.SongRemovedResponse: 479 raise NotImplementedError() 480 481 def on_playlist_added( 482 self, req: extensions_pb2.PlaylistAddedRequest 483 ) -> extensions_pb2.PlaylistAddedResponse: 484 raise NotImplementedError() 485 486 def on_playlist_removed( 487 self, req: extensions_pb2.PlaylistRemovedRequest 488 ) -> extensions_pb2.PlaylistRemovedResponse: 489 raise NotImplementedError() 490 491 def perform_account_login( 492 self, req: extensions_pb2.PerformAccountLoginRequest 493 ) -> extensions_pb2.PerformAccountLoginResponse: 494 raise NotImplementedError() 495 496 def scrobble( 497 self, req: extensions_pb2.ScrobbleRequest 498 ) -> extensions_pb2.ScrobbleResponse: 499 raise NotImplementedError() 500 501 def oauth_callback( 502 self, req: extensions_pb2.OauthCallbackRequest 503 ) -> extensions_pb2.OauthCallbackResponse: 504 raise NotImplementedError() 505 506 def get_song_context_menu( 507 self, req: extensions_pb2.RequestedSongContextMenuRequest 508 ) -> extensions_pb2.RequestedSongContextMenuResponse: 509 raise NotImplementedError() 510 511 def get_playlist_context_menu( 512 self, req: extensions_pb2.RequestedPlaylistContextMenuRequest 513 ) -> extensions_pb2.RequestedPlaylistContextMenuResponse: 514 raise NotImplementedError() 515 516 def on_context_menu_action( 517 self, req: extensions_pb2.ContextMenuActionRequest 518 ) -> extensions_pb2.ContextMenuActionResponse: 519 raise NotImplementedError() 520 521 def get_lyrics( 522 self, req: extensions_pb2.RequestedLyricsRequest 523 ) -> extensions_pb2.RequestedLyricsResponse: 524 raise NotImplementedError()
api =
<Api object>
def
get_provider_scopes( self, _: core.types.protos.extensions_pb2.GetProviderScopesRequest) -> core.types.protos.extensions_pb2.GetProviderScopesResponse:
def
get_playlists( self, req: core.types.protos.extensions_pb2.RequestedPlaylistsRequest) -> core.types.protos.extensions_pb2.RequestedPlaylistsResponse:
def
get_playlist_content( self, req: core.types.protos.extensions_pb2.RequestedPlaylistSongsRequest) -> core.types.protos.extensions_pb2.RequestedPlaylistSongsResponse:
def
get_playlist_from_url( self, req: core.types.protos.extensions_pb2.RequestedPlaylistFromUrlRequest) -> core.types.protos.extensions_pb2.RequestedPlaylistFromUrlResponse:
def
get_playback_details( self, req: core.types.protos.extensions_pb2.PlaybackDetailsRequestedRequest) -> core.types.protos.extensions_pb2.PlaybackDetailsRequestedResponse:
def
get_search( self, req: core.types.protos.extensions_pb2.RequestedSearchResultRequest) -> core.types.protos.extensions_pb2.RequestedSearchResultResponse:
def
get_recommendations( self, req: core.types.protos.extensions_pb2.RequestedRecommendationsRequest) -> core.types.protos.extensions_pb2.RequestedRecommendationsResponse:
def
get_song_from_url( self, req: core.types.protos.extensions_pb2.RequestedSongFromUrlRequest) -> core.types.protos.extensions_pb2.RequestedSongFromUrlResponse:
def
handle_custom_request( self, req: core.types.protos.extensions_pb2.CustomRequest) -> core.types.protos.extensions_pb2.CustomRequestResponse:
def
get_artist_songs( self, req: core.types.protos.extensions_pb2.RequestedArtistSongsRequest) -> core.types.protos.extensions_pb2.RequestedArtistSongsResponse:
def
get_album_songs( self, req: core.types.protos.extensions_pb2.RequestedAlbumSongsRequest) -> core.types.protos.extensions_pb2.RequestedAlbumSongsResponse:
def
get_song_from_id( self, req: core.types.protos.extensions_pb2.RequestedSongFromIdRequest) -> core.types.protos.extensions_pb2.RequestedSongFromIdResponse:
def
on_queue_changed( self, req: core.types.protos.extensions_pb2.SongQueueChangedRequest) -> core.types.protos.extensions_pb2.SongQueueChangedResponse:
def
on_volume_changed( self, req: core.types.protos.extensions_pb2.VolumeChangedRequest) -> core.types.protos.extensions_pb2.VolumeChangedResponse:
def
on_player_state_changed( self, req: core.types.protos.extensions_pb2.PlayerStateChangedRequest) -> core.types.protos.extensions_pb2.PlayerStateChangedResponse:
def
on_song_changed( self, req: core.types.protos.extensions_pb2.SongChangedRequest) -> core.types.protos.extensions_pb2.SongChangedResponse:
def
on_seeked( self, req: core.types.protos.extensions_pb2.SeekedRequest) -> core.types.protos.extensions_pb2.SeekedResponse:
def
on_preferences_changed( self, req: core.types.protos.extensions_pb2.PreferenceChangedRequest) -> core.types.protos.extensions_pb2.PreferenceChangedResponse:
def
on_song_added( self, req: core.types.protos.extensions_pb2.SongAddedRequest) -> core.types.protos.extensions_pb2.SongAddedResponse:
def
on_song_removed( self, req: core.types.protos.extensions_pb2.SongRemovedRequest) -> core.types.protos.extensions_pb2.SongRemovedResponse:
def
on_playlist_added( self, req: core.types.protos.extensions_pb2.PlaylistAddedRequest) -> core.types.protos.extensions_pb2.PlaylistAddedResponse:
def
on_playlist_removed( self, req: core.types.protos.extensions_pb2.PlaylistRemovedRequest) -> core.types.protos.extensions_pb2.PlaylistRemovedResponse:
def
perform_account_login( self, req: core.types.protos.extensions_pb2.PerformAccountLoginRequest) -> core.types.protos.extensions_pb2.PerformAccountLoginResponse:
def
scrobble( self, req: core.types.protos.extensions_pb2.ScrobbleRequest) -> core.types.protos.extensions_pb2.ScrobbleResponse:
@extism.plugin_fn
def
handle_extension_command():
541@extism.plugin_fn 542def handle_extension_command(): 543 input_data = extism.input_bytes() 544 cmd = extensions_pb2.ExtensionCommand() 545 cmd.ParseFromString(input_data) 546 547 instance = ensure_extension_instance() 548 response = extensions_pb2.ExtensionCommandResponse() 549 550 which = cmd.WhichOneof("event") 551 if which is None: 552 return 0 553 554 field_descriptor = cmd.DESCRIPTOR.fields_by_name[which] 555 number = field_descriptor.number 556 557 if number == extensions_pb2.ExtensionCommand.GET_PROVIDER_SCOPES_FIELD_NUMBER: 558 response.get_provider_scopes.CopyFrom( 559 instance.get_provider_scopes(cmd.get_provider_scopes) 560 ) 561 elif number == extensions_pb2.ExtensionCommand.REQUESTED_PLAYLISTS_FIELD_NUMBER: 562 response.requested_playlists.CopyFrom( 563 instance.get_playlists(cmd.requested_playlists) 564 ) 565 elif ( 566 number == extensions_pb2.ExtensionCommand.REQUESTED_PLAYLIST_SONGS_FIELD_NUMBER 567 ): 568 response.requested_playlist_songs.CopyFrom( 569 instance.get_playlist_content(cmd.requested_playlist_songs) 570 ) 571 elif number == extensions_pb2.ExtensionCommand.OAUTH_CALLBACK_FIELD_NUMBER: 572 response.oauth_callback.CopyFrom(instance.oauth_callback(cmd.oauth_callback)) 573 elif number == extensions_pb2.ExtensionCommand.SONG_QUEUE_CHANGED_FIELD_NUMBER: 574 response.song_queue_changed.CopyFrom( 575 instance.on_queue_changed(cmd.song_queue_changed) 576 ) 577 elif number == extensions_pb2.ExtensionCommand.SEEKED_FIELD_NUMBER: 578 response.seeked.CopyFrom(instance.on_seeked(cmd.seeked)) 579 elif number == extensions_pb2.ExtensionCommand.VOLUME_CHANGED_FIELD_NUMBER: 580 response.volume_changed.CopyFrom(instance.on_volume_changed(cmd.volume_changed)) 581 elif number == extensions_pb2.ExtensionCommand.PLAYER_STATE_CHANGED_FIELD_NUMBER: 582 response.player_state_changed.CopyFrom( 583 instance.on_player_state_changed(cmd.player_state_changed) 584 ) 585 elif number == extensions_pb2.ExtensionCommand.SONG_CHANGED_FIELD_NUMBER: 586 response.song_changed.CopyFrom(instance.on_song_changed(cmd.song_changed)) 587 elif number == extensions_pb2.ExtensionCommand.PREFERENCE_CHANGED_FIELD_NUMBER: 588 response.preference_changed.CopyFrom( 589 instance.on_preferences_changed(cmd.preference_changed) 590 ) 591 elif ( 592 number 593 == extensions_pb2.ExtensionCommand.PLAYBACK_DETAILS_REQUESTED_FIELD_NUMBER 594 ): 595 response.playback_details_requested.CopyFrom( 596 instance.get_playback_details(cmd.playback_details_requested) 597 ) 598 elif number == extensions_pb2.ExtensionCommand.CUSTOM_REQUEST_FIELD_NUMBER: 599 response.custom_request.CopyFrom( 600 instance.handle_custom_request(cmd.custom_request) 601 ) 602 elif number == extensions_pb2.ExtensionCommand.REQUESTED_SONG_FROM_URL_FIELD_NUMBER: 603 response.requested_song_from_url.CopyFrom( 604 instance.get_song_from_url(cmd.requested_song_from_url) 605 ) 606 elif ( 607 number 608 == extensions_pb2.ExtensionCommand.REQUESTED_PLAYLIST_FROM_URL_FIELD_NUMBER 609 ): 610 response.requested_playlist_from_url.CopyFrom( 611 instance.get_playlist_from_url(cmd.requested_playlist_from_url) 612 ) 613 elif number == extensions_pb2.ExtensionCommand.REQUESTED_SEARCH_RESULT_FIELD_NUMBER: 614 response.requested_search_result.CopyFrom( 615 instance.get_search(cmd.requested_search_result) 616 ) 617 elif ( 618 number == extensions_pb2.ExtensionCommand.REQUESTED_RECOMMENDATIONS_FIELD_NUMBER 619 ): 620 response.requested_recommendations.CopyFrom( 621 instance.get_recommendations(cmd.requested_recommendations) 622 ) 623 elif number == extensions_pb2.ExtensionCommand.REQUESTED_LYRICS_FIELD_NUMBER: 624 response.requested_lyrics.CopyFrom(instance.get_lyrics(cmd.requested_lyrics)) 625 elif number == extensions_pb2.ExtensionCommand.REQUESTED_ARTIST_SONGS_FIELD_NUMBER: 626 response.requested_artist_songs.CopyFrom( 627 instance.get_artist_songs(cmd.requested_artist_songs) 628 ) 629 elif number == extensions_pb2.ExtensionCommand.REQUESTED_ALBUM_SONGS_FIELD_NUMBER: 630 response.requested_album_songs.CopyFrom( 631 instance.get_album_songs(cmd.requested_album_songs) 632 ) 633 elif number == extensions_pb2.ExtensionCommand.SONG_ADDED_FIELD_NUMBER: 634 response.song_added.CopyFrom(instance.on_song_added(cmd.song_added)) 635 elif number == extensions_pb2.ExtensionCommand.SONG_REMOVED_FIELD_NUMBER: 636 response.song_removed.CopyFrom(instance.on_song_removed(cmd.song_removed)) 637 elif number == extensions_pb2.ExtensionCommand.PLAYLIST_ADDED_FIELD_NUMBER: 638 response.playlist_added.CopyFrom(instance.on_playlist_added(cmd.playlist_added)) 639 elif number == extensions_pb2.ExtensionCommand.PLAYLIST_REMOVED_FIELD_NUMBER: 640 response.playlist_removed.CopyFrom( 641 instance.on_playlist_removed(cmd.playlist_removed) 642 ) 643 elif number == extensions_pb2.ExtensionCommand.REQUESTED_SONG_FROM_ID_FIELD_NUMBER: 644 response.requested_song_from_id.CopyFrom( 645 instance.get_song_from_id(cmd.requested_song_from_id) 646 ) 647 elif number == extensions_pb2.ExtensionCommand.SCROBBLE_FIELD_NUMBER: 648 response.scrobble.CopyFrom(instance.scrobble(cmd.scrobble)) 649 elif ( 650 number 651 == extensions_pb2.ExtensionCommand.REQUESTED_SONG_CONTEXT_MENU_FIELD_NUMBER 652 ): 653 response.requested_song_context_menu.CopyFrom( 654 instance.get_song_context_menu(cmd.requested_song_context_menu) 655 ) 656 elif ( 657 number 658 == extensions_pb2.ExtensionCommand.REQUESTED_PLAYLIST_CONTEXT_MENU_FIELD_NUMBER 659 ): 660 response.requested_playlist_context_menu.CopyFrom( 661 instance.get_playlist_context_menu(cmd.requested_playlist_context_menu) 662 ) 663 elif number == extensions_pb2.ExtensionCommand.CONTEXT_MENU_ACTION_FIELD_NUMBER: 664 response.context_menu_action.CopyFrom( 665 instance.on_context_menu_action(cmd.context_menu_action) 666 ) 667 elif number == extensions_pb2.ExtensionCommand.PERFORM_ACCOUNT_LOGIN_FIELD_NUMBER: 668 response.perform_account_login.CopyFrom( 669 instance.perform_account_login(cmd.perform_account_login) 670 ) 671 672 extism.output_bytes(response.SerializeToString())
extension_module =
None
@extism.plugin_fn
def
entry():
685@extism.plugin_fn 686def entry(): 687 sys.stdout = CustomPrint() 688 print("PYTHON: entry() called") 689 if extension_module: 690 if hasattr(extension_module, "entry"): 691 print("PYTHON: Calling extension_module.entry()") 692 extension_module.entry() 693 else: 694 print("WARNING: 'main' module does not have an 'entry' function") 695 else: 696 raise Exception("Extension module not loaded")
def
duration_to_proto(td: datetime.timedelta) -> google.protobuf.duration_pb2.Duration:
699def duration_to_proto(td: timedelta) -> Duration: 700 """Convert datetime.timedelta to google.protobuf.Duration.""" 701 duration = Duration() 702 duration.seconds = int(td.total_seconds()) 703 duration.nanos = int((td.microseconds % 1000000) * 1000) 704 return duration
Convert datetime.timedelta to google.protobuf.Duration.