From 0a16be4395ff9fed69f2b4055cb24612f42b722d Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 13 Jan 2026 05:53:30 +0700 Subject: [PATCH] feat(extension): add HMAC-SHA1 utility, artist URL handler, and store refresh fix - Add utils.hmacSHA1(key, message) for cryptographic operations in extensions - Add artist type handling in track_provider for extension URL results - Fix extension store not refreshing after uninstall - Update CHANGELOG with new features and Spotify Web extension docs --- CHANGELOG.md | 21 ++++++++++ go_backend/extension_runtime.go | 1 + go_backend/extension_runtime_utils.go | 59 +++++++++++++++++++++++++++ lib/providers/track_provider.dart | 14 +++++++ 4 files changed, 95 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c6bd231..4ec67db9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,12 +19,33 @@ - SpotiFLAC automatically routes matching URLs to the appropriate extension - Supports share intents and paste from clipboard +- **Artist URL Handler Support**: Extensions can now return artist data from URL handlers + - Added `type: "artist"` handling in track_provider.dart + - Navigate to artist screen with albums list from extension + +- **HMAC-SHA1 Utility**: New `utils.hmacSHA1(key, message)` function for extensions + - Enables TOTP generation and other cryptographic operations + - Returns byte array for flexible use + +### Fixed + +- **Extension Store Refresh**: Store tab now properly refreshes after uninstalling an extension + - "Installed" badge correctly updates to "Install" button + ### Documentation - Updated `docs/EXTENSION_DEVELOPMENT.md`: - Added Custom URL Handler section with examples - Added `handleURL` function documentation - Added URL pattern examples for YouTube, SoundCloud, Bandcamp + - Added `utils.hmacSHA1` documentation with TOTP example + +### Extensions + +- **Spotify Web Extension** (example): New extension for Spotify metadata via web API + - Supports personalized playlists (Daily Mix, Discover Weekly, Release Radar, etc.) + - Search, album, playlist, track, and artist fetching + - Located in `docs/extensions_example/spotify-internal/` --- diff --git a/go_backend/extension_runtime.go b/go_backend/extension_runtime.go index 21622721..77c8c311 100644 --- a/go_backend/extension_runtime.go +++ b/go_backend/extension_runtime.go @@ -226,6 +226,7 @@ func (r *ExtensionRuntime) RegisterAPIs(vm *goja.Runtime) { utilsObj.Set("sha256", r.sha256Hash) utilsObj.Set("hmacSHA256", r.hmacSHA256) utilsObj.Set("hmacSHA256Base64", r.hmacSHA256Base64) + utilsObj.Set("hmacSHA1", r.hmacSHA1) utilsObj.Set("parseJSON", r.parseJSON) utilsObj.Set("stringifyJSON", r.stringifyJSON) // Crypto utilities for developers diff --git a/go_backend/extension_runtime_utils.go b/go_backend/extension_runtime_utils.go index b01693d2..cd3819c1 100644 --- a/go_backend/extension_runtime_utils.go +++ b/go_backend/extension_runtime_utils.go @@ -5,6 +5,7 @@ import ( "crypto/hmac" "crypto/md5" "crypto/rand" + "crypto/sha1" "crypto/sha256" "encoding/base64" "encoding/hex" @@ -85,6 +86,64 @@ func (r *ExtensionRuntime) hmacSHA256Base64(call goja.FunctionCall) goja.Value { return r.vm.ToValue(base64.StdEncoding.EncodeToString(mac.Sum(nil))) } +// hmacSHA1 computes HMAC-SHA1 of a message with a key (for TOTP) +// Arguments: message (string or array of bytes), key (string or array of bytes) +// Returns: array of bytes (for TOTP dynamic truncation) +func (r *ExtensionRuntime) hmacSHA1(call goja.FunctionCall) goja.Value { + if len(call.Arguments) < 2 { + return r.vm.ToValue([]byte{}) + } + + // Get key - can be string or array of bytes + var keyBytes []byte + keyArg := call.Arguments[0].Export() + switch k := keyArg.(type) { + case string: + keyBytes = []byte(k) + case []interface{}: + keyBytes = make([]byte, len(k)) + for i, v := range k { + if num, ok := v.(int64); ok { + keyBytes[i] = byte(num) + } else if num, ok := v.(float64); ok { + keyBytes[i] = byte(int(num)) + } + } + default: + return r.vm.ToValue([]byte{}) + } + + // Get message - can be string or array of bytes + var msgBytes []byte + msgArg := call.Arguments[1].Export() + switch m := msgArg.(type) { + case string: + msgBytes = []byte(m) + case []interface{}: + msgBytes = make([]byte, len(m)) + for i, v := range m { + if num, ok := v.(int64); ok { + msgBytes[i] = byte(num) + } else if num, ok := v.(float64); ok { + msgBytes[i] = byte(int(num)) + } + } + default: + return r.vm.ToValue([]byte{}) + } + + mac := hmac.New(sha1.New, keyBytes) + mac.Write(msgBytes) + result := mac.Sum(nil) + + // Convert to array of numbers for JavaScript + jsArray := make([]interface{}, len(result)) + for i, b := range result { + jsArray[i] = int(b) + } + return r.vm.ToValue(jsArray) +} + // parseJSON parses a JSON string func (r *ExtensionRuntime) parseJSON(call goja.FunctionCall) goja.Value { if len(call.Arguments) < 1 { diff --git a/lib/providers/track_provider.dart b/lib/providers/track_provider.dart index dbf190ea..032836f1 100644 --- a/lib/providers/track_provider.dart +++ b/lib/providers/track_provider.dart @@ -165,6 +165,20 @@ class TrackNotifier extends Notifier { searchExtensionId: extensionId, ); return; + } else if (type == 'artist' && result['artist'] != null) { + final artistData = result['artist'] as Map; + final albumsList = artistData['albums'] as List? ?? []; + final albums = albumsList.map((a) => _parseArtistAlbum(a as Map)).toList(); + state = TrackState( + tracks: [], + isLoading: false, + artistId: artistData['id'] as String?, + artistName: artistData['name'] as String?, + coverUrl: artistData['image_url'] as String? ?? artistData['images'] as String?, + artistAlbums: albums, + searchExtensionId: extensionId, + ); + return; } } }