.remove spotfetch api
This commit is contained in:
@@ -142,7 +142,7 @@ type DownloadRequest struct {
|
|||||||
AlbumArtist string `json:"album_artist,omitempty"`
|
AlbumArtist string `json:"album_artist,omitempty"`
|
||||||
ReleaseDate string `json:"release_date,omitempty"`
|
ReleaseDate string `json:"release_date,omitempty"`
|
||||||
CoverURL string `json:"cover_url,omitempty"`
|
CoverURL string `json:"cover_url,omitempty"`
|
||||||
ApiURL string `json:"api_url,omitempty"`
|
TidalAPIURL string `json:"tidal_api_url,omitempty"`
|
||||||
OutputDir string `json:"output_dir,omitempty"`
|
OutputDir string `json:"output_dir,omitempty"`
|
||||||
AudioFormat string `json:"audio_format,omitempty"`
|
AudioFormat string `json:"audio_format,omitempty"`
|
||||||
FilenameFormat string `json:"filename_format,omitempty"`
|
FilenameFormat string `json:"filename_format,omitempty"`
|
||||||
@@ -247,27 +247,6 @@ func (a *App) GetSpotifyMetadata(req SpotifyMetadataRequest) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil && settings != nil {
|
|
||||||
if useAPI, ok := settings["useSpotFetchAPI"].(bool); ok && useAPI {
|
|
||||||
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)), separator, func(tracks interface{}) {
|
|
||||||
runtime.EventsEmit(a.ctx, "metadata-stream", tracks)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to fetch metadata from API: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonData, err := json.MarshalIndent(data, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to encode response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return string(jsonData), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := backend.GetFilteredSpotifyData(ctx, req.URL, req.Batch, time.Duration(req.Delay*float64(time.Second)), separator, func(tracks interface{}) {
|
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)
|
runtime.EventsEmit(a.ctx, "metadata-stream", tracks)
|
||||||
})
|
})
|
||||||
@@ -518,7 +497,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case "tidal":
|
case "tidal":
|
||||||
if req.ApiURL == "" || req.ApiURL == "auto" {
|
if req.TidalAPIURL == "" || req.TidalAPIURL == "auto" {
|
||||||
downloader := backend.NewTidalDownloader("")
|
downloader := backend.NewTidalDownloader("")
|
||||||
if req.ServiceURL != "" {
|
if req.ServiceURL != "" {
|
||||||
filename, err = downloader.DownloadByURLWithFallback(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, req.Composer, metadataSeparator, req.ISRC, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
filename, err = downloader.DownloadByURLWithFallback(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, req.Composer, metadataSeparator, req.ISRC, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||||
@@ -526,7 +505,7 @@ func (a *App) DownloadTrack(req DownloadRequest) (DownloadResponse, error) {
|
|||||||
filename, err = downloader.Download(req.SpotifyID, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, req.Composer, metadataSeparator, req.ISRC, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
filename, err = downloader.Download(req.SpotifyID, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, req.Composer, metadataSeparator, req.ISRC, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
downloader := backend.NewTidalDownloader(req.ApiURL)
|
downloader := backend.NewTidalDownloader(req.TidalAPIURL)
|
||||||
if req.ServiceURL != "" {
|
if req.ServiceURL != "" {
|
||||||
filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, req.Composer, metadataSeparator, req.ISRC, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
filename, err = downloader.DownloadByURL(req.ServiceURL, req.OutputDir, req.AudioFormat, req.FilenameFormat, req.TrackNumber, req.Position, req.TrackName, req.ArtistName, req.AlbumName, req.AlbumArtist, req.ReleaseDate, req.UseAlbumTrackNumber, req.CoverURL, req.EmbedMaxQualityCover, req.SpotifyTrackNumber, req.SpotifyDiscNumber, req.SpotifyTotalTracks, req.SpotifyTotalDiscs, req.Copyright, req.Publisher, req.Composer, metadataSeparator, req.ISRC, spotifyURL, req.AllowFallback, req.UseFirstArtistOnly, req.UseSingleGenre, req.EmbedGenre)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -50,25 +50,6 @@ func LoadConfigSettings() (map[string]interface{}, error) {
|
|||||||
return settings, nil
|
return settings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetSpotFetchAPISettings() (bool, string) {
|
|
||||||
settings, err := LoadConfigSettings()
|
|
||||||
if err != nil || settings == nil {
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
useAPI, _ := settings["useSpotFetchAPI"].(bool)
|
|
||||||
if !useAPI {
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
apiURL, _ := settings["spotFetchAPIUrl"].(string)
|
|
||||||
if apiURL == "" {
|
|
||||||
apiURL = "https://sp.afkarxyz.qzz.io/api"
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, apiURL
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetRedownloadWithSuffixSetting() bool {
|
func GetRedownloadWithSuffixSetting() bool {
|
||||||
settings, err := LoadConfigSettings()
|
settings, err := LoadConfigSettings()
|
||||||
if err != nil || settings == nil {
|
if err != nil || settings == nil {
|
||||||
|
|||||||
@@ -52,20 +52,6 @@ type SpotifyTrackIdentifiers struct {
|
|||||||
UPC string `json:"upc,omitempty"`
|
UPC string `json:"upc,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type spotFetchIdentifierResponse struct {
|
|
||||||
Input string `json:"input"`
|
|
||||||
TrackID string `json:"track_id"`
|
|
||||||
GID string `json:"gid"`
|
|
||||||
CanonicalURI string `json:"canonical_uri"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Artists []string `json:"artists"`
|
|
||||||
AlbumName string `json:"album_name"`
|
|
||||||
ReleaseDate string `json:"release_date"`
|
|
||||||
Label string `json:"label"`
|
|
||||||
ISRC string `json:"isrc"`
|
|
||||||
UPC string `json:"upc"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetSpotifyTrackIdentifiersDirect(spotifyTrackID string) (SpotifyTrackIdentifiers, error) {
|
func GetSpotifyTrackIdentifiersDirect(spotifyTrackID string) (SpotifyTrackIdentifiers, error) {
|
||||||
normalizedTrackID, err := extractSpotifyTrackID(spotifyTrackID)
|
normalizedTrackID, err := extractSpotifyTrackID(spotifyTrackID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -82,23 +68,6 @@ func GetSpotifyTrackIdentifiersDirect(spotifyTrackID string) (SpotifyTrackIdenti
|
|||||||
identifiers.ISRC = cachedISRC
|
identifiers.ISRC = cachedISRC
|
||||||
}
|
}
|
||||||
|
|
||||||
useSpotFetchAPI, spotFetchAPIURL := GetSpotFetchAPISettings()
|
|
||||||
if useSpotFetchAPI {
|
|
||||||
apiIdentifiers, resolvedTrackID, err := lookupSpotifyTrackIdentifiersViaSpotFetchAPI(normalizedTrackID, spotFetchAPIURL)
|
|
||||||
if err == nil {
|
|
||||||
mergeSpotifyTrackIdentifiers(&identifiers, apiIdentifiers)
|
|
||||||
if identifiers.ISRC != "" {
|
|
||||||
fmt.Printf("Found identifiers via SpotFetch API: isrc=%s upc=%s\n", identifiers.ISRC, identifiers.UPC)
|
|
||||||
cacheResolvedSpotifyTrackISRC(normalizedTrackID, resolvedTrackID, identifiers.ISRC)
|
|
||||||
}
|
|
||||||
if identifiers.ISRC != "" && identifiers.UPC != "" {
|
|
||||||
return identifiers, nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Warning: SpotFetch identifier lookup failed, falling back to Spotify metadata: %v\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
httpClient := &http.Client{Timeout: 30 * time.Second}
|
httpClient := &http.Client{Timeout: 30 * time.Second}
|
||||||
|
|
||||||
payload, metadataErr := fetchSpotifyTrackRawData(httpClient, normalizedTrackID)
|
payload, metadataErr := fetchSpotifyTrackRawData(httpClient, normalizedTrackID)
|
||||||
@@ -172,18 +141,6 @@ func cacheResolvedSpotifyTrackISRC(trackID string, resolvedTrackID string, isrc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SongLinkClient) lookupSpotifyISRCViaSpotFetchAPI(spotifyTrackID string, apiBaseURL string) (string, string, error) {
|
|
||||||
identifiers, resolvedTrackID, err := lookupSpotifyTrackIdentifiersViaSpotFetchAPI(spotifyTrackID, apiBaseURL)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
if identifiers.ISRC == "" {
|
|
||||||
return "", "", fmt.Errorf("ISRC missing in SpotFetch identifier response")
|
|
||||||
}
|
|
||||||
|
|
||||||
return identifiers.ISRC, resolvedTrackID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeSpotifyTrackIdentifiers(target *SpotifyTrackIdentifiers, incoming SpotifyTrackIdentifiers) {
|
func mergeSpotifyTrackIdentifiers(target *SpotifyTrackIdentifiers, incoming SpotifyTrackIdentifiers) {
|
||||||
if incoming.ISRC != "" {
|
if incoming.ISRC != "" {
|
||||||
target.ISRC = strings.TrimSpace(incoming.ISRC)
|
target.ISRC = strings.TrimSpace(incoming.ISRC)
|
||||||
@@ -193,52 +150,6 @@ func mergeSpotifyTrackIdentifiers(target *SpotifyTrackIdentifiers, incoming Spot
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func lookupSpotifyTrackIdentifiersViaSpotFetchAPI(spotifyTrackID string, apiBaseURL string) (SpotifyTrackIdentifiers, string, error) {
|
|
||||||
normalizedTrackID := strings.TrimSpace(spotifyTrackID)
|
|
||||||
baseURL := strings.TrimRight(strings.TrimSpace(apiBaseURL), "/")
|
|
||||||
if normalizedTrackID == "" {
|
|
||||||
return SpotifyTrackIdentifiers{}, "", fmt.Errorf("spotify track ID is required")
|
|
||||||
}
|
|
||||||
if baseURL == "" {
|
|
||||||
return SpotifyTrackIdentifiers{}, "", fmt.Errorf("spotfetch api url is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
requestURL := fmt.Sprintf("%s/identifier/%s", baseURL, url.PathEscape(normalizedTrackID))
|
|
||||||
req, err := http.NewRequest(http.MethodGet, requestURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return SpotifyTrackIdentifiers{}, "", fmt.Errorf("failed to create SpotFetch identifier request: %w", err)
|
|
||||||
}
|
|
||||||
req.Header.Set("User-Agent", songLinkUserAgent)
|
|
||||||
req.Header.Set("Accept", "application/json")
|
|
||||||
|
|
||||||
client := &http.Client{Timeout: 15 * time.Second}
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return SpotifyTrackIdentifiers{}, "", fmt.Errorf("SpotFetch identifier request failed: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
bodyPreview, _ := io.ReadAll(io.LimitReader(resp.Body, 256))
|
|
||||||
return SpotifyTrackIdentifiers{}, "", fmt.Errorf("SpotFetch identifier returned status %d (%s)", resp.StatusCode, strings.TrimSpace(string(bodyPreview)))
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload spotFetchIdentifierResponse
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
|
||||||
return SpotifyTrackIdentifiers{}, "", fmt.Errorf("failed to decode SpotFetch identifier response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
identifiers := SpotifyTrackIdentifiers{
|
|
||||||
ISRC: firstISRCMatch(payload.ISRC),
|
|
||||||
UPC: strings.TrimSpace(payload.UPC),
|
|
||||||
}
|
|
||||||
if identifiers.ISRC == "" && identifiers.UPC == "" {
|
|
||||||
return SpotifyTrackIdentifiers{}, "", fmt.Errorf("identifiers missing in SpotFetch response")
|
|
||||||
}
|
|
||||||
|
|
||||||
return identifiers, strings.TrimSpace(payload.TrackID), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func lookupSpotifyAlbumUPC(albumID string) (string, error) {
|
func lookupSpotifyAlbumUPC(albumID string) (string, error) {
|
||||||
normalizedAlbumID := strings.TrimSpace(albumID)
|
normalizedAlbumID := strings.TrimSpace(albumID)
|
||||||
if normalizedAlbumID == "" {
|
if normalizedAlbumID == "" {
|
||||||
|
|||||||
@@ -1,197 +0,0 @@
|
|||||||
package backend
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func streamTrackListChunks(ctx context.Context, tracks []AlbumTrackMetadata, callback MetadataCallback) error {
|
|
||||||
if callback == nil || len(tracks) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
const chunkSize = 25
|
|
||||||
for start := 0; start < len(tracks); start += chunkSize {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
end := start + chunkSize
|
|
||||||
if end > len(tracks) {
|
|
||||||
end = len(tracks)
|
|
||||||
}
|
|
||||||
|
|
||||||
callback(tracks[start:end])
|
|
||||||
|
|
||||||
if end < len(tracks) {
|
|
||||||
time.Sleep(15 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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 == "" {
|
|
||||||
return GetFilteredSpotifyData(ctx, spotifyURL, batch, delay, separator, callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
spotifyType, id := parseSpotifyURLToTypeAndID(spotifyURL)
|
|
||||||
if spotifyType == "" || id == "" {
|
|
||||||
return nil, fmt.Errorf("invalid Spotify URL: %s", spotifyURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
if spotifyType == "artist" {
|
|
||||||
return GetFilteredSpotifyData(ctx, spotifyURL, batch, delay, separator, callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
apiURL := fmt.Sprintf("%s/%s/%s", strings.TrimSuffix(apiBaseURL, "/"), spotifyType, id)
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create API request: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
client := &http.Client{
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("SpotFetch API request failed: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("SpotFetch API error: HTTP %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
bodyBytes, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read API response: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var data interface{}
|
|
||||||
|
|
||||||
switch spotifyType {
|
|
||||||
case "track":
|
|
||||||
var trackResp TrackResponse
|
|
||||||
if err := json.Unmarshal(bodyBytes, &trackResp); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to decode track response: %w", err)
|
|
||||||
}
|
|
||||||
trackID := strings.TrimSpace(trackResp.Track.SpotifyID)
|
|
||||||
if trackID == "" {
|
|
||||||
trackID = strings.TrimSpace(id)
|
|
||||||
}
|
|
||||||
if trackID != "" {
|
|
||||||
if identifiers, _, err := lookupSpotifyTrackIdentifiersViaSpotFetchAPI(trackID, apiBaseURL); err == nil {
|
|
||||||
if identifiers.UPC != "" {
|
|
||||||
trackResp.Track.UPC = identifiers.UPC
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data = trackResp
|
|
||||||
case "album":
|
|
||||||
var albumResp AlbumResponsePayload
|
|
||||||
if err := json.Unmarshal(bodyBytes, &albumResp); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to decode album response: %w", err)
|
|
||||||
}
|
|
||||||
data = &albumResp
|
|
||||||
if callback != nil {
|
|
||||||
callback(&AlbumResponsePayload{
|
|
||||||
AlbumInfo: albumResp.AlbumInfo,
|
|
||||||
TrackList: []AlbumTrackMetadata{},
|
|
||||||
})
|
|
||||||
if err := streamTrackListChunks(ctx, albumResp.TrackList, callback); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "playlist":
|
|
||||||
var playlistResp PlaylistResponsePayload
|
|
||||||
if err := json.Unmarshal(bodyBytes, &playlistResp); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to decode playlist response: %w", err)
|
|
||||||
}
|
|
||||||
data = playlistResp
|
|
||||||
if callback != nil {
|
|
||||||
callback(PlaylistResponsePayload{
|
|
||||||
PlaylistInfo: playlistResp.PlaylistInfo,
|
|
||||||
TrackList: []AlbumTrackMetadata{},
|
|
||||||
})
|
|
||||||
if err := streamTrackListChunks(ctx, playlistResp.TrackList, callback); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "artist":
|
|
||||||
var artistResp ArtistDiscographyPayload
|
|
||||||
if err := json.Unmarshal(bodyBytes, &artistResp); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to decode artist response: %w", err)
|
|
||||||
}
|
|
||||||
data = &artistResp
|
|
||||||
if callback != nil {
|
|
||||||
callback(&ArtistDiscographyPayload{
|
|
||||||
ArtistInfo: artistResp.ArtistInfo,
|
|
||||||
AlbumList: artistResp.AlbumList,
|
|
||||||
TrackList: []AlbumTrackMetadata{},
|
|
||||||
})
|
|
||||||
if err := streamTrackListChunks(ctx, artistResp.TrackList, callback); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unsupported Spotify type: %s", spotifyType)
|
|
||||||
}
|
|
||||||
|
|
||||||
if callback != nil {
|
|
||||||
switch payload := data.(type) {
|
|
||||||
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,
|
|
||||||
UPC: t.UPC,
|
|
||||||
Plays: t.Plays,
|
|
||||||
PreviewURL: t.PreviewURL,
|
|
||||||
IsExplicit: t.IsExplicit,
|
|
||||||
}})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseSpotifyURLToTypeAndID(url string) (string, string) {
|
|
||||||
|
|
||||||
if strings.HasPrefix(url, "spotify:") {
|
|
||||||
parts := strings.Split(url, ":")
|
|
||||||
if len(parts) >= 3 {
|
|
||||||
return parts[1], parts[2]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
re := regexp.MustCompile(`spotify\.com/(track|album|playlist|artist)/([a-zA-Z0-9]+)`)
|
|
||||||
matches := re.FindStringSubmatch(url)
|
|
||||||
if len(matches) == 3 {
|
|
||||||
return matches[1], matches[2]
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", ""
|
|
||||||
}
|
|
||||||
+1
-31
@@ -3,7 +3,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog";
|
||||||
import { Search, X, ArrowUp } from "lucide-react";
|
import { Search, X, ArrowUp } from "lucide-react";
|
||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { getSettings, getSettingsWithDefaults, loadSettings, saveSettings, applyThemeMode, applyFont, updateSettings } from "@/lib/settings";
|
import { getSettings, getSettingsWithDefaults, loadSettings, saveSettings, applyThemeMode, applyFont } from "@/lib/settings";
|
||||||
import { applyTheme } from "@/lib/themes";
|
import { applyTheme } from "@/lib/themes";
|
||||||
import { OpenFolder, CheckFFmpegInstalled, DownloadFFmpeg, GetBrewPath, GetRecentFetches, InstallFFmpegWithBrew, SaveRecentFetches } from "../wailsjs/go/main/App";
|
import { OpenFolder, CheckFFmpegInstalled, DownloadFFmpeg, GetBrewPath, GetRecentFetches, InstallFFmpegWithBrew, SaveRecentFetches } from "../wailsjs/go/main/App";
|
||||||
import { EventsOn, EventsOff, Quit } from "../wailsjs/runtime/runtime";
|
import { EventsOn, EventsOff, Quit } from "../wailsjs/runtime/runtime";
|
||||||
@@ -211,17 +211,6 @@ function App() {
|
|||||||
window.removeEventListener("scroll", handleScroll);
|
window.removeEventListener("scroll", handleScroll);
|
||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
const handleEnableSpotFetchApi = async () => {
|
|
||||||
try {
|
|
||||||
await updateSettings({ useSpotFetchAPI: true });
|
|
||||||
metadata.setShowApiModal(false);
|
|
||||||
toast.success("SpotFetch API enabled! You can now try fetching again.");
|
|
||||||
}
|
|
||||||
catch (err) {
|
|
||||||
console.error("Failed to enable SpotFetch API:", err);
|
|
||||||
toast.error("Failed to update settings");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const scrollToTop = useCallback(() => {
|
const scrollToTop = useCallback(() => {
|
||||||
window.scrollTo({ top: 0, behavior: "smooth" });
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
}, []);
|
}, []);
|
||||||
@@ -694,25 +683,6 @@ function App() {
|
|||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog open={metadata.showApiModal} onOpenChange={metadata.setShowApiModal}>
|
|
||||||
<DialogContent className="sm:max-w-[425px]">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>SpotFetch API Recommended</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Direct fetch failed. This usually happens when your <span className="text-foreground font-bold">country is blocked</span> by Spotify or your IP is restricted. Would you like to enable the <span className="text-foreground font-bold">SpotFetch API</span> to bypass this?
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="outline" onClick={() => metadata.setShowApiModal(false)}>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button onClick={handleEnableSpotFetchApi}>
|
|
||||||
Enable SpotFetch API
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
</TooltipProvider>);
|
</TooltipProvider>);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,8 @@
|
|||||||
import { X, Minus, Maximize, SlidersHorizontal, Info, Globe } from "lucide-react";
|
import { X, Minus, Maximize, SlidersHorizontal, Globe } from "lucide-react";
|
||||||
import { WindowMinimise, WindowToggleMaximise, Quit } from "../../wailsjs/runtime/runtime";
|
import { WindowMinimise, WindowToggleMaximise, Quit } from "../../wailsjs/runtime/runtime";
|
||||||
import { Menubar, MenubarContent, MenubarMenu, MenubarItem, MenubarTrigger, MenubarLabel, MenubarSeparator } from "@/components/ui/menubar";
|
import { Menubar, MenubarContent, MenubarMenu, MenubarItem, MenubarTrigger } from "@/components/ui/menubar";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
|
||||||
import { getSettings, updateSettings } from "@/lib/settings";
|
|
||||||
import { openExternal } from "@/lib/utils";
|
import { openExternal } from "@/lib/utils";
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
export function TitleBar() {
|
export function TitleBar() {
|
||||||
const [useSpotFetchAPI, setUseSpotFetchAPI] = useState(false);
|
|
||||||
useEffect(() => {
|
|
||||||
const settings = getSettings();
|
|
||||||
if (settings) {
|
|
||||||
setUseSpotFetchAPI(settings.useSpotFetchAPI || false);
|
|
||||||
}
|
|
||||||
const handleSettingsUpdate = (event: any) => {
|
|
||||||
const updatedSettings = event.detail;
|
|
||||||
if (updatedSettings && typeof updatedSettings.useSpotFetchAPI !== 'undefined') {
|
|
||||||
setUseSpotFetchAPI(updatedSettings.useSpotFetchAPI);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('settingsUpdated', handleSettingsUpdate);
|
|
||||||
return () => window.removeEventListener('settingsUpdated', handleSettingsUpdate);
|
|
||||||
}, []);
|
|
||||||
const handleSpotFetchAPIToggle = () => {
|
|
||||||
const newValue = !useSpotFetchAPI;
|
|
||||||
setUseSpotFetchAPI(newValue);
|
|
||||||
updateSettings({ useSpotFetchAPI: newValue });
|
|
||||||
};
|
|
||||||
const handleMinimize = () => {
|
const handleMinimize = () => {
|
||||||
WindowMinimise();
|
WindowMinimise();
|
||||||
};
|
};
|
||||||
@@ -47,26 +24,6 @@ export function TitleBar() {
|
|||||||
<SlidersHorizontal className="w-3.5 h-3.5"/>
|
<SlidersHorizontal className="w-3.5 h-3.5"/>
|
||||||
</MenubarTrigger>
|
</MenubarTrigger>
|
||||||
<MenubarContent align="end" className="min-w-[200px]">
|
<MenubarContent align="end" className="min-w-[200px]">
|
||||||
<div className="flex items-center gap-1.5 px-2 py-1.5">
|
|
||||||
<MenubarLabel className="p-0">SpotFetch API</MenubarLabel>
|
|
||||||
<TooltipProvider delayDuration={300}>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Info className="w-3.5 h-3.5 cursor-help text-muted-foreground"/>
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent side="left" className="max-w-xs">
|
|
||||||
<p className="font-semibold mb-2">Spotify Blocked Countries:</p>
|
|
||||||
<p className="text-xs">Afghanistan, Antarctica, Central African Republic, China, Cuba, Eritrea, Iran, Myanmar, North Korea, Russia, Somalia, South Sudan, Sudan, Syria, Turkmenistan, Yemen</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
<MenubarSeparator />
|
|
||||||
<MenubarItem onClick={handleSpotFetchAPIToggle} className="justify-between">
|
|
||||||
<span>Use SpotFetch API</span>
|
|
||||||
<span className="ml-4">{useSpotFetchAPI ? "✓" : ""}</span>
|
|
||||||
</MenubarItem>
|
|
||||||
<MenubarSeparator />
|
|
||||||
<MenubarItem onClick={() => openExternal("https://afkarxyz.qzz.io")} className="gap-2">
|
<MenubarItem onClick={() => openExternal("https://afkarxyz.qzz.io")} className="gap-2">
|
||||||
<Globe className="w-4 h-4 opacity-70"/>
|
<Globe className="w-4 h-4 opacity-70"/>
|
||||||
<span>Website</span>
|
<span>Website</span>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
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";
|
||||||
@@ -12,7 +11,6 @@ export function useMetadata() {
|
|||||||
const loadingToastId = useRef<string | number | null>(null);
|
const loadingToastId = useRef<string | number | null>(null);
|
||||||
const fetchedCount = useRef(0);
|
const fetchedCount = useRef(0);
|
||||||
const currentName = useRef("");
|
const currentName = useRef("");
|
||||||
const [showApiModal, setShowApiModal] = useState(false);
|
|
||||||
const [showAlbumDialog, setShowAlbumDialog] = useState(false);
|
const [showAlbumDialog, setShowAlbumDialog] = useState(false);
|
||||||
const [selectedAlbum, setSelectedAlbum] = useState<{
|
const [selectedAlbum, setSelectedAlbum] = useState<{
|
||||||
id: string;
|
id: string;
|
||||||
@@ -215,14 +213,8 @@ export function useMetadata() {
|
|||||||
catch (err) {
|
catch (err) {
|
||||||
const errorMsg = err instanceof Error ? err.message : "Failed to fetch metadata";
|
const errorMsg = err instanceof Error ? err.message : "Failed to fetch metadata";
|
||||||
logger.error(`fetch failed: ${errorMsg}`);
|
logger.error(`fetch failed: ${errorMsg}`);
|
||||||
const settings = getSettings();
|
|
||||||
if (!settings.useSpotFetchAPI) {
|
|
||||||
setShowApiModal(true);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
toast.error(errorMsg);
|
toast.error(errorMsg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
finally {
|
finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -323,14 +315,8 @@ export function useMetadata() {
|
|||||||
catch (err) {
|
catch (err) {
|
||||||
const errorMsg = err instanceof Error ? err.message : "Failed to fetch album metadata";
|
const errorMsg = err instanceof Error ? err.message : "Failed to fetch album metadata";
|
||||||
logger.error(`fetch failed: ${errorMsg}`);
|
logger.error(`fetch failed: ${errorMsg}`);
|
||||||
const settings = getSettings();
|
|
||||||
if (!settings.useSpotFetchAPI) {
|
|
||||||
setShowApiModal(true);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
toast.error(errorMsg);
|
toast.error(errorMsg);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
finally {
|
finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setSelectedAlbum(null);
|
setSelectedAlbum(null);
|
||||||
@@ -348,8 +334,6 @@ export function useMetadata() {
|
|||||||
handleConfirmAlbumFetch,
|
handleConfirmAlbumFetch,
|
||||||
handleArtistClick,
|
handleArtistClick,
|
||||||
loadFromCache,
|
loadFromCache,
|
||||||
showApiModal,
|
|
||||||
setShowApiModal,
|
|
||||||
resetMetadata: () => setMetadata(null),
|
resetMetadata: () => setMetadata(null),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ export interface Settings {
|
|||||||
autoOrder: "tidal-qobuz-amazon" | "tidal-amazon-qobuz" | "qobuz-tidal-amazon" | "qobuz-amazon-tidal" | "amazon-tidal-qobuz" | "amazon-qobuz-tidal" | string;
|
autoOrder: "tidal-qobuz-amazon" | "tidal-amazon-qobuz" | "qobuz-tidal-amazon" | "qobuz-amazon-tidal" | "amazon-tidal-qobuz" | "amazon-qobuz-tidal" | string;
|
||||||
autoQuality: "16" | "24";
|
autoQuality: "16" | "24";
|
||||||
allowFallback: boolean;
|
allowFallback: boolean;
|
||||||
useSpotFetchAPI: boolean;
|
|
||||||
spotFetchAPIUrl: string;
|
|
||||||
createPlaylistFolder: boolean;
|
createPlaylistFolder: boolean;
|
||||||
playlistOwnerFolderName: boolean;
|
playlistOwnerFolderName: boolean;
|
||||||
createM3u8File: boolean;
|
createM3u8File: boolean;
|
||||||
@@ -118,8 +116,6 @@ export const DEFAULT_SETTINGS: Settings = {
|
|||||||
autoOrder: "tidal-qobuz-amazon",
|
autoOrder: "tidal-qobuz-amazon",
|
||||||
autoQuality: "16",
|
autoQuality: "16",
|
||||||
allowFallback: true,
|
allowFallback: true,
|
||||||
useSpotFetchAPI: false,
|
|
||||||
spotFetchAPIUrl: "https://sp.afkarxyz.qzz.io/api",
|
|
||||||
createPlaylistFolder: true,
|
createPlaylistFolder: true,
|
||||||
playlistOwnerFolderName: false,
|
playlistOwnerFolderName: false,
|
||||||
createM3u8File: false,
|
createM3u8File: false,
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export interface DownloadRequest {
|
|||||||
album_artist?: string;
|
album_artist?: string;
|
||||||
release_date?: string;
|
release_date?: string;
|
||||||
cover_url?: string;
|
cover_url?: string;
|
||||||
api_url?: string;
|
tidal_api_url?: string;
|
||||||
output_dir?: string;
|
output_dir?: string;
|
||||||
audio_format?: string;
|
audio_format?: string;
|
||||||
folder_name?: string;
|
folder_name?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user