import 'dart:async'; import 'dart:convert'; import 'package:flutter_js/extensions/fetch.dart'; import 'package:flutter_js/extensions/xhr.dart'; import 'package:flutter_js/flutter_js.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/services/logger/logger.dart'; const defaultMetadataLimit = "20"; /// Signature for metadata and related methods that will return Spotube native /// objects e.g. SpotubeTrack, SpotubePlaylist, etc. class MetadataApiSignature { final JavascriptRuntime runtime; MetadataApiSignature._(this.runtime); static Future init(String libraryCode) async { final runtime = getJavascriptRuntime(xhr: true).enableXhr(); runtime.enableHandlePromises(); await runtime.enableFetch(); Timer.periodic( const Duration(milliseconds: 100), (timer) { runtime.executePendingJob(); }, ); final res = runtime.evaluate( """ ;$libraryCode; const metadataApi = new MetadataApi(); """, ); if (res.isError) { AppLogger.reportError( "Error evaluating code: $libraryCode\n${res.rawResult}", ); } return MetadataApiSignature._(runtime); } void dispose() { runtime.dispose(); } Future invoke(String method, [List? args]) async { final completer = Completer(); runtime.onMessage(method, (result) { try { if (result is Map && result.containsKey("error")) { completer.completeError(result["error"]); } else { completer.complete(result is String ? jsonDecode(result) : result); } } catch (e, stack) { AppLogger.reportError(e, stack); } }); final code = """ $method(...${args != null ? jsonEncode(args) : "[]"}) .then((res) => { try { sendMessage("$method", JSON.stringify(res)); } catch (e) { console.error("Failed to send message in $method.then: ", `\${e.toString()}\n\${e.stack.toString()}`); } }).catch((e) => { try { console.error("Error in $method: ", `\${e.toString()}\n\${e.stack.toString()}`); sendMessage("$method", JSON.stringify({error: `\${e.toString()}\n\${e.stack.toString()}`})); } catch (e) { console.error("Failed to send message in $method.catch: ", `\${e.toString()}\n\${e.stack.toString()}`); } }); """; final res = await runtime.evaluateAsync(code); if (res.isError) { AppLogger.reportError("Error evaluating code: $code\n${res.rawResult}"); completer.completeError("Error evaluating code: $code\n${res.rawResult}"); return completer.future; } return completer.future; } // ----- Track ------ Future getTrack(String id) async { final result = await invoke("metadataApi.getTrack", [id]); return SpotubeTrackObject.fromJson(result); } Future> listTracks({ List? ids, String limit = defaultMetadataLimit, String? cursor, }) async { final result = await invoke( "metadataApi.listTracks", [ ids, limit, cursor, ], ); return SpotubePaginationResponseObject.fromJson( result, SpotubeTrackObject.fromJson, ); } Future> listTracksByAlbum( String albumId, { String limit = defaultMetadataLimit, String? cursor, }) async { final res = await invoke( "metadataApi.listTracksByAlbum", [albumId, limit, cursor], ); return SpotubePaginationResponseObject.fromJson( res, SpotubeTrackObject.fromJson, ); } Future> listTopTracksByArtist( String artistId, { String limit = defaultMetadataLimit, String? cursor, }) async { final res = await invoke( "metadataApi.listTopTracksByArtist", [artistId, limit, cursor], ); return SpotubePaginationResponseObject.fromJson( res, SpotubeTrackObject.fromJson, ); } Future> listTracksByPlaylist( String playlistId, { String limit = defaultMetadataLimit, String? cursor, }) async { final res = await invoke( "metadataApi.listTracksByPlaylist", [playlistId, limit, cursor], ); return SpotubePaginationResponseObject.fromJson( res, SpotubeTrackObject.fromJson, ); } Future> listUserSavedTracks( String userId, { String limit = defaultMetadataLimit, String? cursor, }) async { final res = await invoke( "metadataApi.listUserSavedTracks", [userId, limit, cursor], ); return SpotubePaginationResponseObject.fromJson( res, SpotubeTrackObject.fromJson, ); } // ----- Album ------ Future getAlbum(String id) async { final res = await invoke("metadataApi.getAlbum", [id]); return SpotubeAlbumObject.fromJson(res); } Future> listAlbums({ List? ids, String limit = defaultMetadataLimit, String? cursor, }) async { final res = await invoke( "metadataApi.listAlbums", [ids, limit, cursor], ); return SpotubePaginationResponseObject.fromJson( res, SpotubeAlbumObject.fromJson, ); } Future> listAlbumsByArtist( String artistId, { String limit = defaultMetadataLimit, String? cursor, }) async { final res = await invoke( "metadataApi.listAlbumsByArtist", [artistId, limit, cursor], ); return SpotubePaginationResponseObject.fromJson( res, SpotubeAlbumObject.fromJson, ); } Future> listUserSavedAlbums( String userId, { String limit = defaultMetadataLimit, String? cursor, }) async { final res = await invoke( "metadataApi.listUserSavedAlbums", [userId, limit, cursor], ); return SpotubePaginationResponseObject.fromJson( res, SpotubeAlbumObject.fromJson, ); } // ----- Playlist ------ Future getPlaylist(String id) async { final res = await invoke("metadataApi.getPlaylist", [id]); return SpotubePlaylistObject.fromJson(res); } Future> listFeedPlaylists( String feedId, { String limit = defaultMetadataLimit, String? cursor, }) async { final res = await invoke( "metadataApi.listFeedPlaylists", [feedId, limit, cursor], ); return SpotubePaginationResponseObject.fromJson( res, SpotubePlaylistObject.fromJson, ); } Future> listUserSavedPlaylists( String userId, { String limit = defaultMetadataLimit, String? cursor, }) async { final res = await invoke( "metadataApi.listUserSavedPlaylists", [userId, limit, cursor], ); return SpotubePaginationResponseObject.fromJson( res, SpotubePlaylistObject.fromJson, ); } Future createPlaylist( String userId, String name, { String? description, bool? public, bool? collaborative, String? imageBase64, }) async { final res = await invoke( "metadataApi.createPlaylist", [ userId, name, description, public, collaborative, imageBase64, ], ); return SpotubePlaylistObject.fromJson(res); } Future updatePlaylist( String playlistId, { String? name, String? description, bool? public, bool? collaborative, String? imageBase64, }) async { await invoke( "metadataApi.updatePlaylist", [ playlistId, name, description, public, collaborative, imageBase64, ], ); } Future deletePlaylist(String userId, String playlistId) async { await unsavePlaylist(userId, playlistId); } Future addTracksToPlaylist( String playlistId, List trackIds, { int? position, }) async { await invoke( "metadataApi.addTracksToPlaylist", [ playlistId, trackIds, position, ], ); } Future removeTracksFromPlaylist( String playlistId, List trackIds, ) async { await invoke( "metadataApi.removeTracksFromPlaylist", [ playlistId, trackIds, ], ); } // ----- Artist ------ Future getArtist(String id) async { final res = await invoke("metadataApi.getArtist", [id]); return SpotubeArtistObject.fromJson(res); } Future> listArtists({ List? ids, String limit = defaultMetadataLimit, String? cursor, }) async { final res = await invoke( "metadataApi.listArtists", [ids, limit, cursor], ); return SpotubePaginationResponseObject.fromJson( res, SpotubeArtistObject.fromJson, ); } Future> listUserSavedArtists( String userId, { String limit = defaultMetadataLimit, String? cursor, }) async { final res = await invoke( "metadataApi.listUserSavedArtists", [userId, limit, cursor], ); return SpotubePaginationResponseObject.fromJson( res, SpotubeArtistObject.fromJson, ); } // ----- Search ------ Future search( String query, { String limit = defaultMetadataLimit, String? cursor, }) async { final res = await invoke( "metadataApi.search", [query, limit, cursor], ); return SpotubeSearchResponseObject.fromJson(res); } // ----- Feed ------ Future getFeed(String id) async { final res = await invoke("metadataApi.getFeed", [id]); return SpotubeFeedObject.fromJson(res); } Future> listFeeds({ String limit = defaultMetadataLimit, String? cursor, }) async { final res = await invoke("metadataApi.listFeeds", [limit, cursor]); return SpotubePaginationResponseObject.fromJson( res, SpotubeFeedObject.fromJson, ); } // ----- User ------ Future followArtist(String userId, String artistId) async { await invoke("metadataApi.followArtist", [userId, artistId]); } Future unfollowArtist(String userId, String artistId) async { await invoke("metadataApi.unfollowArtist", [userId, artistId]); } Future savePlaylist(String userId, String playlistId) async { await invoke("metadataApi.savePlaylist", [userId, playlistId]); } Future unsavePlaylist(String userId, String playlistId) async { await invoke("metadataApi.unsavePlaylist", [userId, playlistId]); } Future saveAlbum(String userId, String albumId) async { await invoke("metadataApi.saveAlbum", [userId, albumId]); } Future unsaveAlbum(String userId, String albumId) async { await invoke("metadataApi.unsaveAlbum", [userId, albumId]); } Future saveTrack(String userId, String trackId) async { await invoke("metadataApi.saveTrack", [userId, trackId]); } Future unsaveTrack(String userId, String trackId) async { await invoke("metadataApi.unsaveTrack", [userId, trackId]); } }