.refine artist fetch all tracks

This commit is contained in:
afkarxyz
2026-03-25 13:43:14 +07:00
parent dd67b54ea9
commit d8722c58dc
7 changed files with 279 additions and 61 deletions
+34 -3
View File
@@ -142,12 +142,27 @@ func (a *App) GetSpotifyMetadata(req SpotifyMetadataRequest) (string, error) {
defer cancel() defer cancel()
settings, err := a.LoadSettings() settings, err := a.LoadSettings()
separator := req.Separator
if separator == "" {
separator = ", "
if err == nil && settings != nil {
if sep, ok := settings["separator"].(string); ok {
if sep == "semicolon" {
separator = "; "
} else if sep == "comma" {
separator = ", "
}
}
}
}
if err == nil && settings != nil { if err == nil && settings != nil {
if useAPI, ok := settings["useSpotFetchAPI"].(bool); ok && useAPI { if useAPI, ok := settings["useSpotFetchAPI"].(bool); ok && useAPI {
if apiURL, ok := settings["spotFetchAPIUrl"].(string); ok && apiURL != "" { if apiURL, ok := settings["spotFetchAPIUrl"].(string); ok && apiURL != "" {
data, err := backend.GetSpotifyDataWithAPI(ctx, req.URL, true, apiURL, req.Batch, time.Duration(req.Delay*float64(time.Second))) data, err := backend.GetSpotifyDataWithAPI(ctx, req.URL, true, apiURL, req.Batch, time.Duration(req.Delay*float64(time.Second)), separator, func(tracks interface{}) {
runtime.EventsEmit(a.ctx, "metadata-stream", tracks)
})
if err != nil { if err != nil {
return "", fmt.Errorf("failed to fetch metadata from API: %v", err) return "", fmt.Errorf("failed to fetch metadata from API: %v", err)
} }
@@ -162,7 +177,9 @@ func (a *App) GetSpotifyMetadata(req SpotifyMetadataRequest) (string, error) {
} }
} }
data, err := backend.GetFilteredSpotifyData(ctx, req.URL, req.Batch, time.Duration(req.Delay*float64(time.Second))) data, err := backend.GetFilteredSpotifyData(ctx, req.URL, req.Batch, time.Duration(req.Delay*float64(time.Second)), separator, func(tracks interface{}) {
runtime.EventsEmit(a.ctx, "metadata-stream", tracks)
})
if err != nil { if err != nil {
return "", fmt.Errorf("failed to fetch metadata: %v", err) return "", fmt.Errorf("failed to fetch metadata: %v", err)
} }
@@ -283,7 +300,21 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
defer cancel() defer cancel()
trackURL := fmt.Sprintf("https://open.spotify.com/track/%s", req.SpotifyID) trackURL := fmt.Sprintf("https://open.spotify.com/track/%s", req.SpotifyID)
trackData, err := backend.GetFilteredSpotifyData(ctx, trackURL, false, 0) metadataSeparator := req.Separator
if metadataSeparator == "" {
metadataSeparator = ", "
metadataSettings, _ := a.LoadSettings()
if metadataSettings != nil {
if sep, ok := metadataSettings["separator"].(string); ok {
if sep == "semicolon" {
metadataSeparator = "; "
} else if sep == "comma" {
metadataSeparator = ", "
}
}
}
}
trackData, err := backend.GetFilteredSpotifyData(ctx, trackURL, false, 0, metadataSeparator, nil)
if err == nil { if err == nil {
var trackResp struct { var trackResp struct {
+14 -14
View File
@@ -485,7 +485,7 @@ func extractDuration(ms float64) map[string]interface{} {
} }
} }
func FilterTrack(data map[string]interface{}, albumFetchData ...map[string]interface{}) map[string]interface{} { func FilterTrack(data map[string]interface{}, separator string, albumFetchData ...map[string]interface{}) map[string]interface{} {
dataMap := getMap(data, "data") dataMap := getMap(data, "data")
trackData := getMap(dataMap, "trackUnion") trackData := getMap(dataMap, "trackUnion")
if len(trackData) == 0 { if len(trackData) == 0 {
@@ -665,7 +665,7 @@ func FilterTrack(data map[string]interface{}, albumFetchData ...map[string]inter
for _, artist := range albumArtists { for _, artist := range albumArtists {
albumArtistNames = append(albumArtistNames, getString(artist, "name")) albumArtistNames = append(albumArtistNames, getString(artist, "name"))
} }
albumArtistsString = strings.Join(albumArtistNames, GetSeparator()) albumArtistsString = strings.Join(albumArtistNames, separator)
} }
if albumArtistsString == "" { if albumArtistsString == "" {
albumArtistsString = getString(albumUnionData, "artists") albumArtistsString = getString(albumUnionData, "artists")
@@ -681,7 +681,7 @@ func FilterTrack(data map[string]interface{}, albumFetchData ...map[string]inter
for _, artist := range albumArtists { for _, artist := range albumArtists {
albumArtistNames = append(albumArtistNames, getString(artist, "name")) albumArtistNames = append(albumArtistNames, getString(artist, "name"))
} }
albumArtistsString = strings.Join(albumArtistNames, GetSeparator()) albumArtistsString = strings.Join(albumArtistNames, separator)
} }
} }
@@ -715,7 +715,7 @@ func FilterTrack(data map[string]interface{}, albumFetchData ...map[string]inter
for _, artist := range artists { for _, artist := range artists {
artistNames = append(artistNames, getString(artist, "name")) artistNames = append(artistNames, getString(artist, "name"))
} }
artistsString := strings.Join(artistNames, GetSeparator()) artistsString := strings.Join(artistNames, separator)
copyrightTexts := []string{} copyrightTexts := []string{}
for _, item := range copyrightInfo { for _, item := range copyrightInfo {
@@ -802,7 +802,7 @@ func FilterTrack(data map[string]interface{}, albumFetchData ...map[string]inter
return filtered return filtered
} }
func FilterAlbum(data map[string]interface{}) map[string]interface{} { func FilterAlbum(data map[string]interface{}, separator string) map[string]interface{} {
dataMap := getMap(data, "data") dataMap := getMap(data, "data")
albumData := getMap(dataMap, "albumUnion") albumData := getMap(dataMap, "albumUnion")
if len(albumData) == 0 { if len(albumData) == 0 {
@@ -814,7 +814,7 @@ func FilterAlbum(data map[string]interface{}) map[string]interface{} {
for _, artist := range artists { for _, artist := range artists {
artistNames = append(artistNames, getString(artist, "name")) artistNames = append(artistNames, getString(artist, "name"))
} }
albumArtistsString := strings.Join(artistNames, GetSeparator()) albumArtistsString := strings.Join(artistNames, separator)
coverObj := extractCoverImage(getMap(albumData, "coverArt")) coverObj := extractCoverImage(getMap(albumData, "coverArt"))
var cover interface{} var cover interface{}
@@ -875,7 +875,7 @@ func FilterAlbum(data map[string]interface{}) map[string]interface{} {
for _, artist := range trackArtists { for _, artist := range trackArtists {
trackArtistNames = append(trackArtistNames, getString(artist, "name")) trackArtistNames = append(trackArtistNames, getString(artist, "name"))
} }
trackArtistsString := strings.Join(trackArtistNames, GetSeparator()) trackArtistsString := strings.Join(trackArtistNames, separator)
trackURI := getString(track, "uri") trackURI := getString(track, "uri")
trackID := "" trackID := ""
@@ -943,7 +943,7 @@ func FilterAlbum(data map[string]interface{}) map[string]interface{} {
return filtered return filtered
} }
func FilterPlaylist(data map[string]interface{}) map[string]interface{} { func FilterPlaylist(data map[string]interface{}, separator string) map[string]interface{} {
dataMap := getMap(data, "data") dataMap := getMap(data, "data")
playlistData := getMap(dataMap, "playlistV2") playlistData := getMap(dataMap, "playlistV2")
if len(playlistData) == 0 { if len(playlistData) == 0 {
@@ -1075,7 +1075,7 @@ func FilterPlaylist(data map[string]interface{}) map[string]interface{} {
for _, artist := range trackArtists { for _, artist := range trackArtists {
trackArtistNames = append(trackArtistNames, getString(artist, "name")) trackArtistNames = append(trackArtistNames, getString(artist, "name"))
} }
artistsString := strings.Join(trackArtistNames, GetSeparator()) artistsString := strings.Join(trackArtistNames, separator)
trackDurationMs := getFloat64(getMap(trackData, "trackDuration"), "totalMilliseconds") trackDurationMs := getFloat64(getMap(trackData, "trackDuration"), "totalMilliseconds")
durationObj := extractDuration(trackDurationMs) durationObj := extractDuration(trackDurationMs)
@@ -1121,7 +1121,7 @@ func FilterPlaylist(data map[string]interface{}) map[string]interface{} {
for _, artist := range albumArtists { for _, artist := range albumArtists {
albumArtistNames = append(albumArtistNames, getString(artist, "name")) albumArtistNames = append(albumArtistNames, getString(artist, "name"))
} }
albumArtistsString = strings.Join(albumArtistNames, GetSeparator()) albumArtistsString = strings.Join(albumArtistNames, separator)
} }
} }
@@ -1295,7 +1295,7 @@ func stripHTMLTags(s string) string {
return re.ReplaceAllString(s, "") return re.ReplaceAllString(s, "")
} }
func FilterArtist(data map[string]interface{}) map[string]interface{} { func FilterArtist(data map[string]interface{}, separator string) map[string]interface{} {
dataMap := getMap(data, "data") dataMap := getMap(data, "data")
artistData := getMap(dataMap, "artistUnion") artistData := getMap(dataMap, "artistUnion")
if len(artistData) == 0 { if len(artistData) == 0 {
@@ -1424,7 +1424,7 @@ func FilterArtist(data map[string]interface{}) map[string]interface{} {
return filtered return filtered
} }
func FilterSearch(data map[string]interface{}) map[string]interface{} { func FilterSearch(data map[string]interface{}, separator string) map[string]interface{} {
dataMap := getMap(data, "data") dataMap := getMap(data, "data")
searchData := getMap(dataMap, "searchV2") searchData := getMap(dataMap, "searchV2")
if len(searchData) == 0 { if len(searchData) == 0 {
@@ -1514,7 +1514,7 @@ func FilterSearch(data map[string]interface{}) map[string]interface{} {
for _, artist := range trackArtists { for _, artist := range trackArtists {
trackArtistNames = append(trackArtistNames, getString(artist, "name")) trackArtistNames = append(trackArtistNames, getString(artist, "name"))
} }
trackArtistsString := strings.Join(trackArtistNames, GetSeparator()) trackArtistsString := strings.Join(trackArtistNames, separator)
durationString := getString(trackDuration, "formatted") durationString := getString(trackDuration, "formatted")
@@ -1586,7 +1586,7 @@ func FilterSearch(data map[string]interface{}) map[string]interface{} {
for _, artist := range albumArtists { for _, artist := range albumArtists {
albumArtistNames = append(albumArtistNames, getString(artist, "name")) albumArtistNames = append(albumArtistNames, getString(artist, "name"))
} }
albumArtistsString := strings.Join(albumArtistNames, GetSeparator()) albumArtistsString := strings.Join(albumArtistNames, separator)
dateInfo := getMap(album, "date") dateInfo := getMap(album, "date")
var year interface{} var year interface{}
+39 -3
View File
@@ -11,10 +11,9 @@ import (
"time" "time"
) )
func GetSpotifyDataWithAPI(ctx context.Context, spotifyURL string, useAPI bool, apiBaseURL string, batch bool, delay time.Duration) (interface{}, error) { func GetSpotifyDataWithAPI(ctx context.Context, spotifyURL string, useAPI bool, apiBaseURL string, batch bool, delay time.Duration, separator string, callback MetadataCallback) (interface{}, error) {
if !useAPI || apiBaseURL == "" { if !useAPI || apiBaseURL == "" {
return GetFilteredSpotifyData(ctx, spotifyURL, batch, delay, separator, callback)
return GetFilteredSpotifyData(ctx, spotifyURL, batch, delay)
} }
spotifyType, id := parseSpotifyURLToTypeAndID(spotifyURL) spotifyType, id := parseSpotifyURLToTypeAndID(spotifyURL)
@@ -79,6 +78,43 @@ func GetSpotifyDataWithAPI(ctx context.Context, spotifyURL string, useAPI bool,
return nil, fmt.Errorf("unsupported Spotify type: %s", spotifyType) return nil, fmt.Errorf("unsupported Spotify type: %s", spotifyType)
} }
if callback != nil {
switch payload := data.(type) {
case *AlbumResponsePayload:
if len(payload.TrackList) > 0 {
callback(payload.TrackList)
}
case PlaylistResponsePayload:
if len(payload.TrackList) > 0 {
callback(payload.TrackList)
}
case *ArtistDiscographyPayload:
if len(payload.TrackList) > 0 {
callback(payload.TrackList)
}
case TrackResponse:
t := payload.Track
callback([]AlbumTrackMetadata{{
SpotifyID: t.SpotifyID,
Artists: t.Artists,
Name: t.Name,
AlbumName: t.AlbumName,
AlbumArtist: t.AlbumArtist,
DurationMS: t.DurationMS,
Images: t.Images,
ReleaseDate: t.ReleaseDate,
TrackNumber: t.TrackNumber,
TotalTracks: t.TotalTracks,
DiscNumber: t.DiscNumber,
TotalDiscs: t.TotalDiscs,
ExternalURL: t.ExternalURL,
Plays: t.Plays,
PreviewURL: t.PreviewURL,
IsExplicit: t.IsExplicit,
}})
}
}
return data, nil return data, nil
} }
+100 -30
View File
@@ -18,13 +18,17 @@ var (
errInvalidSpotifyURL = errors.New("invalid or unsupported Spotify URL") errInvalidSpotifyURL = errors.New("invalid or unsupported Spotify URL")
) )
type MetadataCallback func(data interface{})
type SpotifyMetadataClient struct { type SpotifyMetadataClient struct {
httpClient *http.Client httpClient *http.Client
Separator string
} }
func NewSpotifyMetadataClient() *SpotifyMetadataClient { func NewSpotifyMetadataClient() *SpotifyMetadataClient {
return &SpotifyMetadataClient{ return &SpotifyMetadataClient{
httpClient: &http.Client{Timeout: 30 * time.Second}, httpClient: &http.Client{Timeout: 30 * time.Second},
Separator: ", ",
} }
} }
@@ -342,54 +346,57 @@ type SearchResponse struct {
Playlists []SearchResult `json:"playlists"` Playlists []SearchResult `json:"playlists"`
} }
func GetFilteredSpotifyData(ctx context.Context, spotifyURL string, batch bool, delay time.Duration) (interface{}, error) { func GetFilteredSpotifyData(ctx context.Context, spotifyURL string, batch bool, delay time.Duration, separator string, callback MetadataCallback) (interface{}, error) {
client := NewSpotifyMetadataClient() client := NewSpotifyMetadataClient()
return client.GetFilteredData(ctx, spotifyURL, batch, delay) if separator != "" {
client.Separator = separator
}
return client.GetFilteredData(ctx, spotifyURL, batch, delay, callback)
} }
func (c *SpotifyMetadataClient) GetFilteredData(ctx context.Context, spotifyURL string, batch bool, delay time.Duration) (interface{}, error) { func (c *SpotifyMetadataClient) GetFilteredData(ctx context.Context, spotifyURL string, batch bool, delay time.Duration, callback MetadataCallback) (interface{}, error) {
parsed, err := parseSpotifyURI(spotifyURL) parsed, err := parseSpotifyURI(spotifyURL)
if err != nil { if err != nil {
return nil, err return nil, err
} }
raw, err := c.getRawSpotifyData(ctx, parsed, batch, delay) raw, err := c.getRawSpotifyData(ctx, parsed, batch, delay, callback)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return c.processSpotifyData(ctx, raw) return c.processSpotifyData(ctx, raw, callback)
} }
func (c *SpotifyMetadataClient) getRawSpotifyData(ctx context.Context, parsed spotifyURI, batch bool, delay time.Duration) (interface{}, error) { func (c *SpotifyMetadataClient) getRawSpotifyData(ctx context.Context, parsed spotifyURI, batch bool, delay time.Duration, callback MetadataCallback) (interface{}, error) {
switch parsed.Type { switch parsed.Type {
case "playlist": case "playlist":
return c.fetchPlaylist(ctx, parsed.ID) return c.fetchPlaylist(ctx, parsed.ID, callback)
case "album": case "album":
return c.fetchAlbum(ctx, parsed.ID) return c.fetchAlbum(ctx, parsed.ID, callback)
case "track": case "track":
return c.fetchTrack(ctx, parsed.ID) return c.fetchTrack(ctx, parsed.ID)
case "artist_discography": case "artist_discography":
return c.fetchArtistDiscography(ctx, parsed) return c.fetchArtistDiscography(ctx, parsed, callback)
case "artist": case "artist":
discographyParsed := spotifyURI{Type: "artist_discography", ID: parsed.ID, DiscographyGroup: "all"} discographyParsed := spotifyURI{Type: "artist_discography", ID: parsed.ID, DiscographyGroup: "all"}
return c.fetchArtistDiscography(ctx, discographyParsed) return c.fetchArtistDiscography(ctx, discographyParsed, callback)
default: default:
return nil, fmt.Errorf("unsupported Spotify type: %s", parsed.Type) return nil, fmt.Errorf("unsupported Spotify type: %s", parsed.Type)
} }
} }
func (c *SpotifyMetadataClient) processSpotifyData(ctx context.Context, raw interface{}) (interface{}, error) { func (c *SpotifyMetadataClient) processSpotifyData(ctx context.Context, raw interface{}, callback MetadataCallback) (interface{}, error) {
switch payload := raw.(type) { switch payload := raw.(type) {
case *apiPlaylistResponse: case *apiPlaylistResponse:
return c.formatPlaylistData(payload), nil return c.formatPlaylistData(payload, callback), nil
case *apiAlbumResponse: case *apiAlbumResponse:
return c.formatAlbumData(payload) return c.formatAlbumData(payload, callback)
case *apiTrackResponse: case *apiTrackResponse:
return c.formatTrackData(payload), nil return c.formatTrackData(payload), nil
case *apiArtistResponse: case *apiArtistResponse:
return c.formatArtistDiscographyData(ctx, payload) return c.formatArtistDiscographyData(ctx, payload, callback)
default: default:
return nil, errors.New("unknown raw payload type") return nil, errors.New("unknown raw payload type")
} }
@@ -437,7 +444,7 @@ func (c *SpotifyMetadataClient) fetchTrack(ctx context.Context, trackID string)
if albumID != "" { if albumID != "" {
albumResponse, err := c.fetchAlbumWithClient(ctx, client, albumID) albumResponse, err := c.fetchAlbumWithClient(ctx, client, albumID, nil)
if err == nil && albumResponse != nil { if err == nil && albumResponse != nil {
albumJSON, _ := json.Marshal(albumResponse) albumJSON, _ := json.Marshal(albumResponse)
@@ -482,7 +489,7 @@ func (c *SpotifyMetadataClient) fetchTrack(ctx context.Context, trackID string)
} }
} }
filteredData := FilterTrack(data, albumFetchData) filteredData := FilterTrack(data, c.Separator, albumFetchData)
jsonData, err := json.Marshal(filteredData) jsonData, err := json.Marshal(filteredData)
if err != nil { if err != nil {
@@ -497,15 +504,15 @@ func (c *SpotifyMetadataClient) fetchTrack(ctx context.Context, trackID string)
return &result, nil return &result, nil
} }
func (c *SpotifyMetadataClient) fetchAlbum(ctx context.Context, albumID string) (*apiAlbumResponse, error) { func (c *SpotifyMetadataClient) fetchAlbum(ctx context.Context, albumID string, callback MetadataCallback) (*apiAlbumResponse, error) {
client := NewSpotifyClient() client := NewSpotifyClient()
if err := client.Initialize(); err != nil { if err := client.Initialize(); err != nil {
return nil, fmt.Errorf("failed to initialize spotify client: %w", err) return nil, fmt.Errorf("failed to initialize spotify client: %w", err)
} }
return c.fetchAlbumWithClient(ctx, client, albumID) return c.fetchAlbumWithClient(ctx, client, albumID, callback)
} }
func (c *SpotifyMetadataClient) fetchAlbumWithClient(ctx context.Context, client *SpotifyClient, albumID string) (*apiAlbumResponse, error) { func (c *SpotifyMetadataClient) fetchAlbumWithClient(ctx context.Context, client *SpotifyClient, albumID string, callback MetadataCallback) (*apiAlbumResponse, error) {
allItems := []interface{}{} allItems := []interface{}{}
offset := 0 offset := 0
@@ -537,6 +544,15 @@ func (c *SpotifyMetadataClient) fetchAlbumWithClient(ctx context.Context, client
if data == nil { if data == nil {
data = response data = response
if callback != nil {
filtered := FilterAlbum(data, c.Separator)
jsonData, _ := json.Marshal(filtered)
var result apiAlbumResponse
if json.Unmarshal(jsonData, &result) == nil {
formatted, _ := c.formatAlbumData(&result, nil)
callback(formatted)
}
}
} }
albumData := getMap(getMap(response, "data"), "albumUnion") albumData := getMap(getMap(response, "data"), "albumUnion")
@@ -579,7 +595,7 @@ func (c *SpotifyMetadataClient) fetchAlbumWithClient(ctx context.Context, client
tracksV2["totalCount"] = len(allItems) tracksV2["totalCount"] = len(allItems)
} }
filteredData := FilterAlbum(data) filteredData := FilterAlbum(data, c.Separator)
jsonData, err := json.Marshal(filteredData) jsonData, err := json.Marshal(filteredData)
if err != nil { if err != nil {
@@ -594,7 +610,7 @@ func (c *SpotifyMetadataClient) fetchAlbumWithClient(ctx context.Context, client
return &result, nil return &result, nil
} }
func (c *SpotifyMetadataClient) fetchPlaylist(ctx context.Context, playlistID string) (*apiPlaylistResponse, error) { func (c *SpotifyMetadataClient) fetchPlaylist(ctx context.Context, playlistID string, callback MetadataCallback) (*apiPlaylistResponse, error) {
client := NewSpotifyClient() client := NewSpotifyClient()
if err := client.Initialize(); err != nil { if err := client.Initialize(); err != nil {
return nil, fmt.Errorf("failed to initialize spotify client: %w", err) return nil, fmt.Errorf("failed to initialize spotify client: %w", err)
@@ -630,6 +646,15 @@ func (c *SpotifyMetadataClient) fetchPlaylist(ctx context.Context, playlistID st
if data == nil { if data == nil {
data = response data = response
if callback != nil {
filtered := FilterPlaylist(data, c.Separator)
jsonData, _ := json.Marshal(filtered)
var result apiPlaylistResponse
if json.Unmarshal(jsonData, &result) == nil {
formatted := c.formatPlaylistData(&result, nil)
callback(formatted)
}
}
} }
playlistData := getMap(getMap(response, "data"), "playlistV2") playlistData := getMap(getMap(response, "data"), "playlistV2")
@@ -672,7 +697,7 @@ func (c *SpotifyMetadataClient) fetchPlaylist(ctx context.Context, playlistID st
content["totalCount"] = len(allItems) content["totalCount"] = len(allItems)
} }
filteredData := FilterPlaylist(data) filteredData := FilterPlaylist(data, c.Separator)
jsonData, err := json.Marshal(filteredData) jsonData, err := json.Marshal(filteredData)
if err != nil { if err != nil {
@@ -687,7 +712,7 @@ func (c *SpotifyMetadataClient) fetchPlaylist(ctx context.Context, playlistID st
return &result, nil return &result, nil
} }
func (c *SpotifyMetadataClient) fetchArtistDiscography(ctx context.Context, parsed spotifyURI) (*apiArtistResponse, error) { func (c *SpotifyMetadataClient) fetchArtistDiscography(ctx context.Context, parsed spotifyURI, callback MetadataCallback) (*apiArtistResponse, error) {
client := NewSpotifyClient() client := NewSpotifyClient()
if err := client.Initialize(); err != nil { if err := client.Initialize(); err != nil {
return nil, fmt.Errorf("failed to initialize spotify client: %w", err) return nil, fmt.Errorf("failed to initialize spotify client: %w", err)
@@ -712,6 +737,16 @@ func (c *SpotifyMetadataClient) fetchArtistDiscography(ctx context.Context, pars
return nil, fmt.Errorf("failed to query artist overview: %w", err) return nil, fmt.Errorf("failed to query artist overview: %w", err)
} }
if callback != nil {
filtered := FilterArtist(data, c.Separator)
jsonData, _ := json.Marshal(filtered)
var result apiArtistResponse
if json.Unmarshal(jsonData, &result) == nil {
formatted, _ := c.formatArtistDiscographyData(ctx, &result, nil)
callback(formatted)
}
}
allDiscographyItems := []interface{}{} allDiscographyItems := []interface{}{}
offset := 0 offset := 0
limit := 50 limit := 50
@@ -841,7 +876,7 @@ func (c *SpotifyMetadataClient) fetchArtistDiscography(ctx context.Context, pars
} }
} }
filteredData := FilterArtist(data) filteredData := FilterArtist(data, c.Separator)
jsonData, err := json.Marshal(filteredData) jsonData, err := json.Marshal(filteredData)
if err != nil { if err != nil {
@@ -898,7 +933,7 @@ func (c *SpotifyMetadataClient) formatTrackData(raw *apiTrackResponse) TrackResp
} }
} }
func (c *SpotifyMetadataClient) formatAlbumData(raw *apiAlbumResponse) (*AlbumResponsePayload, error) { func (c *SpotifyMetadataClient) formatAlbumData(raw *apiAlbumResponse, callback MetadataCallback) (*AlbumResponsePayload, error) {
var artistID, artistURL string var artistID, artistURL string
info := AlbumInfoMetadata{ info := AlbumInfoMetadata{
@@ -911,6 +946,13 @@ func (c *SpotifyMetadataClient) formatAlbumData(raw *apiAlbumResponse) (*AlbumRe
ArtistURL: artistURL, ArtistURL: artistURL,
} }
if callback != nil {
callback(AlbumResponsePayload{
AlbumInfo: info,
TrackList: []AlbumTrackMetadata{},
})
}
tracks := make([]AlbumTrackMetadata, 0, len(raw.Tracks)) tracks := make([]AlbumTrackMetadata, 0, len(raw.Tracks))
for idx, item := range raw.Tracks { for idx, item := range raw.Tracks {
durationMS := parseDuration(item.Duration) durationMS := parseDuration(item.Duration)
@@ -955,13 +997,17 @@ func (c *SpotifyMetadataClient) formatAlbumData(raw *apiAlbumResponse) (*AlbumRe
}) })
} }
if callback != nil {
callback(tracks)
}
return &AlbumResponsePayload{ return &AlbumResponsePayload{
AlbumInfo: info, AlbumInfo: info,
TrackList: tracks, TrackList: tracks,
}, nil }, nil
} }
func (c *SpotifyMetadataClient) formatPlaylistData(raw *apiPlaylistResponse) PlaylistResponsePayload { func (c *SpotifyMetadataClient) formatPlaylistData(raw *apiPlaylistResponse, callback MetadataCallback) PlaylistResponsePayload {
var info PlaylistInfoMetadata var info PlaylistInfoMetadata
info.Tracks.Total = raw.Count info.Tracks.Total = raw.Count
info.Followers.Total = raw.Followers info.Followers.Total = raw.Followers
@@ -971,6 +1017,13 @@ func (c *SpotifyMetadataClient) formatPlaylistData(raw *apiPlaylistResponse) Pla
info.Cover = raw.Cover info.Cover = raw.Cover
info.Description = raw.Description info.Description = raw.Description
if callback != nil {
callback(PlaylistResponsePayload{
PlaylistInfo: info,
TrackList: []AlbumTrackMetadata{},
})
}
tracks := make([]AlbumTrackMetadata, 0, len(raw.Tracks)) tracks := make([]AlbumTrackMetadata, 0, len(raw.Tracks))
for _, item := range raw.Tracks { for _, item := range raw.Tracks {
durationMS := parseDuration(item.Duration) durationMS := parseDuration(item.Duration)
@@ -1015,13 +1068,17 @@ func (c *SpotifyMetadataClient) formatPlaylistData(raw *apiPlaylistResponse) Pla
}) })
} }
if callback != nil {
callback(tracks)
}
return PlaylistResponsePayload{ return PlaylistResponsePayload{
PlaylistInfo: info, PlaylistInfo: info,
TrackList: tracks, TrackList: tracks,
} }
} }
func (c *SpotifyMetadataClient) formatArtistDiscographyData(ctx context.Context, raw *apiArtistResponse) (*ArtistDiscographyPayload, error) { func (c *SpotifyMetadataClient) formatArtistDiscographyData(ctx context.Context, raw *apiArtistResponse, callback MetadataCallback) (*ArtistDiscographyPayload, error) {
discType := "all" discType := "all"
info := ArtistInfoMetadata{ info := ArtistInfoMetadata{
@@ -1067,7 +1124,17 @@ func (c *SpotifyMetadataClient) formatArtistDiscographyData(ctx context.Context,
Images: alb.Cover, Images: alb.Cover,
ExternalURL: fmt.Sprintf("https://open.spotify.com/album/%s", alb.ID), ExternalURL: fmt.Sprintf("https://open.spotify.com/album/%s", alb.ID),
}) })
}
if callback != nil {
callback(ArtistDiscographyPayload{
ArtistInfo: info,
AlbumList: albumList,
TrackList: []AlbumTrackMetadata{},
})
}
for _, alb := range raw.Discography.All {
go func(albumID string, albumName string) { go func(albumID string, albumName string) {
sem <- struct{}{} sem <- struct{}{}
@@ -1081,7 +1148,7 @@ func (c *SpotifyMetadataClient) formatArtistDiscographyData(ctx context.Context,
default: default:
} }
albumData, err := c.fetchAlbumWithClient(ctx, sharedClient, albumID) albumData, err := c.fetchAlbumWithClient(ctx, sharedClient, albumID, nil)
if err != nil { if err != nil {
fmt.Printf("Error getting tracks for album %s: %v\n", albumName, err) fmt.Printf("Error getting tracks for album %s: %v\n", albumName, err)
resultsChan <- fetchResult{tracks: []AlbumTrackMetadata{}} resultsChan <- fetchResult{tracks: []AlbumTrackMetadata{}}
@@ -1131,6 +1198,9 @@ func (c *SpotifyMetadataClient) formatArtistDiscographyData(ctx context.Context,
IsExplicit: tr.IsExplicit, IsExplicit: tr.IsExplicit,
}) })
} }
if callback != nil {
callback(tracks)
}
resultsChan <- fetchResult{tracks: tracks} resultsChan <- fetchResult{tracks: tracks}
}(alb.ID, alb.Name) }(alb.ID, alb.Name)
} }
@@ -1290,7 +1360,7 @@ func (c *SpotifyMetadataClient) Search(ctx context.Context, query string, limit
return nil, fmt.Errorf("failed to query search: %w", err) return nil, fmt.Errorf("failed to query search: %w", err)
} }
filteredData := FilterSearch(data) filteredData := FilterSearch(data, c.Separator)
jsonData, err := json.Marshal(filteredData) jsonData, err := json.Marshal(filteredData)
if err != nil { if err != nil {
@@ -1407,7 +1477,7 @@ func (c *SpotifyMetadataClient) SearchByType(ctx context.Context, query string,
return nil, fmt.Errorf("failed to query search: %w", err) return nil, fmt.Errorf("failed to query search: %w", err)
} }
filteredData := FilterSearch(data) filteredData := FilterSearch(data, c.Separator)
jsonData, err := json.Marshal(filteredData) jsonData, err := json.Marshal(filteredData)
if err != nil { if err != nil {
+7 -5
View File
@@ -21,6 +21,7 @@ interface ArtistInfoProps {
header?: string; header?: string;
gallery?: string[]; gallery?: string[];
followers: number; followers: number;
total_albums?: number;
genres: string[]; genres: string[];
biography?: string; biography?: string;
verified?: boolean; verified?: boolean;
@@ -99,6 +100,7 @@ export function ArtistInfo({ artistInfo, albumList, trackList, searchQuery, sort
const [downloadingGalleryIndex, setDownloadingGalleryIndex] = useState<number | null>(null); const [downloadingGalleryIndex, setDownloadingGalleryIndex] = useState<number | null>(null);
const [downloadingAllGallery, setDownloadingAllGallery] = useState(false); const [downloadingAllGallery, setDownloadingAllGallery] = useState(false);
const [activeTab, setActiveTab] = useState<"albums" | "tracks" | "gallery">("albums"); const [activeTab, setActiveTab] = useState<"albums" | "tracks" | "gallery">("albums");
const displayedAlbumCount = artistInfo.total_albums || albumList.length;
const filteredAlbumGroups = useMemo(() => { const filteredAlbumGroups = useMemo(() => {
const albumTypeMap = new Map(albumList.map(a => [a.name, a.album_type])); const albumTypeMap = new Map(albumList.map(a => [a.name, a.album_type]));
const albumGroups = trackList.reduce((acc, track) => { const albumGroups = trackList.reduce((acc, track) => {
@@ -330,9 +332,9 @@ export function ArtistInfo({ artistInfo, albumList, trackList, searchQuery, sort
</>)} </>)}
</div> </div>
<div className="flex items-center gap-2 text-sm flex-wrap text-white/90"> <div className="flex items-center gap-2 text-sm flex-wrap text-white/90">
<span>{albumList.length} {albumList.length === 1 ? "album" : "albums"}</span> <span>{displayedAlbumCount.toLocaleString()} {displayedAlbumCount === 1 ? "album" : "albums"}</span>
<span></span> <span></span>
<span>{trackList.length} {trackList.length === 1 ? "track" : "tracks"}</span> <span>{trackList.length.toLocaleString()} {trackList.length === 1 ? "track" : "tracks"}</span>
{artistInfo.genres.length > 0 && (<> {artistInfo.genres.length > 0 && (<>
<span></span> <span></span>
<span>{artistInfo.genres.join(", ")}</span> <span>{artistInfo.genres.join(", ")}</span>
@@ -383,9 +385,9 @@ export function ArtistInfo({ artistInfo, albumList, trackList, searchQuery, sort
</>)} </>)}
</div> </div>
<div className="flex items-center gap-2 text-sm flex-wrap"> <div className="flex items-center gap-2 text-sm flex-wrap">
<span>{albumList.length} {albumList.length === 1 ? "album" : "albums"}</span> <span>{displayedAlbumCount.toLocaleString()} {displayedAlbumCount === 1 ? "album" : "albums"}</span>
<span></span> <span></span>
<span>{trackList.length} {trackList.length === 1 ? "track" : "tracks"}</span> <span>{trackList.length.toLocaleString()} {trackList.length === 1 ? "track" : "tracks"}</span>
{artistInfo.genres.length > 0 && (<> {artistInfo.genres.length > 0 && (<>
<span></span> <span></span>
<span>{artistInfo.genres.join(", ")}</span> <span>{artistInfo.genres.join(", ")}</span>
@@ -412,7 +414,7 @@ export function ArtistInfo({ artistInfo, albumList, trackList, searchQuery, sort
{activeTab === "gallery" && hasGallery && (<div className="space-y-4"> {activeTab === "gallery" && hasGallery && (<div className="space-y-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-2xl font-bold">Gallery ({artistInfo.gallery!.length})</h3> <h3 className="text-2xl font-bold">Gallery ({artistInfo.gallery!.length.toLocaleString()})</h3>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<Button onClick={handleDownloadAllGallery} size="sm" variant="outline" disabled={downloadingAllGallery}> <Button onClick={handleDownloadAllGallery} size="sm" variant="outline" disabled={downloadingAllGallery}>
+72 -1
View File
@@ -1,13 +1,17 @@
import { useState } from "react"; import { useEffect, useRef, useState } from "react";
import { getSettings } from "@/lib/settings"; import { getSettings } from "@/lib/settings";
import { fetchSpotifyMetadata } from "@/lib/api"; import { fetchSpotifyMetadata } from "@/lib/api";
import { toastWithSound as toast } from "@/lib/toast-with-sound"; import { toastWithSound as toast } from "@/lib/toast-with-sound";
import { logger } from "@/lib/logger"; import { logger } from "@/lib/logger";
import { AddFetchHistory } from "../../wailsjs/go/main/App"; import { AddFetchHistory } from "../../wailsjs/go/main/App";
import { EventsOff, EventsOn } from "../../wailsjs/runtime/runtime";
import type { SpotifyMetadataResponse } from "@/types/api"; import type { SpotifyMetadataResponse } from "@/types/api";
export function useMetadata() { export function useMetadata() {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [metadata, setMetadata] = useState<SpotifyMetadataResponse | null>(null); const [metadata, setMetadata] = useState<SpotifyMetadataResponse | null>(null);
const loadingToastId = useRef<string | number | null>(null);
const fetchedCount = useRef(0);
const currentName = useRef("");
const [showApiModal, setShowApiModal] = useState(false); const [showApiModal, setShowApiModal] = useState(false);
const [showAlbumDialog, setShowAlbumDialog] = useState(false); const [showAlbumDialog, setShowAlbumDialog] = useState(false);
const [selectedAlbum, setSelectedAlbum] = useState<{ const [selectedAlbum, setSelectedAlbum] = useState<{
@@ -16,6 +20,73 @@ export function useMetadata() {
external_urls: string; external_urls: string;
} | null>(null); } | null>(null);
const [pendingArtistName, setPendingArtistName] = useState<string | null>(null); const [pendingArtistName, setPendingArtistName] = useState<string | null>(null);
useEffect(() => {
if (loading) {
fetchedCount.current = 0;
currentName.current = "";
loadingToastId.current = toast.silentInfo("fetching metadata...", {
duration: Infinity,
description: "please wait while we retrieve the information"
});
return;
}
if (loadingToastId.current) {
toast.dismiss(loadingToastId.current);
loadingToastId.current = null;
}
}, [loading]);
useEffect(() => {
const handler = (data: any) => {
if (!data) {
return;
}
if (Array.isArray(data)) {
fetchedCount.current += data.length;
if (loadingToastId.current && currentName.current) {
toast.silentInfo(`fetching tracks for ${currentName.current.toLowerCase()}...`, {
id: loadingToastId.current,
description: `${fetchedCount.current.toLocaleString()} tracks fetched`
});
}
}
else {
const baseInfo = data;
const name = "artist_info" in baseInfo ? baseInfo.artist_info.name :
"album_info" in baseInfo ? baseInfo.album_info.name :
"playlist_info" in baseInfo ? (baseInfo.playlist_info.name || baseInfo.playlist_info.owner.name) : "";
if (name) {
currentName.current = name;
if (loadingToastId.current) {
toast.silentInfo(`fetching tracks for ${name.toLowerCase()}...`, {
id: loadingToastId.current,
description: `${fetchedCount.current.toLocaleString()} tracks fetched`
});
}
}
}
setMetadata(prev => {
if (Array.isArray(data)) {
if (!prev || !("track_list" in prev)) {
return prev;
}
return {
...prev,
track_list: [...prev.track_list, ...data]
};
}
if (prev && "track_list" in prev && prev.track_list.length > 0) {
return prev;
}
const baseInfo = data;
if (!("track_list" in baseInfo)) {
baseInfo.track_list = [];
}
return baseInfo;
});
};
EventsOn("metadata-stream", handler);
return () => EventsOff("metadata-stream");
}, []);
const getUrlType = (url: string): string => { const getUrlType = (url: string): string => {
if (url.includes("/track/")) if (url.includes("/track/"))
return "track"; return "track";
+13 -5
View File
@@ -5,41 +5,49 @@ import { getSettings } from "./settings";
const toastStyle = { const toastStyle = {
className: "font-mono lowercase", className: "font-mono lowercase",
}; };
type ToastData = Parameters<typeof toast.success>[1];
const isSfxEnabled = () => getSettings().sfxEnabled; const isSfxEnabled = () => getSettings().sfxEnabled;
export const toastWithSound = { export const toastWithSound = {
success: (message: string, data?: any) => { success: (message: string, data?: ToastData) => {
const msg = message.toLowerCase(); const msg = message.toLowerCase();
logger.success(msg); logger.success(msg);
if (isSfxEnabled()) if (isSfxEnabled())
playSuccessSound(); playSuccessSound();
return toast.success(msg, { ...toastStyle, ...data }); return toast.success(msg, { ...toastStyle, ...data });
}, },
error: (message: string, data?: any) => { error: (message: string, data?: ToastData) => {
const msg = message.toLowerCase(); const msg = message.toLowerCase();
logger.error(msg); logger.error(msg);
if (isSfxEnabled()) if (isSfxEnabled())
playErrorSound(); playErrorSound();
return toast.error(msg, { ...toastStyle, ...data }); return toast.error(msg, { ...toastStyle, ...data });
}, },
warning: (message: string, data?: any) => { warning: (message: string, data?: ToastData) => {
const msg = message.toLowerCase(); const msg = message.toLowerCase();
logger.warning(msg); logger.warning(msg);
if (isSfxEnabled()) if (isSfxEnabled())
playWarningSound(); playWarningSound();
return toast.warning(msg, { ...toastStyle, ...data }); return toast.warning(msg, { ...toastStyle, ...data });
}, },
info: (message: string, data?: any) => { info: (message: string, data?: ToastData) => {
const msg = message.toLowerCase(); const msg = message.toLowerCase();
logger.info(msg); logger.info(msg);
if (isSfxEnabled()) if (isSfxEnabled())
playInfoSound(); playInfoSound();
return toast.info(msg, { ...toastStyle, ...data }); return toast.info(msg, { ...toastStyle, ...data });
}, },
message: (message: string, data?: any) => { message: (message: string, data?: ToastData) => {
const msg = message.toLowerCase(); const msg = message.toLowerCase();
logger.info(msg); logger.info(msg);
if (isSfxEnabled()) if (isSfxEnabled())
playInfoSound(); playInfoSound();
return toast(msg, { ...toastStyle, ...data }); return toast(msg, { ...toastStyle, ...data });
}, },
silentInfo: (message: string, data?: ToastData) => {
const msg = message.toLowerCase();
logger.info(msg);
return toast.info(msg, { ...toastStyle, ...data });
},
dismiss: (id?: string | number) => toast.dismiss(id),
toast: toast,
}; };