moosync_edk

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

Convert datetime.timedelta to google.protobuf.Duration.