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