mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-03 19:27:57 +02:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e9c7bf830e | |||
| 8bc97d5bd3 | |||
| f2c241c323 | |||
| 9c512ffe28 | |||
| 53a1da6249 | |||
| d4274e8ca8 | |||
| 49a9f12841 |
+22
-4
@@ -1,6 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
## [2.1.5-preview] - 2026-01-07
|
||||
## [2.1.5] - 2026-01-08
|
||||
|
||||
### Added
|
||||
- **Deezer as Alternative Metadata Source**: Choose between Deezer or Spotify for search
|
||||
@@ -11,13 +11,31 @@
|
||||
- Uses SongLink/Odesli API to convert Spotify track/album ID to Deezer ID
|
||||
- Fetches metadata from Deezer instead
|
||||
- Works for tracks and albums (playlists are user-specific, artists require Spotify API)
|
||||
- **Debug Logging for Search Source**: Console logs now show which metadata source is being used
|
||||
- `[Search] Using metadata source: deezer/spotify for query: "..."`
|
||||
- `[FetchURL] Fetching track with Deezer fallback enabled...`
|
||||
|
||||
### Changed
|
||||
- **Default Download Service**: Changed from Tidal to Qobuz
|
||||
- Fallback order is now: Qobuz → Tidal → Amazon
|
||||
- **Deezer API Updated to v2.0**: More reliable and complete metadata
|
||||
- Direct ISRC lookup via `/track/isrc:{ISRC}` endpoint
|
||||
- Search results now fetch full track info to include ISRC
|
||||
|
||||
### Fixed
|
||||
- **Progress Bar Not Updating**: Fixed bug where download progress jumped from 1% directly to 100%
|
||||
- Progress now updates smoothly every 64KB of data received
|
||||
- First progress update happens immediately when download starts
|
||||
- **Incomplete Downloads**: Fixed bug where interrupted downloads could result in corrupted/incomplete files
|
||||
- File size is validated against server's Content-Length header
|
||||
- Incomplete files are automatically deleted and error is reported
|
||||
- Applies to all services: Tidal, Qobuz, and Amazon
|
||||
- **ISRC Not Available from Deezer Search**: Search results now fetch full track details to get ISRC
|
||||
- Improves track matching accuracy when downloading
|
||||
|
||||
### Technical
|
||||
- New settings field: `metadataSource` in `lib/models/settings.dart`
|
||||
- New UI: Search Source selector in Options Settings page
|
||||
- Improved `ItemProgressWriter` with threshold-based progress updates
|
||||
- Download functions now properly handle network interruptions
|
||||
- Deezer API base URL changed to `https://api.deezer.com/2.0`
|
||||
|
||||
## [2.1.0] - 2026-01-06
|
||||
|
||||
|
||||
@@ -32,9 +32,6 @@ SpotiFLAC supports two metadata sources for searching tracks:
|
||||
| **Deezer** (Default) | No developer account needed, rate limit per user IP | Slightly less comprehensive catalog |
|
||||
| **Spotify** | More comprehensive catalog, better search results | Requires developer API credentials to avoid rate limiting |
|
||||
|
||||
### Using Deezer (Recommended)
|
||||
Works out of the box. No setup required.
|
||||
|
||||
### Using Spotify
|
||||
To use Spotify as your search source without hitting rate limits:
|
||||
1. Create a Spotify Developer account at [developer.spotify.com](https://developer.spotify.com)
|
||||
@@ -43,19 +40,13 @@ To use Spotify as your search source without hitting rate limits:
|
||||
4. Enter your Client ID and Secret
|
||||
5. Change **Search Source** to Spotify
|
||||
|
||||
> **Note**: Spotify URLs (track, album) are always supported regardless of your search source setting. The app will automatically fall back to Deezer if Spotify API is rate limited.
|
||||
|
||||
## Support
|
||||
|
||||
If you find this app useful, consider supporting the development:
|
||||
|
||||
[](https://ko-fi.com/zarzet)
|
||||
|
||||
## Other project
|
||||
|
||||
### [SpotiFLAC (Desktop)](https://github.com/afkarxyz/SpotiFLAC)
|
||||
Get Spotify tracks in true FLAC from Tidal, Qobuz & Amazon Music for Windows, macOS & Linux
|
||||
|
||||
[](https://ko-fi.com/zarzet)
|
||||
|
||||
## Disclaimer
|
||||
|
||||
> **iOS Support**: This app is primarily tested on Android. iOS support is experimental and may have bugs — the developer is too poor to afford an iPhone for proper testing. If you encounter issues on iOS, please report them!
|
||||
|
||||
+31
-11
@@ -294,35 +294,55 @@ func (a *AmazonDownloader) DownloadFile(downloadURL, outputPath, itemID string)
|
||||
return fmt.Errorf("download failed: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
expectedSize := resp.ContentLength
|
||||
// Set total bytes if available
|
||||
if resp.ContentLength > 0 && itemID != "" {
|
||||
SetItemBytesTotal(itemID, resp.ContentLength)
|
||||
if expectedSize > 0 && itemID != "" {
|
||||
SetItemBytesTotal(itemID, expectedSize)
|
||||
}
|
||||
|
||||
out, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// Use buffered writer for better performance (256KB buffer)
|
||||
bufWriter := bufio.NewWriterSize(out, 256*1024)
|
||||
defer bufWriter.Flush()
|
||||
|
||||
// Use item progress writer with buffered output
|
||||
var bytesWritten int64
|
||||
var written int64
|
||||
if itemID != "" {
|
||||
pw := NewItemProgressWriter(bufWriter, itemID)
|
||||
bytesWritten, err = io.Copy(pw, resp.Body)
|
||||
written, err = io.Copy(pw, resp.Body)
|
||||
} else {
|
||||
// Fallback: direct copy without progress tracking
|
||||
bytesWritten, err = io.Copy(bufWriter, resp.Body)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write file: %w", err)
|
||||
written, err = io.Copy(bufWriter, resp.Body)
|
||||
}
|
||||
|
||||
fmt.Printf("\r[Amazon] Downloaded: %.2f MB (Complete)\n", float64(bytesWritten)/(1024*1024))
|
||||
// Flush buffer before checking for errors
|
||||
flushErr := bufWriter.Flush()
|
||||
closeErr := out.Close()
|
||||
|
||||
// Check for any errors
|
||||
if err != nil {
|
||||
os.Remove(outputPath)
|
||||
return fmt.Errorf("download interrupted: %w", err)
|
||||
}
|
||||
if flushErr != nil {
|
||||
os.Remove(outputPath)
|
||||
return fmt.Errorf("failed to flush buffer: %w", flushErr)
|
||||
}
|
||||
if closeErr != nil {
|
||||
os.Remove(outputPath)
|
||||
return fmt.Errorf("failed to close file: %w", closeErr)
|
||||
}
|
||||
|
||||
// Verify file size if Content-Length was provided
|
||||
if expectedSize > 0 && written != expectedSize {
|
||||
os.Remove(outputPath)
|
||||
return fmt.Errorf("incomplete download: expected %d bytes, got %d bytes", expectedSize, written)
|
||||
}
|
||||
|
||||
fmt.Printf("\r[Amazon] Downloaded: %.2f MB (Complete)\n", float64(written)/(1024*1024))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
+37
-16
@@ -13,11 +13,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
deezerSearchURL = "https://api.deezer.com/search"
|
||||
deezerTrackURL = "https://api.deezer.com/track/%s"
|
||||
deezerAlbumURL = "https://api.deezer.com/album/%s"
|
||||
deezerArtistURL = "https://api.deezer.com/artist/%s"
|
||||
deezerPlaylistURL = "https://api.deezer.com/playlist/%s"
|
||||
deezerBaseURL = "https://api.deezer.com/2.0"
|
||||
deezerSearchURL = deezerBaseURL + "/search"
|
||||
deezerTrackURL = deezerBaseURL + "/track/%s"
|
||||
deezerAlbumURL = deezerBaseURL + "/album/%s"
|
||||
deezerArtistURL = deezerBaseURL + "/artist/%s"
|
||||
deezerPlaylistURL = deezerBaseURL + "/playlist/%s"
|
||||
|
||||
deezerCacheTTL = 10 * time.Minute
|
||||
)
|
||||
@@ -152,7 +153,14 @@ func (c *DeezerClient) SearchAll(ctx context.Context, query string, trackLimit,
|
||||
}
|
||||
|
||||
for _, track := range trackResp.Data {
|
||||
result.Tracks = append(result.Tracks, c.convertTrack(track))
|
||||
// Fetch full track info to get ISRC (search results don't include ISRC)
|
||||
fullTrack, err := c.fetchFullTrack(ctx, fmt.Sprintf("%d", track.ID))
|
||||
if err == nil && fullTrack != nil {
|
||||
result.Tracks = append(result.Tracks, c.convertTrack(*fullTrack))
|
||||
} else {
|
||||
// Fallback to search result without ISRC
|
||||
result.Tracks = append(result.Tracks, c.convertTrack(track))
|
||||
}
|
||||
}
|
||||
|
||||
// Search artists
|
||||
@@ -423,23 +431,36 @@ func (c *DeezerClient) GetPlaylist(ctx context.Context, playlistID string) (*Pla
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SearchByISRC searches for a track by ISRC
|
||||
// SearchByISRC searches for a track by ISRC using direct endpoint
|
||||
func (c *DeezerClient) SearchByISRC(ctx context.Context, isrc string) (*TrackMetadata, error) {
|
||||
searchURL := fmt.Sprintf("%s/track?q=isrc:%s&limit=1", deezerSearchURL, isrc)
|
||||
// Use direct ISRC endpoint (API 2.0)
|
||||
// https://api.deezer.com/2.0/track/isrc:{ISRC}
|
||||
directURL := fmt.Sprintf("%s/track/isrc:%s", deezerBaseURL, isrc)
|
||||
|
||||
var resp struct {
|
||||
Data []deezerTrack `json:"data"`
|
||||
}
|
||||
if err := c.getJSON(ctx, searchURL, &resp); err != nil {
|
||||
return nil, err
|
||||
var track deezerTrack
|
||||
if err := c.getJSON(ctx, directURL, &track); err != nil {
|
||||
// Fallback to search if direct endpoint fails
|
||||
searchURL := fmt.Sprintf("%s/track?q=isrc:%s&limit=1", deezerSearchURL, isrc)
|
||||
var resp struct {
|
||||
Data []deezerTrack `json:"data"`
|
||||
}
|
||||
if err := c.getJSON(ctx, searchURL, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(resp.Data) == 0 {
|
||||
return nil, fmt.Errorf("no track found for ISRC: %s", isrc)
|
||||
}
|
||||
result := c.convertTrack(resp.Data[0])
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
if len(resp.Data) == 0 {
|
||||
// Check if we got a valid response (ID > 0)
|
||||
if track.ID == 0 {
|
||||
return nil, fmt.Errorf("no track found for ISRC: %s", isrc)
|
||||
}
|
||||
|
||||
track := c.convertTrack(resp.Data[0])
|
||||
return &track, nil
|
||||
result := c.convertTrack(track)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (c *DeezerClient) fetchFullTrack(ctx context.Context, trackID string) (*deezerTrack, error) {
|
||||
|
||||
@@ -276,12 +276,14 @@ func DownloadWithFallback(requestJSON string) (string, error) {
|
||||
req.OutputDir = strings.TrimSpace(req.OutputDir)
|
||||
|
||||
// Build service order starting with preferred service
|
||||
allServices := []string{"tidal", "qobuz", "amazon"}
|
||||
allServices := []string{"qobuz", "tidal", "amazon"}
|
||||
preferredService := req.Service
|
||||
if preferredService == "" {
|
||||
preferredService = "tidal"
|
||||
preferredService = "qobuz"
|
||||
}
|
||||
|
||||
fmt.Printf("[DownloadWithFallback] Preferred service from request: '%s'\n", req.Service)
|
||||
|
||||
// Create ordered list: preferred first, then others
|
||||
services := []string{preferredService}
|
||||
for _, s := range allServices {
|
||||
@@ -290,9 +292,12 @@ func DownloadWithFallback(requestJSON string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[DownloadWithFallback] Service order: %v\n", services)
|
||||
|
||||
var lastErr error
|
||||
|
||||
for _, service := range services {
|
||||
fmt.Printf("[DownloadWithFallback] Trying service: %s\n", service)
|
||||
req.Service = service
|
||||
|
||||
var result DownloadResult
|
||||
|
||||
+15
-16
@@ -195,39 +195,38 @@ func getDownloadDir() string {
|
||||
}
|
||||
|
||||
// ItemProgressWriter wraps io.Writer to track download progress for a specific item
|
||||
// Uses buffered writing for better performance
|
||||
type ItemProgressWriter struct {
|
||||
writer interface{ Write([]byte) (int, error) }
|
||||
itemID string
|
||||
current int64
|
||||
buffer []byte
|
||||
bufPos int
|
||||
writer interface{ Write([]byte) (int, error) }
|
||||
itemID string
|
||||
current int64
|
||||
lastReported int64 // Track last reported bytes for threshold-based updates
|
||||
}
|
||||
|
||||
const progressWriterBufferSize = 256 * 1024 // 256KB buffer for faster writes
|
||||
const progressUpdateThreshold = 64 * 1024 // Update progress every 64KB
|
||||
|
||||
// NewItemProgressWriter creates a new progress writer for a specific item
|
||||
func NewItemProgressWriter(w interface{ Write([]byte) (int, error) }, itemID string) *ItemProgressWriter {
|
||||
return &ItemProgressWriter{
|
||||
writer: w,
|
||||
itemID: itemID,
|
||||
current: 0,
|
||||
buffer: make([]byte, progressWriterBufferSize),
|
||||
bufPos: 0,
|
||||
writer: w,
|
||||
itemID: itemID,
|
||||
current: 0,
|
||||
lastReported: 0,
|
||||
}
|
||||
}
|
||||
|
||||
// Write implements io.Writer with buffering
|
||||
// Write implements io.Writer with threshold-based progress updates
|
||||
func (pw *ItemProgressWriter) Write(p []byte) (int, error) {
|
||||
n, err := pw.writer.Write(p)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
pw.current += int64(n)
|
||||
|
||||
// Update progress less frequently (every 64KB) to reduce lock contention
|
||||
if pw.current%(64*1024) == 0 || pw.current == 0 {
|
||||
|
||||
// Update progress when we've received at least 64KB since last update
|
||||
// Also update on first write to show download has started
|
||||
if pw.lastReported == 0 || pw.current-pw.lastReported >= progressUpdateThreshold {
|
||||
SetItemBytesReceived(pw.itemID, pw.current)
|
||||
pw.lastReported = pw.current
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
+32
-7
@@ -473,30 +473,55 @@ func (q *QobuzDownloader) DownloadFile(downloadURL, outputPath, itemID string) e
|
||||
return fmt.Errorf("download failed: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
expectedSize := resp.ContentLength
|
||||
// Set total bytes if available
|
||||
if resp.ContentLength > 0 && itemID != "" {
|
||||
SetItemBytesTotal(itemID, resp.ContentLength)
|
||||
if expectedSize > 0 && itemID != "" {
|
||||
SetItemBytesTotal(itemID, expectedSize)
|
||||
}
|
||||
|
||||
out, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// Use buffered writer for better performance (256KB buffer)
|
||||
bufWriter := bufio.NewWriterSize(out, 256*1024)
|
||||
defer bufWriter.Flush()
|
||||
|
||||
// Use item progress writer with buffered output
|
||||
var written int64
|
||||
if itemID != "" {
|
||||
progressWriter := NewItemProgressWriter(bufWriter, itemID)
|
||||
_, err = io.Copy(progressWriter, resp.Body)
|
||||
written, err = io.Copy(progressWriter, resp.Body)
|
||||
} else {
|
||||
// Fallback: direct copy without progress tracking
|
||||
_, err = io.Copy(bufWriter, resp.Body)
|
||||
written, err = io.Copy(bufWriter, resp.Body)
|
||||
}
|
||||
return err
|
||||
|
||||
// Flush buffer before checking for errors
|
||||
flushErr := bufWriter.Flush()
|
||||
closeErr := out.Close()
|
||||
|
||||
// Check for any errors
|
||||
if err != nil {
|
||||
os.Remove(outputPath)
|
||||
return fmt.Errorf("download interrupted: %w", err)
|
||||
}
|
||||
if flushErr != nil {
|
||||
os.Remove(outputPath)
|
||||
return fmt.Errorf("failed to flush buffer: %w", flushErr)
|
||||
}
|
||||
if closeErr != nil {
|
||||
os.Remove(outputPath)
|
||||
return fmt.Errorf("failed to close file: %w", closeErr)
|
||||
}
|
||||
|
||||
// Verify file size if Content-Length was provided
|
||||
if expectedSize > 0 && written != expectedSize {
|
||||
os.Remove(outputPath)
|
||||
return fmt.Errorf("incomplete download: expected %d bytes, got %d bytes", expectedSize, written)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// QobuzDownloadResult contains download result with quality info
|
||||
|
||||
+57
-14
@@ -746,30 +746,55 @@ func (t *TidalDownloader) DownloadFile(downloadURL, outputPath, itemID string) e
|
||||
return fmt.Errorf("download failed: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
expectedSize := resp.ContentLength
|
||||
// Set total bytes if available
|
||||
if resp.ContentLength > 0 && itemID != "" {
|
||||
SetItemBytesTotal(itemID, resp.ContentLength)
|
||||
if expectedSize > 0 && itemID != "" {
|
||||
SetItemBytesTotal(itemID, expectedSize)
|
||||
}
|
||||
|
||||
out, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// Use buffered writer for better performance (256KB buffer)
|
||||
bufWriter := bufio.NewWriterSize(out, 256*1024)
|
||||
defer bufWriter.Flush()
|
||||
|
||||
// Use item progress writer with buffered output
|
||||
var written int64
|
||||
if itemID != "" {
|
||||
progressWriter := NewItemProgressWriter(bufWriter, itemID)
|
||||
_, err = io.Copy(progressWriter, resp.Body)
|
||||
written, err = io.Copy(progressWriter, resp.Body)
|
||||
} else {
|
||||
// Fallback: direct copy without progress tracking
|
||||
_, err = io.Copy(bufWriter, resp.Body)
|
||||
written, err = io.Copy(bufWriter, resp.Body)
|
||||
}
|
||||
return err
|
||||
|
||||
// Flush buffer before checking for errors
|
||||
flushErr := bufWriter.Flush()
|
||||
closeErr := out.Close()
|
||||
|
||||
// Check for any errors
|
||||
if err != nil {
|
||||
os.Remove(outputPath)
|
||||
return fmt.Errorf("download interrupted: %w", err)
|
||||
}
|
||||
if flushErr != nil {
|
||||
os.Remove(outputPath)
|
||||
return fmt.Errorf("failed to flush buffer: %w", flushErr)
|
||||
}
|
||||
if closeErr != nil {
|
||||
os.Remove(outputPath)
|
||||
return fmt.Errorf("failed to close file: %w", closeErr)
|
||||
}
|
||||
|
||||
// Verify file size if Content-Length was provided
|
||||
if expectedSize > 0 && written != expectedSize {
|
||||
os.Remove(outputPath)
|
||||
return fmt.Errorf("incomplete download: expected %d bytes, got %d bytes", expectedSize, written)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TidalDownloader) downloadFromManifest(manifestB64, outputPath, itemID string) error {
|
||||
@@ -805,26 +830,44 @@ func (t *TidalDownloader) downloadFromManifest(manifestB64, outputPath, itemID s
|
||||
return fmt.Errorf("download failed with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
expectedSize := resp.ContentLength
|
||||
// Set total bytes for progress tracking
|
||||
if resp.ContentLength > 0 && itemID != "" {
|
||||
SetItemBytesTotal(itemID, resp.ContentLength)
|
||||
if expectedSize > 0 && itemID != "" {
|
||||
SetItemBytesTotal(itemID, expectedSize)
|
||||
}
|
||||
|
||||
out, err := os.Create(outputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create file: %w", err)
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// Use item progress writer
|
||||
var written int64
|
||||
if itemID != "" {
|
||||
progressWriter := NewItemProgressWriter(out, itemID)
|
||||
_, err = io.Copy(progressWriter, resp.Body)
|
||||
written, err = io.Copy(progressWriter, resp.Body)
|
||||
} else {
|
||||
// Fallback: direct copy without progress tracking
|
||||
_, err = io.Copy(out, resp.Body)
|
||||
written, err = io.Copy(out, resp.Body)
|
||||
}
|
||||
return err
|
||||
|
||||
closeErr := out.Close()
|
||||
|
||||
if err != nil {
|
||||
os.Remove(outputPath)
|
||||
return fmt.Errorf("download interrupted: %w", err)
|
||||
}
|
||||
if closeErr != nil {
|
||||
os.Remove(outputPath)
|
||||
return fmt.Errorf("failed to close file: %w", closeErr)
|
||||
}
|
||||
|
||||
// Verify file size if Content-Length was provided
|
||||
if expectedSize > 0 && written != expectedSize {
|
||||
os.Remove(outputPath)
|
||||
return fmt.Errorf("incomplete download: expected %d bytes, got %d bytes", expectedSize, written)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DASH format - download segments to temporary file
|
||||
|
||||
@@ -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 = '2.1.5-preview';
|
||||
static const String buildNumber = '42';
|
||||
static const String version = '2.1.5';
|
||||
static const String buildNumber = '43';
|
||||
static const String fullVersion = '$version+$buildNumber';
|
||||
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class AppSettings {
|
||||
final String metadataSource; // spotify, deezer - source for search and metadata
|
||||
|
||||
const AppSettings({
|
||||
this.defaultService = 'tidal',
|
||||
this.defaultService = 'qobuz',
|
||||
this.audioQuality = 'LOSSLESS',
|
||||
this.filenameFormat = '{title} - {artist}',
|
||||
this.downloadDirectory = '',
|
||||
|
||||
@@ -5,6 +5,8 @@ import 'package:spotiflac_android/models/settings.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
|
||||
const _settingsKey = 'app_settings';
|
||||
const _migrationVersionKey = 'settings_migration_version';
|
||||
const _currentMigrationVersion = 1;
|
||||
|
||||
class SettingsNotifier extends Notifier<AppSettings> {
|
||||
@override
|
||||
@@ -18,11 +20,35 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
final json = prefs.getString(_settingsKey);
|
||||
if (json != null) {
|
||||
state = AppSettings.fromJson(jsonDecode(json));
|
||||
|
||||
// Run migrations if needed
|
||||
await _runMigrations(prefs);
|
||||
|
||||
// Apply Spotify credentials to Go backend on load
|
||||
_applySpotifyCredentials();
|
||||
}
|
||||
}
|
||||
|
||||
/// Run one-time migrations for settings
|
||||
Future<void> _runMigrations(SharedPreferences prefs) async {
|
||||
final lastMigration = prefs.getInt(_migrationVersionKey) ?? 0;
|
||||
|
||||
if (lastMigration < 1) {
|
||||
// Migration 1: Set metadataSource to 'deezer' for existing users
|
||||
// Only apply if user hasn't enabled custom Spotify credentials
|
||||
// (users with custom credentials likely prefer Spotify)
|
||||
if (!state.useCustomSpotifyCredentials) {
|
||||
state = state.copyWith(metadataSource: 'deezer');
|
||||
await _saveSettings();
|
||||
}
|
||||
}
|
||||
|
||||
// Save current migration version
|
||||
if (lastMigration < _currentMigrationVersion) {
|
||||
await prefs.setInt(_migrationVersionKey, _currentMigrationVersion);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveSettings() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_settingsKey, jsonEncode(state.toJson()));
|
||||
|
||||
@@ -111,7 +111,7 @@ class PlatformBridge {
|
||||
int discNumber = 1,
|
||||
int totalTracks = 1,
|
||||
String? releaseDate,
|
||||
String preferredService = 'tidal',
|
||||
String preferredService = 'qobuz',
|
||||
String? itemId,
|
||||
int durationMs = 0,
|
||||
}) async {
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
name: spotiflac_android
|
||||
description: Download Spotify tracks in FLAC from Tidal, Qobuz & Amazon Music
|
||||
publish_to: 'none'
|
||||
version: 2.1.5-preview+42
|
||||
version: 2.1.5+43
|
||||
|
||||
environment:
|
||||
sdk: ^3.10.0
|
||||
|
||||
Reference in New Issue
Block a user