moosync_edk

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

Convert datetime.timedelta to google.protobuf.Duration.