mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-25 01:04:11 +02:00
v3.7.1: YT Music extension priority for YouTube downloads, Qobuz store fallback, queue fixes, server-side search filters
This commit is contained in:
@@ -1,5 +1,26 @@
|
||||
# Changelog
|
||||
|
||||
## [3.7.1] - 2026-03-06
|
||||
|
||||
### Added
|
||||
|
||||
- **Smarter YouTube Downloads**: If the YouTube Music extension is installed, the app now uses it first to find the correct song — more accurate than SongLink, especially for new releases.
|
||||
- **Songs-Only Search Filter**: YouTube Music extension search now filters results server-side, so you only get actual songs — no music videos or covers mixed in.
|
||||
- **Qobuz Search Fallback**: If Qobuz API search returns nothing, the app now tries the Qobuz web store as a backup to find the track.
|
||||
- **Better ISRC Lookup**: Tracks can now be resolved via ISRC even without a Spotify ID, using Deezer as an intermediary.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Download Queue Stability**: Fixed duplicate queue item IDs, cancel not working reliably, and "Clear All" not properly stopping active downloads.
|
||||
- **Queue Restore on Restart**: Duplicate or broken queue item IDs are now auto-fixed when the app restarts.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Update Checker**: The app can now detect updates across all versions, not just within the same major version.
|
||||
- **Localization Cleanup**: Cleaned up and consolidated translation files across all 13 supported languages.
|
||||
|
||||
---
|
||||
|
||||
## [3.7.0] - 2026-03-04
|
||||
|
||||
Hey everyone, thank you so much for sticking with SpotiFLAC Mobile.
|
||||
|
||||
+159
-80
@@ -12,6 +12,8 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -31,13 +33,17 @@ var (
|
||||
const (
|
||||
qobuzTrackGetBaseURL = "https://www.qobuz.com/api.json/0.2/track/get?track_id="
|
||||
qobuzTrackSearchBaseURL = "https://www.qobuz.com/api.json/0.2/track/search?query="
|
||||
qobuzStoreSearchBaseURL = "https://www.qobuz.com/us-en/search/tracks/"
|
||||
qobuzTrackPlayBaseURL = "https://play.qobuz.com/track/"
|
||||
qobuzDownloadAPIURL = "https://www.musicdl.me/api/qobuz/download"
|
||||
qobuzDabMusicAPIURL = "https://dabmusic.xyz/api/stream?trackId="
|
||||
qobuzDeebAPIURL = "https://dab.yeet.su/api/stream?trackId="
|
||||
qobuzSquidAPIURL = "https://qobuz.squid.wtf/api/download-music?country=US&track_id="
|
||||
qobuzDebugKeyXORMask = byte(0x5A)
|
||||
)
|
||||
|
||||
var qobuzStoreTrackIDRegex = regexp.MustCompile(`/v4/ajax/popin-add-cart/track/([0-9]+)`)
|
||||
|
||||
var qobuzDebugKeyObfuscated = []byte{
|
||||
0x69, 0x3b, 0x38, 0x3e, 0x36, 0x37, 0x35, 0x2f, 0x36, 0x3b,
|
||||
0x33, 0x29, 0x2e, 0x32, 0x3f, 0x3d, 0x35, 0x3b, 0x2e, 0x3b,
|
||||
@@ -403,6 +409,7 @@ func (q *QobuzDownloader) GetAvailableProviders() []qobuzAPIProvider {
|
||||
{Name: "dabmusic", URL: qobuzDabMusicAPIURL, Kind: qobuzAPIKindStandard},
|
||||
// "deeb" is mapped from the legacy reference fallback endpoint.
|
||||
{Name: "deeb", URL: qobuzDeebAPIURL, Kind: qobuzAPIKindStandard},
|
||||
{Name: "squid", URL: qobuzSquidAPIURL, Kind: qobuzAPIKindStandard},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,39 +567,18 @@ func getQobuzDebugKey() string {
|
||||
}
|
||||
|
||||
func (q *QobuzDownloader) SearchTrackByISRC(isrc string) (*QobuzTrack, error) {
|
||||
searchURL := fmt.Sprintf("%s%s&limit=50&app_id=%s", qobuzTrackSearchBaseURL, url.QueryEscape(isrc), q.appID)
|
||||
|
||||
req, err := http.NewRequest("GET", searchURL, nil)
|
||||
candidates, err := q.searchQobuzTracksWithFallback(isrc, 50)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := DoRequestWithUserAgent(q.client, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("search failed: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Tracks struct {
|
||||
Items []QobuzTrack `json:"items"`
|
||||
} `json:"tracks"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range result.Tracks.Items {
|
||||
if result.Tracks.Items[i].ISRC == isrc {
|
||||
return &result.Tracks.Items[i], nil
|
||||
for i := range candidates {
|
||||
if candidates[i].ISRC == isrc {
|
||||
return &candidates[i], nil
|
||||
}
|
||||
}
|
||||
|
||||
if len(result.Tracks.Items) == 0 {
|
||||
if len(candidates) == 0 {
|
||||
return nil, fmt.Errorf("no tracks found for ISRC: %s", isrc)
|
||||
}
|
||||
|
||||
@@ -602,38 +588,17 @@ func (q *QobuzDownloader) SearchTrackByISRC(isrc string) (*QobuzTrack, error) {
|
||||
func (q *QobuzDownloader) SearchTrackByISRCWithDuration(isrc string, expectedDurationSec int) (*QobuzTrack, error) {
|
||||
GoLog("[Qobuz] Searching by ISRC: %s\n", isrc)
|
||||
|
||||
searchURL := fmt.Sprintf("%s%s&limit=50&app_id=%s", qobuzTrackSearchBaseURL, url.QueryEscape(isrc), q.appID)
|
||||
|
||||
req, err := http.NewRequest("GET", searchURL, nil)
|
||||
candidates, err := q.searchQobuzTracksWithFallback(isrc, 50)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := DoRequestWithUserAgent(q.client, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("search failed: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Tracks struct {
|
||||
Items []QobuzTrack `json:"items"`
|
||||
} `json:"tracks"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
GoLog("[Qobuz] ISRC search returned %d results\n", len(result.Tracks.Items))
|
||||
GoLog("[Qobuz] ISRC search returned %d results\n", len(candidates))
|
||||
|
||||
var isrcMatches []*QobuzTrack
|
||||
for i := range result.Tracks.Items {
|
||||
if result.Tracks.Items[i].ISRC == isrc {
|
||||
isrcMatches = append(isrcMatches, &result.Tracks.Items[i])
|
||||
for i := range candidates {
|
||||
if candidates[i].ISRC == isrc {
|
||||
isrcMatches = append(isrcMatches, &candidates[i])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -668,7 +633,7 @@ func (q *QobuzDownloader) SearchTrackByISRCWithDuration(isrc string, expectedDur
|
||||
return isrcMatches[0], nil
|
||||
}
|
||||
|
||||
if len(result.Tracks.Items) == 0 {
|
||||
if len(candidates) == 0 {
|
||||
return nil, fmt.Errorf("no tracks found for ISRC: %s", isrc)
|
||||
}
|
||||
|
||||
@@ -725,6 +690,7 @@ func (q *QobuzDownloader) SearchTrackByMetadataWithDuration(trackName, artistNam
|
||||
|
||||
var allTracks []QobuzTrack
|
||||
searchedQueries := make(map[string]bool)
|
||||
seenTrackIDs := make(map[int64]struct{})
|
||||
|
||||
for _, query := range queries {
|
||||
cleanQuery := strings.TrimSpace(query)
|
||||
@@ -735,38 +701,26 @@ func (q *QobuzDownloader) SearchTrackByMetadataWithDuration(trackName, artistNam
|
||||
|
||||
GoLog("[Qobuz] Searching for: %s\n", cleanQuery)
|
||||
|
||||
searchURL := fmt.Sprintf("%s%s&limit=50&app_id=%s", qobuzTrackSearchBaseURL, url.QueryEscape(cleanQuery), q.appID)
|
||||
|
||||
req, err := http.NewRequest("GET", searchURL, nil)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
resp, err := DoRequestWithUserAgent(q.client, req)
|
||||
result, err := q.searchQobuzTracksWithFallback(cleanQuery, 50)
|
||||
if err != nil {
|
||||
GoLog("[Qobuz] Search error for '%s': %v\n", cleanQuery, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
resp.Body.Close()
|
||||
continue
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Tracks struct {
|
||||
Items []QobuzTrack `json:"items"`
|
||||
} `json:"tracks"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
resp.Body.Close()
|
||||
continue
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if len(result.Tracks.Items) > 0 {
|
||||
GoLog("[Qobuz] Found %d results for '%s'\n", len(result.Tracks.Items), cleanQuery)
|
||||
allTracks = append(allTracks, result.Tracks.Items...)
|
||||
if len(result) > 0 {
|
||||
GoLog("[Qobuz] Found %d results for '%s'\n", len(result), cleanQuery)
|
||||
for i := range result {
|
||||
trackID := result[i].ID
|
||||
if trackID <= 0 {
|
||||
allTracks = append(allTracks, result[i])
|
||||
continue
|
||||
}
|
||||
if _, ok := seenTrackIDs[trackID]; ok {
|
||||
continue
|
||||
}
|
||||
seenTrackIDs[trackID] = struct{}{}
|
||||
allTracks = append(allTracks, result[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -837,6 +791,131 @@ func (q *QobuzDownloader) SearchTrackByMetadataWithDuration(trackName, artistNam
|
||||
return nil, fmt.Errorf("no matching track found for: %s - %s", artistName, trackName)
|
||||
}
|
||||
|
||||
func (q *QobuzDownloader) searchQobuzTracksViaAPI(query string, limit int) ([]QobuzTrack, error) {
|
||||
searchURL := fmt.Sprintf("%s%s&limit=%d&app_id=%s", qobuzTrackSearchBaseURL, url.QueryEscape(query), limit, q.appID)
|
||||
req, err := http.NewRequest("GET", searchURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := DoRequestWithUserAgent(q.client, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("search failed: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Tracks struct {
|
||||
Items []QobuzTrack `json:"items"`
|
||||
} `json:"tracks"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result.Tracks.Items, nil
|
||||
}
|
||||
|
||||
func extractQobuzTrackIDsFromStoreSearchHTML(body []byte) []int64 {
|
||||
matches := qobuzStoreTrackIDRegex.FindAllSubmatch(body, -1)
|
||||
if len(matches) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
trackIDs := make([]int64, 0, len(matches))
|
||||
seen := make(map[int64]struct{}, len(matches))
|
||||
for _, match := range matches {
|
||||
if len(match) < 2 {
|
||||
continue
|
||||
}
|
||||
id, err := strconv.ParseInt(string(match[1]), 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
trackIDs = append(trackIDs, id)
|
||||
}
|
||||
return trackIDs
|
||||
}
|
||||
|
||||
func (q *QobuzDownloader) searchQobuzTracksViaStore(query string, limit int) ([]QobuzTrack, error) {
|
||||
searchURL := qobuzStoreSearchBaseURL + url.PathEscape(query)
|
||||
req, err := http.NewRequest("GET", searchURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := DoRequestWithUserAgent(q.client, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("store search failed: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trackIDs := extractQobuzTrackIDsFromStoreSearchHTML(body)
|
||||
if len(trackIDs) == 0 {
|
||||
return nil, fmt.Errorf("store search did not contain track IDs")
|
||||
}
|
||||
|
||||
if limit > 0 && len(trackIDs) > limit {
|
||||
trackIDs = trackIDs[:limit]
|
||||
}
|
||||
|
||||
tracks := make([]QobuzTrack, 0, len(trackIDs))
|
||||
for _, id := range trackIDs {
|
||||
track, trackErr := q.GetTrackByID(id)
|
||||
if trackErr != nil || track == nil {
|
||||
continue
|
||||
}
|
||||
tracks = append(tracks, *track)
|
||||
}
|
||||
|
||||
if len(tracks) == 0 {
|
||||
return nil, fmt.Errorf("store fallback returned IDs but no track metadata could be loaded")
|
||||
}
|
||||
return tracks, nil
|
||||
}
|
||||
|
||||
func (q *QobuzDownloader) searchQobuzTracksWithFallback(query string, limit int) ([]QobuzTrack, error) {
|
||||
apiTracks, apiErr := q.searchQobuzTracksViaAPI(query, limit)
|
||||
if apiErr == nil {
|
||||
if len(apiTracks) > 0 {
|
||||
return apiTracks, nil
|
||||
}
|
||||
GoLog("[Qobuz] API search returned 0 results for '%s', trying store fallback\n", query)
|
||||
} else {
|
||||
GoLog("[Qobuz] API search failed for '%s': %v. Trying store fallback.\n", query, apiErr)
|
||||
}
|
||||
|
||||
storeTracks, storeErr := q.searchQobuzTracksViaStore(query, limit)
|
||||
if storeErr == nil && len(storeTracks) > 0 {
|
||||
GoLog("[Qobuz] Store fallback returned %d candidate tracks for '%s'\n", len(storeTracks), query)
|
||||
return storeTracks, nil
|
||||
}
|
||||
|
||||
if apiErr != nil && storeErr != nil {
|
||||
return nil, fmt.Errorf("api search failed (%v); store fallback failed (%v)", apiErr, storeErr)
|
||||
}
|
||||
if storeErr != nil {
|
||||
return nil, storeErr
|
||||
}
|
||||
return nil, fmt.Errorf("no tracks found for query: %s", query)
|
||||
}
|
||||
|
||||
type qobuzAPIResult struct {
|
||||
provider qobuzAPIProvider
|
||||
info qobuzDownloadInfo
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package gobackend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -36,6 +37,12 @@ var (
|
||||
songLinkClientOnce sync.Once
|
||||
songLinkRegion = "US"
|
||||
songLinkRegionMu sync.RWMutex
|
||||
songLinkSearchByISRC = func(ctx context.Context, isrc string) (*TrackMetadata, error) {
|
||||
return GetDeezerClient().SearchByISRC(ctx, isrc)
|
||||
}
|
||||
songLinkCheckAvailabilityFromDeezer = func(s *SongLinkClient, deezerTrackID string) (*TrackAvailability, error) {
|
||||
return s.CheckAvailabilityFromDeezer(deezerTrackID)
|
||||
}
|
||||
)
|
||||
|
||||
func NewSongLinkClient() *SongLinkClient {
|
||||
@@ -109,6 +116,20 @@ func buildSongLinkURLByPlatform(platform, entityType, entityID, userCountry stri
|
||||
}
|
||||
|
||||
func (s *SongLinkClient) CheckTrackAvailability(spotifyTrackID string, isrc string) (*TrackAvailability, error) {
|
||||
spotifyTrackID = strings.TrimSpace(spotifyTrackID)
|
||||
isrc = strings.ToUpper(strings.TrimSpace(isrc))
|
||||
|
||||
switch {
|
||||
case spotifyTrackID != "":
|
||||
return s.checkTrackAvailabilityFromSpotify(spotifyTrackID)
|
||||
case isrc != "":
|
||||
return s.checkTrackAvailabilityFromISRC(isrc)
|
||||
default:
|
||||
return nil, fmt.Errorf("spotify track ID and ISRC are empty")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SongLinkClient) checkTrackAvailabilityFromSpotify(spotifyTrackID string) (*TrackAvailability, error) {
|
||||
songLinkRateLimiter.WaitForSlot()
|
||||
|
||||
spotifyURL := fmt.Sprintf("https://open.spotify.com/track/%s", spotifyTrackID)
|
||||
@@ -200,6 +221,47 @@ func (s *SongLinkClient) CheckTrackAvailability(spotifyTrackID string, isrc stri
|
||||
return availability, nil
|
||||
}
|
||||
|
||||
func (s *SongLinkClient) checkTrackAvailabilityFromISRC(isrc string) (*TrackAvailability, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), SongLinkTimeout)
|
||||
defer cancel()
|
||||
|
||||
track, err := songLinkSearchByISRC(ctx, isrc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve Deezer track from ISRC %s: %w", isrc, err)
|
||||
}
|
||||
|
||||
deezerTrackID := songLinkExtractDeezerTrackID(track)
|
||||
if deezerTrackID == "" {
|
||||
return nil, fmt.Errorf("failed to resolve Deezer track ID from ISRC %s", isrc)
|
||||
}
|
||||
|
||||
availability, err := songLinkCheckAvailabilityFromDeezer(s, deezerTrackID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve SongLink availability from ISRC %s via Deezer %s: %w", isrc, deezerTrackID, err)
|
||||
}
|
||||
|
||||
return availability, nil
|
||||
}
|
||||
|
||||
func songLinkExtractDeezerTrackID(track *TrackMetadata) string {
|
||||
if track == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if deezerID, ok := strings.CutPrefix(strings.TrimSpace(track.SpotifyID), "deezer:"); ok {
|
||||
deezerID = strings.TrimSpace(deezerID)
|
||||
if deezerID != "" {
|
||||
return deezerID
|
||||
}
|
||||
}
|
||||
|
||||
if deezerID := extractDeezerIDFromURL(strings.TrimSpace(track.ExternalURL)); deezerID != "" {
|
||||
return deezerID
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *SongLinkClient) GetStreamingURLs(spotifyTrackID string) (map[string]string, error) {
|
||||
availability, err := s.CheckTrackAvailability(spotifyTrackID, "")
|
||||
if err != nil {
|
||||
|
||||
+65
-4
@@ -539,12 +539,65 @@ func ExtractYouTubeVideoID(urlStr string) (string, error) {
|
||||
return "", fmt.Errorf("could not extract video ID from URL")
|
||||
}
|
||||
|
||||
// searchYouTubeMusicViaExtension uses the YT Music extension's customSearch
|
||||
// to find a track by artist + title. It filters for tracks only (not videos,
|
||||
// albums, or playlists) and returns the YouTube Music watch URL for the first
|
||||
// matching track, or "" if nothing was found.
|
||||
func searchYouTubeMusicViaExtension(artistName, trackName string) string {
|
||||
extManager := GetExtensionManager()
|
||||
searchProviders := extManager.GetSearchProviders()
|
||||
|
||||
// Find the ytmusic-spotiflac extension
|
||||
var ytProvider *ExtensionProviderWrapper
|
||||
for _, p := range searchProviders {
|
||||
if p.extension.ID == "ytmusic-spotiflac" {
|
||||
ytProvider = p
|
||||
break
|
||||
}
|
||||
}
|
||||
if ytProvider == nil {
|
||||
GoLog("[YouTube] YT Music extension not found or not enabled, skipping fallback\n")
|
||||
return ""
|
||||
}
|
||||
|
||||
query := strings.TrimSpace(artistName + " " + trackName)
|
||||
if query == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
GoLog("[YouTube] Searching YT Music extension for: %s\n", query)
|
||||
results, err := ytProvider.CustomSearch(query, map[string]interface{}{
|
||||
"filter": "tracks",
|
||||
})
|
||||
if err != nil {
|
||||
GoLog("[YouTube] YT Music extension search failed: %v\n", err)
|
||||
return ""
|
||||
}
|
||||
|
||||
// Find the first track result (item_type == "track" with a valid video ID)
|
||||
for _, track := range results {
|
||||
if track.ItemType != "" && track.ItemType != "track" {
|
||||
continue
|
||||
}
|
||||
videoID := strings.TrimSpace(track.ID)
|
||||
if videoID == "" {
|
||||
continue
|
||||
}
|
||||
if isYouTubeVideoID(videoID) {
|
||||
return BuildYouTubeWatchURL(videoID)
|
||||
}
|
||||
}
|
||||
|
||||
GoLog("[YouTube] YT Music extension returned no matching tracks for: %s\n", query)
|
||||
return ""
|
||||
}
|
||||
|
||||
func downloadFromYouTube(req DownloadRequest) (YouTubeDownloadResult, error) {
|
||||
downloader := NewYouTubeDownloader()
|
||||
|
||||
format, bitrate, quality := parseYouTubeQualityInput(req.Quality)
|
||||
|
||||
// URL lookup priority: YouTube video ID > Spotify ID > Deezer ID > ISRC
|
||||
// URL lookup priority: YouTube video ID > YT Music extension > SongLink (Spotify/Deezer/ISRC)
|
||||
var youtubeURL string
|
||||
var lookupErr error
|
||||
|
||||
@@ -554,7 +607,15 @@ func downloadFromYouTube(req DownloadRequest) (YouTubeDownloadResult, error) {
|
||||
GoLog("[YouTube] SpotifyID appears to be YouTube video ID, using directly: %s\n", youtubeURL)
|
||||
}
|
||||
|
||||
// Try Spotify ID via SongLink
|
||||
// Try YT Music extension search first (if installed) - more accurate, tracks only
|
||||
if youtubeURL == "" && (req.TrackName != "" || req.ArtistName != "") {
|
||||
youtubeURL = searchYouTubeMusicViaExtension(req.ArtistName, req.TrackName)
|
||||
if youtubeURL != "" {
|
||||
GoLog("[YouTube] Found YouTube URL via YT Music extension: %s\n", youtubeURL)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: Try Spotify ID via SongLink
|
||||
if youtubeURL == "" && req.SpotifyID != "" && !isYouTubeVideoID(req.SpotifyID) {
|
||||
GoLog("[YouTube] Looking up YouTube URL via SongLink for Spotify ID: %s\n", req.SpotifyID)
|
||||
songlink := NewSongLinkClient()
|
||||
@@ -566,7 +627,7 @@ func downloadFromYouTube(req DownloadRequest) (YouTubeDownloadResult, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Try Deezer ID via SongLink
|
||||
// Fallback: Try Deezer ID via SongLink
|
||||
if youtubeURL == "" && req.DeezerID != "" {
|
||||
GoLog("[YouTube] Looking up YouTube URL via SongLink for Deezer ID: %s\n", req.DeezerID)
|
||||
songlink := NewSongLinkClient()
|
||||
@@ -578,7 +639,7 @@ func downloadFromYouTube(req DownloadRequest) (YouTubeDownloadResult, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Try ISRC via SongLink
|
||||
// Fallback: Try ISRC via SongLink
|
||||
if youtubeURL == "" && req.ISRC != "" {
|
||||
GoLog("[YouTube] Looking up YouTube URL via SongLink for ISRC: %s\n", req.ISRC)
|
||||
songlink := NewSongLinkClient()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
/// App version and info constants
|
||||
/// Update version here only - all other files will reference this
|
||||
class AppInfo {
|
||||
static const String version = '3.7.0';
|
||||
static const String buildNumber = '103';
|
||||
static const String version = '3.7.1';
|
||||
static const String buildNumber = '104';
|
||||
static const String fullVersion = '$version+$buildNumber';
|
||||
|
||||
|
||||
|
||||
@@ -74,13 +74,6 @@ class UpdateChecker {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ignore releases from a different major version (e.g. v4.x when we
|
||||
// rolled back to v3.x). Only offer updates within the same major line.
|
||||
if (_majorVersion(latestVersion) != _majorVersion(AppInfo.version)) {
|
||||
_log.i('Skipping update from different major version (current: ${AppInfo.version}, latest: $latestVersion)');
|
||||
return null;
|
||||
}
|
||||
|
||||
final body = releaseData['body'] as String? ?? 'No changelog available';
|
||||
final htmlUrl = releaseData['html_url'] as String? ?? '${AppInfo.githubUrl}/releases';
|
||||
final publishedAt = DateTime.tryParse(releaseData['published_at'] as String? ?? '') ?? DateTime.now();
|
||||
@@ -125,14 +118,6 @@ class UpdateChecker {
|
||||
}
|
||||
}
|
||||
|
||||
static int _majorVersion(String version) {
|
||||
try {
|
||||
return int.parse(version.split('-').first.split('.').first);
|
||||
} catch (_) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
static bool _isNewerVersion(String latest, String current) {
|
||||
try {
|
||||
final latestBase = latest.split('-').first;
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
name: spotiflac_android
|
||||
description: Download Spotify tracks in FLAC from Tidal, Qobuz & Amazon Music
|
||||
publish_to: "none"
|
||||
version: 3.7.0+103
|
||||
version: 3.7.1+104
|
||||
|
||||
environment:
|
||||
sdk: ^3.10.0
|
||||
|
||||
Reference in New Issue
Block a user