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