diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index cae6700..ea71ad4 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -222,6 +222,36 @@ jobs:
contents: write
steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Extract changelog for version
+ id: changelog
+ run: |
+ VERSION=${{ needs.get-version.outputs.version }}
+ VERSION_NUM=${VERSION#v} # Remove 'v' prefix
+
+ # Extract changelog section for this version
+ # Look for ## [X.X.X] and capture until next ## [ or end of file
+ CHANGELOG=$(awk -v ver="$VERSION_NUM" '
+ /^## \[/ {
+ if (found) exit
+ if ($0 ~ "\\[" ver "\\]") found=1
+ next
+ }
+ found { print }
+ ' CHANGELOG.md)
+
+ # If no changelog found, use default message
+ if [ -z "$CHANGELOG" ]; then
+ CHANGELOG="See CHANGELOG.md for details."
+ fi
+
+ # Save to file for multiline support
+ echo "$CHANGELOG" > /tmp/changelog.txt
+ echo "Extracted changelog:"
+ cat /tmp/changelog.txt
+
- name: Download Android APK
uses: actions/download-artifact@v4
with:
@@ -234,24 +264,45 @@ jobs:
name: ios-ipa
path: ./release
+ - name: Prepare release body
+ run: |
+ VERSION=${{ needs.get-version.outputs.version }}
+ cat > /tmp/release_body.txt << 'HEADER'
+ ## SpotiFLAC $VERSION
+
+ Download Spotify tracks in FLAC quality from Tidal, Qobuz & Amazon Music.
+
+ ### What's New
+ HEADER
+
+ # Replace $VERSION in header
+ sed -i "s/\$VERSION/$VERSION/g" /tmp/release_body.txt
+
+ cat /tmp/changelog.txt >> /tmp/release_body.txt
+
+ cat >> /tmp/release_body.txt << FOOTER
+
+ ---
+
+ ### Downloads
+ - **Android (arm64)**: \`SpotiFLAC-${VERSION}-arm64.apk\` (recommended)
+ - **Android (arm32)**: \`SpotiFLAC-${VERSION}-arm32.apk\` (older devices)
+ - **iOS**: \`SpotiFLAC-${VERSION}-ios-unsigned.ipa\` (sideload required)
+
+ ### Installation
+ **Android**: Enable "Install from unknown sources" and install the APK
+ **iOS**: Use AltStore, Sideloadly, or similar tools to sideload the IPA
+ FOOTER
+
+ echo "Release body:"
+ cat /tmp/release_body.txt
+
- name: Create Release
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ needs.get-version.outputs.version }}
name: SpotiFLAC ${{ needs.get-version.outputs.version }}
- body: |
- ## SpotiFLAC ${{ needs.get-version.outputs.version }}
-
- Download Spotify tracks in FLAC quality from Tidal, Qobuz & Amazon Music.
-
- ### Downloads
- - **Android (arm64)**: `SpotiFLAC-${{ needs.get-version.outputs.version }}-arm64.apk` (recommended)
- - **Android (arm32)**: `SpotiFLAC-${{ needs.get-version.outputs.version }}-arm32.apk` (older devices)
- - **iOS**: `SpotiFLAC-${{ needs.get-version.outputs.version }}-ios-unsigned.ipa` (sideload required)
-
- ### Installation
- **Android**: Enable "Install from unknown sources" and install the APK
- **iOS**: Use AltStore, Sideloadly, or similar tools to sideload the IPA
+ body_path: /tmp/release_body.txt
files: ./release/*
draft: false
prerelease: false
diff --git a/README.md b/README.md
index 3dc1444..c31d67f 100644
--- a/README.md
+++ b/README.md
@@ -16,10 +16,11 @@ Get Spotify tracks in true FLAC from Tidal, Qobuz & Amazon Music — no account
## Screenshots
-
-
-
-
+
+
+
+
+
## Other project
diff --git a/docs/Screenshot_20260101-210622_SpotiFLAC.png b/assets/images/Screenshot_20260101-210622_SpotiFLAC.png
similarity index 100%
rename from docs/Screenshot_20260101-210622_SpotiFLAC.png
rename to assets/images/Screenshot_20260101-210622_SpotiFLAC.png
diff --git a/docs/Screenshot_20260101-210626_SpotiFLAC.png b/assets/images/Screenshot_20260101-210626_SpotiFLAC.png
similarity index 100%
rename from docs/Screenshot_20260101-210626_SpotiFLAC.png
rename to assets/images/Screenshot_20260101-210626_SpotiFLAC.png
diff --git a/docs/Screenshot_20260101-210653_SpotiFLAC.png b/assets/images/Screenshot_20260101-210653_SpotiFLAC.png
similarity index 100%
rename from docs/Screenshot_20260101-210653_SpotiFLAC.png
rename to assets/images/Screenshot_20260101-210653_SpotiFLAC.png
diff --git a/assets/images/photo_2026-01-01_23-44-06.jpg b/assets/images/photo_2026-01-01_23-44-06.jpg
new file mode 100644
index 0000000..71a582f
Binary files /dev/null and b/assets/images/photo_2026-01-01_23-44-06.jpg differ
diff --git a/assets/images/photo_2026-01-01_23-56-11.jpg b/assets/images/photo_2026-01-01_23-56-11.jpg
new file mode 100644
index 0000000..cdefe05
Binary files /dev/null and b/assets/images/photo_2026-01-01_23-56-11.jpg differ
diff --git a/docs/Screenshot_20260101-210633_SpotiFLAC.png b/docs/Screenshot_20260101-210633_SpotiFLAC.png
deleted file mode 100644
index 7ac054d..0000000
Binary files a/docs/Screenshot_20260101-210633_SpotiFLAC.png and /dev/null differ
diff --git a/go_backend/exports.go b/go_backend/exports.go
index 916f06a..d5c4ec1 100644
--- a/go_backend/exports.go
+++ b/go_backend/exports.go
@@ -99,6 +99,7 @@ type DownloadRequest struct {
CoverURL string `json:"cover_url"`
OutputDir string `json:"output_dir"`
FilenameFormat string `json:"filename_format"`
+ Quality string `json:"quality"` // LOSSLESS, HI_RES, HI_RES_LOSSLESS
EmbedLyrics bool `json:"embed_lyrics"`
EmbedMaxQualityCover bool `json:"embed_max_quality_cover"`
TrackNumber int `json:"track_number"`
diff --git a/go_backend/qobuz.go b/go_backend/qobuz.go
index 1c6fea5..0034c44 100644
--- a/go_backend/qobuz.go
+++ b/go_backend/qobuz.go
@@ -346,8 +346,22 @@ func downloadFromQobuz(req DownloadRequest) (string, error) {
return "EXISTS:" + outputPath, nil
}
+ // Map quality from Tidal format to Qobuz format
+ // Tidal: LOSSLESS (16-bit), HI_RES (24-bit), HI_RES_LOSSLESS (24-bit hi-res)
+ // Qobuz: 5 (MP3 320), 6 (16-bit), 7 (24-bit 96kHz), 27 (24-bit 192kHz)
+ qobuzQuality := "27" // Default to highest quality
+ switch req.Quality {
+ case "LOSSLESS":
+ qobuzQuality = "6" // 16-bit FLAC
+ case "HI_RES":
+ qobuzQuality = "7" // 24-bit 96kHz
+ case "HI_RES_LOSSLESS":
+ qobuzQuality = "27" // 24-bit 192kHz
+ }
+ fmt.Printf("[Qobuz] Using quality: %s (mapped from %s)\n", qobuzQuality, req.Quality)
+
// Get download URL using parallel API requests
- downloadURL, err := downloader.GetDownloadURL(track.ID, "27") // 27 = FLAC 24-bit
+ downloadURL, err := downloader.GetDownloadURL(track.ID, qobuzQuality)
if err != nil {
return "", fmt.Errorf("failed to get download URL: %w", err)
}
diff --git a/go_backend/tidal.go b/go_backend/tidal.go
index 8551e4a..8d3df9f 100644
--- a/go_backend/tidal.go
+++ b/go_backend/tidal.go
@@ -859,8 +859,15 @@ func downloadFromTidal(req DownloadRequest) (string, error) {
return "EXISTS:" + outputPath, nil
}
+ // Determine quality to use (default to LOSSLESS if not specified)
+ quality := req.Quality
+ if quality == "" {
+ quality = "LOSSLESS"
+ }
+ fmt.Printf("[Tidal] Using quality: %s\n", quality)
+
// Get download URL using parallel API requests
- downloadURL, err := downloader.GetDownloadURL(track.ID, "LOSSLESS")
+ downloadURL, err := downloader.GetDownloadURL(track.ID, quality)
if err != nil {
return "", fmt.Errorf("failed to get download URL: %w", err)
}
diff --git a/lib/app.dart b/lib/app.dart
index fc8638e..654f75b 100644
--- a/lib/app.dart
+++ b/lib/app.dart
@@ -7,10 +7,11 @@ import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/theme/dynamic_color_wrapper.dart';
final _routerProvider = Provider((ref) {
- final settings = ref.watch(settingsProvider);
+ // Only watch isFirstLaunch to prevent router rebuild on other settings changes
+ final isFirstLaunch = ref.watch(settingsProvider.select((s) => s.isFirstLaunch));
return GoRouter(
- initialLocation: settings.isFirstLaunch ? '/setup' : '/',
+ initialLocation: isFirstLaunch ? '/setup' : '/',
routes: [
GoRoute(
path: '/',
diff --git a/lib/constants/app_info.dart b/lib/constants/app_info.dart
new file mode 100644
index 0000000..25f4723
--- /dev/null
+++ b/lib/constants/app_info.dart
@@ -0,0 +1,17 @@
+/// App version and info constants
+/// Update version here only - all other files will reference this
+class AppInfo {
+ static const String version = '1.2.0';
+ static const String buildNumber = '10';
+ static const String fullVersion = '$version+$buildNumber';
+
+ static const String appName = 'SpotiFLAC';
+ static const String copyright = '© 2026 SpotiFLAC';
+
+ static const String mobileAuthor = 'zarzet';
+ static const String originalAuthor = 'afkarxyz';
+
+ static const String githubRepo = 'zarzet/SpotiFLAC-Mobile';
+ static const String githubUrl = 'https://github.com/$githubRepo';
+ static const String originalGithubUrl = 'https://github.com/afkarxyz/SpotiFLAC';
+}
diff --git a/lib/models/settings.dart b/lib/models/settings.dart
index c91f221..af12030 100644
--- a/lib/models/settings.dart
+++ b/lib/models/settings.dart
@@ -13,6 +13,7 @@ class AppSettings {
final bool maxQualityCover;
final bool isFirstLaunch;
final int concurrentDownloads; // 1 = sequential (default), max 3
+ final bool checkForUpdates; // Check for updates on app start
const AppSettings({
this.defaultService = 'tidal',
@@ -24,6 +25,7 @@ class AppSettings {
this.maxQualityCover = true,
this.isFirstLaunch = true,
this.concurrentDownloads = 1, // Default: sequential (off)
+ this.checkForUpdates = true, // Default: enabled
});
AppSettings copyWith({
@@ -36,6 +38,7 @@ class AppSettings {
bool? maxQualityCover,
bool? isFirstLaunch,
int? concurrentDownloads,
+ bool? checkForUpdates,
}) {
return AppSettings(
defaultService: defaultService ?? this.defaultService,
@@ -47,6 +50,7 @@ class AppSettings {
maxQualityCover: maxQualityCover ?? this.maxQualityCover,
isFirstLaunch: isFirstLaunch ?? this.isFirstLaunch,
concurrentDownloads: concurrentDownloads ?? this.concurrentDownloads,
+ checkForUpdates: checkForUpdates ?? this.checkForUpdates,
);
}
diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart
index 57a53eb..564baae 100644
--- a/lib/models/settings.g.dart
+++ b/lib/models/settings.g.dart
@@ -16,6 +16,7 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings(
maxQualityCover: json['maxQualityCover'] as bool? ?? true,
isFirstLaunch: json['isFirstLaunch'] as bool? ?? true,
concurrentDownloads: (json['concurrentDownloads'] as num?)?.toInt() ?? 1,
+ checkForUpdates: json['checkForUpdates'] as bool? ?? true,
);
Map _$AppSettingsToJson(AppSettings instance) =>
@@ -29,4 +30,5 @@ Map _$AppSettingsToJson(AppSettings instance) =>
'maxQualityCover': instance.maxQualityCover,
'isFirstLaunch': instance.isFirstLaunch,
'concurrentDownloads': instance.concurrentDownloads,
+ 'checkForUpdates': instance.checkForUpdates,
};
diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart
index 3dcc963..5cefccb 100644
--- a/lib/providers/download_queue_provider.dart
+++ b/lib/providers/download_queue_provider.dart
@@ -9,6 +9,7 @@ import 'package:ffmpeg_kit_flutter_new_audio/return_code.dart';
import 'package:spotiflac_android/models/download_item.dart';
import 'package:spotiflac_android/models/settings.dart';
import 'package:spotiflac_android/models/track.dart';
+import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/services/ffmpeg_service.dart';
@@ -18,20 +19,37 @@ class DownloadHistoryItem {
final String trackName;
final String artistName;
final String albumName;
+ final String? albumArtist;
final String? coverUrl;
final String filePath;
final String service;
final DateTime downloadedAt;
+ // Additional metadata
+ final String? isrc;
+ final String? spotifyId;
+ final int? trackNumber;
+ final int? discNumber;
+ final int? duration;
+ final String? releaseDate;
+ final String? quality;
const DownloadHistoryItem({
required this.id,
required this.trackName,
required this.artistName,
required this.albumName,
+ this.albumArtist,
this.coverUrl,
required this.filePath,
required this.service,
required this.downloadedAt,
+ this.isrc,
+ this.spotifyId,
+ this.trackNumber,
+ this.discNumber,
+ this.duration,
+ this.releaseDate,
+ this.quality,
});
Map toJson() => {
@@ -39,10 +57,18 @@ class DownloadHistoryItem {
'trackName': trackName,
'artistName': artistName,
'albumName': albumName,
+ 'albumArtist': albumArtist,
'coverUrl': coverUrl,
'filePath': filePath,
'service': service,
'downloadedAt': downloadedAt.toIso8601String(),
+ 'isrc': isrc,
+ 'spotifyId': spotifyId,
+ 'trackNumber': trackNumber,
+ 'discNumber': discNumber,
+ 'duration': duration,
+ 'releaseDate': releaseDate,
+ 'quality': quality,
};
factory DownloadHistoryItem.fromJson(Map json) => DownloadHistoryItem(
@@ -50,10 +76,18 @@ class DownloadHistoryItem {
trackName: json['trackName'] as String,
artistName: json['artistName'] as String,
albumName: json['albumName'] as String,
+ albumArtist: json['albumArtist'] as String?,
coverUrl: json['coverUrl'] as String?,
filePath: json['filePath'] as String,
service: json['service'] as String,
downloadedAt: DateTime.parse(json['downloadedAt'] as String),
+ isrc: json['isrc'] as String?,
+ spotifyId: json['spotifyId'] as String?,
+ trackNumber: json['trackNumber'] as int?,
+ discNumber: json['discNumber'] as int?,
+ duration: json['duration'] as int?,
+ releaseDate: json['releaseDate'] as String?,
+ quality: json['quality'] as String?,
);
}
@@ -151,6 +185,7 @@ class DownloadQueueState {
final bool isProcessing;
final String outputDir;
final String filenameFormat;
+ final String audioQuality; // LOSSLESS, HI_RES, HI_RES_LOSSLESS
final bool autoFallback;
final int concurrentDownloads; // 1 = sequential, max 3
@@ -160,6 +195,7 @@ class DownloadQueueState {
this.isProcessing = false,
this.outputDir = '',
this.filenameFormat = '{artist} - {title}',
+ this.audioQuality = 'LOSSLESS',
this.autoFallback = true,
this.concurrentDownloads = 1,
});
@@ -170,6 +206,7 @@ class DownloadQueueState {
bool? isProcessing,
String? outputDir,
String? filenameFormat,
+ String? audioQuality,
bool? autoFallback,
int? concurrentDownloads,
}) {
@@ -179,6 +216,7 @@ class DownloadQueueState {
isProcessing: isProcessing ?? this.isProcessing,
outputDir: outputDir ?? this.outputDir,
filenameFormat: filenameFormat ?? this.filenameFormat,
+ audioQuality: audioQuality ?? this.audioQuality,
autoFallback: autoFallback ?? this.autoFallback,
concurrentDownloads: concurrentDownloads ?? this.concurrentDownloads,
);
@@ -284,12 +322,17 @@ class DownloadQueueNotifier extends Notifier {
state = state.copyWith(
outputDir: settings.downloadDirectory.isNotEmpty ? settings.downloadDirectory : state.outputDir,
filenameFormat: settings.filenameFormat,
+ audioQuality: settings.audioQuality,
autoFallback: settings.autoFallback,
concurrentDownloads: settings.concurrentDownloads,
);
}
String addToQueue(Track track, String service) {
+ // Sync settings before adding to queue
+ final settings = ref.read(settingsProvider);
+ updateSettings(settings);
+
final id = '${track.isrc ?? track.id}-${DateTime.now().millisecondsSinceEpoch}';
final item = DownloadItem(
id: id,
@@ -309,6 +352,10 @@ class DownloadQueueNotifier extends Notifier {
}
void addMultipleToQueue(List