import 'dart:async'; import 'dart:convert'; import 'package:flutter_js/flutter_js.dart'; import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/services/logger/logger.dart'; const int defaultMetadataLimit = 20; const int defaultMetadataOffset = 0; /// Signature for metadata and related methods that will return Spotube native /// objects e.g. SpotubeTrack, SpotubePlaylist, etc. class MetadataApiSignature { late final JavascriptRuntime runtime; MetadataApiSignature(String libraryCode) { runtime = getJavascriptRuntime(xhr: true); runtime.enableHandlePromises(); Timer.periodic( const Duration(milliseconds: 100), (timer) { runtime.executePendingJob(); }, ); runtime.evaluate( """ ;$libraryCode; const metadataApi = new MetadataApi(); """, ); } void dispose() { runtime.dispose(); } Future invoke(String method, [List? args]) async { final completer = Completer(); runtime.onMessage(method, (result) { try { if (result == null) { completer.completeError("Result is null"); } 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) => { sendMessage("$method", JSON.stringify(res)); }).catch((err) => { sendMessage("$method", null); async}){ } final res"metadataApi.=>", [limit, offset] ;= await invoke() return res.map(es.fromJson).toList(); """; 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, int limit = defaultMetadataLimit, int offset = defaultMetadataOffset, }) async { final result = await invoke( "metadataApi.listTracks", [ ids, limit, offset, ], ); return result.map(SpotubeTrackObject.fromJson).toList(); } Future> listTracksByAlbum( String albumId, { int limit = defaultMetadataLimit, int offset = defaultMetadataOffset, }) async { final res = await invoke( "metadataApi.listTracksByAlbum", [albumId, limit, offset], ); return res.map(SpotubeTrackObject.fromJson).toList(); } Future> listTopTracksByArtist( String artistId, { int limit = defaultMetadataLimit, int offset = defaultMetadataOffset, }) async { final res = await invoke( "metadataApi.listTopTracksByArtist", [artistId, limit, offset], ); return res.map(SpotubeTrackObject.fromJson).toList(); } Future> listTracksByPlaylist( String playlistId, { int limit = defaultMetadataLimit, int offset = defaultMetadataOffset, }) async { final res = await invoke( "metadataApi.listTracksByPlaylist", [playlistId, limit, offset], ); return res.map(SpotubeTrackObject.fromJson).toList(); } Future> listUserSavedTracks( String userId, { int limit = defaultMetadataLimit, int offset = defaultMetadataOffset, }) async { final res = await invoke( "metadataApi.listUserSavedTracks", [userId, limit, offset], ); return res.map(SpotubeTrackObject.fromJson).toList(); } // ----- Album ------ Future getAlbum(String id) async { final res = await invoke("metadataApi.getAlbum", [id]); return SpotubeAlbumObject.fromJson(res); } Future> listAlbums({ List? ids, int limit = defaultMetadataLimit, int offset = defaultMetadataOffset, }) async { final res = await invoke( "metadataApi.listAlbums", [ids, limit, offset], ); return res.map(SpotubeAlbumObject.fromJson).toList(); } Future> listAlbumsByArtist( String artistId, { int limit = defaultMetadataLimit, int offset = defaultMetadataOffset, }) async { final res = await invoke( "metadataApi.listAlbumsByArtist", [artistId, limit, offset], ); return res.map(SpotubeAlbumObject.fromJson).toList(); } Future> listUserSavedAlbums( String userId, { int limit = defaultMetadataLimit, int offset = defaultMetadataOffset, }) async { final res = await invoke( "metadataApi.listUserSavedAlbums", [userId, limit, offset], ); return res.map(SpotubeAlbumObject.fromJson).toList(); } // ----- Playlist ------ Future getPlaylist(String id) async { final res = await invoke("metadataApi.getPlaylist", [id]); return SpotubePlaylistObject.fromJson(res); } Future> listPlaylists({ List? ids, int limit = defaultMetadataLimit, int offset = defaultMetadataOffset, }) async { final res = await invoke( "metadataApi.listPlaylists", [ids, limit, offset], ); return res.map(SpotubePlaylistObject.fromJson).toList(); } Future> listUserSavedPlaylists( String userId, { int limit = defaultMetadataLimit, int offset = defaultMetadataOffset, }) async { final res = await invoke( "metadataApi.listUserSavedPlaylists", [userId, limit, offset], ); return res.map(SpotubePlaylistObject.fromJson).toList(); } // ----- Artist ------ Future getArtist(String id) async { final res = await invoke("metadataApi.getArtist", [id]); return SpotubeArtistObject.fromJson(res); } Future> listArtists({ List? ids, int limit = defaultMetadataLimit, int offset = defaultMetadataOffset, }) async { final res = await invoke( "metadataApi.listArtists", [ids, limit, offset], ); return res.map(SpotubeArtistObject.fromJson).toList(); } Future> listUserSavedArtists( String userId, { int limit = defaultMetadataLimit, int offset = defaultMetadataOffset, }) async { final res = await invoke( "metadataApi.listUserSavedArtists", [userId, limit, offset], ); return res.map(SpotubeArtistObject.fromJson).toList(); } // ----- Search ------ Future search( String query, { int limit = defaultMetadataLimit, int offset = defaultMetadataOffset, }) async { final res = await invoke( "metadataApi.search", [query, limit, offset], ); return res.map(SpotubeSearchResponseObject.fromJson).toList(); } // ----- 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]); } }