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:
21@extism.import_fn("extism:host/user", "open_clientfd")
22def open_sock(path: str) -> int: ...
@extism.import_fn('extism:host/user', 'system_time')
def system_time() -> int:
25@extism.import_fn("extism:host/user", "system_time")
26def system_time() -> int: ...
@extism.import_fn('extism:host/user', 'write_sock')
def write_sock(sock_id: int, buf: bytes) -> int:
29@extism.import_fn("extism:host/user", "write_sock")
30def 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:
33@extism.import_fn("extism:host/user", "read_sock")
34def read_sock(sock_id: int, read_len: int) -> bytes: ...
@extism.import_fn('extism:host/user', 'hash')
def hash(hash_type: str, data: bytes) -> bytes:
37@extism.import_fn("extism:host/user", "hash")
38def hash(hash_type: str, data: bytes) -> bytes: ...
@extism.import_fn('extism:host/user', 'send_main_command')
def send_main_command(data: int) -> int:
41@extism.import_fn("extism:host/user", "send_main_command")
42def send_main_command(data: int) -> int: ...
@extism.import_fn('extism:host/user', 'batch_http_request')
def batch_http_request_host(data: int) -> int:
45@extism.import_fn("extism:host/user", "batch_http_request")
46def batch_http_request_host(data: int) -> int: ...
class CustomPrint:
49class CustomPrint:
50    buf = ""
51
52    def write(self, text):
53        self.buf += text
54        self.flush()
55
56    def flush(self):
57        extism.log(extism.LogLevel.Debug, self.buf)
58        self.buf = ""
buf = ''
def write(self, text):
52    def write(self, text):
53        self.buf += text
54        self.flush()
def flush(self):
56    def flush(self):
57        extism.log(extism.LogLevel.Debug, self.buf)
58        self.buf = ""
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
url
method
headers
timeout_ms
def to_proto(self) -> core.types.protos.extensions_pb2.HttpRequest:
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        )
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)
HttpResponse(proto: core.types.protos.extensions_pb2.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
status_code: int
status_text: str
headers: dict
body: bytes
ok: bool
96    @property
97    def ok(self) -> bool:
98        return 200 <= self.status_code < 300
text: str
100    @property
101    def text(self) -> str:
102        return self.body.decode("utf-8", errors="replace")
def json(self) -> Any:
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]:
168def http_batch_get(
169    urls: List[str],
170    headers: Optional[Dict[str, str]] = None,
171    timeout_ms: Optional[int] = None,
172) -> List[HttpResponse]:
173    reqs = [HttpRequest(url=u, method="GET", headers=headers, timeout_ms=timeout_ms) for u in urls]
174    return http_batch_request(reqs)
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:
197def http_get(
198    url: str,
199    headers: Optional[Dict[str, str]] = None,
200    timeout_ms: Optional[int] = None,
201) -> HttpResponse:
202    return http_request(url=url, method="GET", headers=headers, timeout_ms=timeout_ms)
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]:
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)
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>:
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
def get_volume(self) -> float:
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
def get_time(self) -> float:
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
def get_queue(self) -> Any:
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
def get_preference( self, data: core.types.protos.extensions_pb2.PreferenceData) -> core.types.protos.extensions_pb2.PreferenceData:
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
def get_secure( self, data: core.types.protos.extensions_pb2.PreferenceData) -> core.types.protos.extensions_pb2.PreferenceData:
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
def set_preference(self, data: core.types.protos.extensions_pb2.PreferenceData) -> bool:
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
def set_secure(self, data: core.types.protos.extensions_pb2.PreferenceData) -> bool:
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
def add_songs( self, songs: List[core.types.protos.songs_pb2.Song]) -> List[core.types.protos.songs_pb2.Song]:
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)
def remove_song(self, song: core.types.protos.songs_pb2.Song) -> bool:
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
def update_song( self, song: core.types.protos.songs_pb2.Song) -> core.types.protos.songs_pb2.Song:
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
def add_playlist(self, playlist: core.types.protos.songs_pb2.Playlist) -> str:
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
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_oauth(self, url: str) -> bool:
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
def open_external_url(self, url: str) -> bool:
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
def update_accounts(self, account: Optional[str] = None) -> bool:
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
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
def unregister_user_preferences(self, keys: List[str]) -> bool:
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
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:
379    def get_provider_scopes(
380        self, _: extensions_pb2.GetProviderScopesRequest
381    ) -> extensions_pb2.GetProviderScopesResponse:
382        return extensions_pb2.GetProviderScopesResponse()
def get_playlists( self, req: core.types.protos.extensions_pb2.RequestedPlaylistsRequest) -> core.types.protos.extensions_pb2.RequestedPlaylistsResponse:
384    def get_playlists(
385        self, req: extensions_pb2.RequestedPlaylistsRequest
386    ) -> extensions_pb2.RequestedPlaylistsResponse:
387        raise NotImplementedError()
def get_playlist_content( self, req: core.types.protos.extensions_pb2.RequestedPlaylistSongsRequest) -> core.types.protos.extensions_pb2.RequestedPlaylistSongsResponse:
389    def get_playlist_content(
390        self, req: extensions_pb2.RequestedPlaylistSongsRequest
391    ) -> extensions_pb2.RequestedPlaylistSongsResponse:
392        raise NotImplementedError()
def get_playlist_from_url( self, req: core.types.protos.extensions_pb2.RequestedPlaylistFromUrlRequest) -> core.types.protos.extensions_pb2.RequestedPlaylistFromUrlResponse:
394    def get_playlist_from_url(
395        self, req: extensions_pb2.RequestedPlaylistFromUrlRequest
396    ) -> extensions_pb2.RequestedPlaylistFromUrlResponse:
397        raise NotImplementedError()
def get_playback_details( self, req: core.types.protos.extensions_pb2.PlaybackDetailsRequestedRequest) -> core.types.protos.extensions_pb2.PlaybackDetailsRequestedResponse:
399    def get_playback_details(
400        self, req: extensions_pb2.PlaybackDetailsRequestedRequest
401    ) -> extensions_pb2.PlaybackDetailsRequestedResponse:
402        raise NotImplementedError()
def get_recommendations( self, req: core.types.protos.extensions_pb2.RequestedRecommendationsRequest) -> core.types.protos.extensions_pb2.RequestedRecommendationsResponse:
409    def get_recommendations(
410        self, req: extensions_pb2.RequestedRecommendationsRequest
411    ) -> extensions_pb2.RequestedRecommendationsResponse:
412        raise NotImplementedError()
def get_song_from_url( self, req: core.types.protos.extensions_pb2.RequestedSongFromUrlRequest) -> core.types.protos.extensions_pb2.RequestedSongFromUrlResponse:
414    def get_song_from_url(
415        self, req: extensions_pb2.RequestedSongFromUrlRequest
416    ) -> extensions_pb2.RequestedSongFromUrlResponse:
417        raise NotImplementedError()
def handle_custom_request( self, req: core.types.protos.extensions_pb2.CustomRequest) -> core.types.protos.extensions_pb2.CustomRequestResponse:
419    def handle_custom_request(
420        self, req: extensions_pb2.CustomRequest
421    ) -> extensions_pb2.CustomRequestResponse:
422        raise NotImplementedError()
def get_artist_songs( self, req: core.types.protos.extensions_pb2.RequestedArtistSongsRequest) -> core.types.protos.extensions_pb2.RequestedArtistSongsResponse:
424    def get_artist_songs(
425        self, req: extensions_pb2.RequestedArtistSongsRequest
426    ) -> extensions_pb2.RequestedArtistSongsResponse:
427        raise NotImplementedError()
def get_album_songs( self, req: core.types.protos.extensions_pb2.RequestedAlbumSongsRequest) -> core.types.protos.extensions_pb2.RequestedAlbumSongsResponse:
429    def get_album_songs(
430        self, req: extensions_pb2.RequestedAlbumSongsRequest
431    ) -> extensions_pb2.RequestedAlbumSongsResponse:
432        raise NotImplementedError()
def get_song_from_id( self, req: core.types.protos.extensions_pb2.RequestedSongFromIdRequest) -> core.types.protos.extensions_pb2.RequestedSongFromIdResponse:
434    def get_song_from_id(
435        self, req: extensions_pb2.RequestedSongFromIdRequest
436    ) -> extensions_pb2.RequestedSongFromIdResponse:
437        raise NotImplementedError()
def on_queue_changed( self, req: core.types.protos.extensions_pb2.SongQueueChangedRequest) -> core.types.protos.extensions_pb2.SongQueueChangedResponse:
439    def on_queue_changed(
440        self, req: extensions_pb2.SongQueueChangedRequest
441    ) -> extensions_pb2.SongQueueChangedResponse:
442        raise NotImplementedError()
def on_volume_changed( self, req: core.types.protos.extensions_pb2.VolumeChangedRequest) -> core.types.protos.extensions_pb2.VolumeChangedResponse:
444    def on_volume_changed(
445        self, req: extensions_pb2.VolumeChangedRequest
446    ) -> extensions_pb2.VolumeChangedResponse:
447        raise NotImplementedError()
def on_player_state_changed( self, req: core.types.protos.extensions_pb2.PlayerStateChangedRequest) -> core.types.protos.extensions_pb2.PlayerStateChangedResponse:
449    def on_player_state_changed(
450        self, req: extensions_pb2.PlayerStateChangedRequest
451    ) -> extensions_pb2.PlayerStateChangedResponse:
452        raise NotImplementedError()
def on_song_changed( self, req: core.types.protos.extensions_pb2.SongChangedRequest) -> core.types.protos.extensions_pb2.SongChangedResponse:
454    def on_song_changed(
455        self, req: extensions_pb2.SongChangedRequest
456    ) -> extensions_pb2.SongChangedResponse:
457        raise NotImplementedError()
def on_seeked( self, req: core.types.protos.extensions_pb2.SeekedRequest) -> core.types.protos.extensions_pb2.SeekedResponse:
459    def on_seeked(
460        self, req: extensions_pb2.SeekedRequest
461    ) -> extensions_pb2.SeekedResponse:
462        raise NotImplementedError()
def on_preferences_changed( self, req: core.types.protos.extensions_pb2.PreferenceChangedRequest) -> core.types.protos.extensions_pb2.PreferenceChangedResponse:
464    def on_preferences_changed(
465        self, req: extensions_pb2.PreferenceChangedRequest
466    ) -> extensions_pb2.PreferenceChangedResponse:
467        raise NotImplementedError()
def on_song_added( self, req: core.types.protos.extensions_pb2.SongAddedRequest) -> core.types.protos.extensions_pb2.SongAddedResponse:
469    def on_song_added(
470        self, req: extensions_pb2.SongAddedRequest
471    ) -> extensions_pb2.SongAddedResponse:
472        raise NotImplementedError()
def on_song_removed( self, req: core.types.protos.extensions_pb2.SongRemovedRequest) -> core.types.protos.extensions_pb2.SongRemovedResponse:
474    def on_song_removed(
475        self, req: extensions_pb2.SongRemovedRequest
476    ) -> extensions_pb2.SongRemovedResponse:
477        raise NotImplementedError()
def on_playlist_added( self, req: core.types.protos.extensions_pb2.PlaylistAddedRequest) -> core.types.protos.extensions_pb2.PlaylistAddedResponse:
479    def on_playlist_added(
480        self, req: extensions_pb2.PlaylistAddedRequest
481    ) -> extensions_pb2.PlaylistAddedResponse:
482        raise NotImplementedError()
def on_playlist_removed( self, req: core.types.protos.extensions_pb2.PlaylistRemovedRequest) -> core.types.protos.extensions_pb2.PlaylistRemovedResponse:
484    def on_playlist_removed(
485        self, req: extensions_pb2.PlaylistRemovedRequest
486    ) -> extensions_pb2.PlaylistRemovedResponse:
487        raise NotImplementedError()
def get_accounts( self, req: core.types.protos.extensions_pb2.GetAccountsRequest) -> core.types.protos.extensions_pb2.GetAccountsResponse:
489    def get_accounts(
490        self, req: extensions_pb2.GetAccountsRequest
491    ) -> extensions_pb2.GetAccountsResponse:
492        return extensions_pb2.GetAccountsResponse()
def perform_account_login( self, req: core.types.protos.extensions_pb2.PerformAccountLoginRequest) -> core.types.protos.extensions_pb2.PerformAccountLoginResponse:
494    def perform_account_login(
495        self, req: extensions_pb2.PerformAccountLoginRequest
496    ) -> extensions_pb2.PerformAccountLoginResponse:
497        raise NotImplementedError()
def scrobble( self, req: core.types.protos.extensions_pb2.ScrobbleRequest) -> core.types.protos.extensions_pb2.ScrobbleResponse:
499    def scrobble(
500        self, req: extensions_pb2.ScrobbleRequest
501    ) -> extensions_pb2.ScrobbleResponse:
502        raise NotImplementedError()
def oauth_callback( self, req: core.types.protos.extensions_pb2.OauthCallbackRequest) -> core.types.protos.extensions_pb2.OauthCallbackResponse:
504    def oauth_callback(
505        self, req: extensions_pb2.OauthCallbackRequest
506    ) -> extensions_pb2.OauthCallbackResponse:
507        raise NotImplementedError()
def get_song_context_menu( self, req: core.types.protos.extensions_pb2.RequestedSongContextMenuRequest) -> core.types.protos.extensions_pb2.RequestedSongContextMenuResponse:
509    def get_song_context_menu(
510        self, req: extensions_pb2.RequestedSongContextMenuRequest
511    ) -> extensions_pb2.RequestedSongContextMenuResponse:
512        raise NotImplementedError()
def get_playlist_context_menu( self, req: core.types.protos.extensions_pb2.RequestedPlaylistContextMenuRequest) -> core.types.protos.extensions_pb2.RequestedPlaylistContextMenuResponse:
514    def get_playlist_context_menu(
515        self, req: extensions_pb2.RequestedPlaylistContextMenuRequest
516    ) -> extensions_pb2.RequestedPlaylistContextMenuResponse:
517        raise NotImplementedError()
def on_context_menu_action( self, req: core.types.protos.extensions_pb2.ContextMenuActionRequest) -> core.types.protos.extensions_pb2.ContextMenuActionResponse:
519    def on_context_menu_action(
520        self, req: extensions_pb2.ContextMenuActionRequest
521    ) -> extensions_pb2.ContextMenuActionResponse:
522        raise NotImplementedError()
def get_lyrics( self, req: core.types.protos.extensions_pb2.RequestedLyricsRequest) -> core.types.protos.extensions_pb2.RequestedLyricsResponse:
524    def get_lyrics(
525        self, req: extensions_pb2.RequestedLyricsRequest
526    ) -> extensions_pb2.RequestedLyricsResponse:
527        raise NotImplementedError()
extension_instance: Optional[Extension] = None
def register_extension(extension: Extension):
533def register_extension(extension: Extension):
534    global extension_instance
535    extension_instance = extension
def ensure_extension_instance() -> Extension:
538def ensure_extension_instance() -> Extension:
539    if extension_instance is None:
540        raise Exception("Extension instance is not initialized")
541    return extension_instance
@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.