merge: main into dev - resolved conflicts for mime utils, version, gitignore

This commit is contained in:
zarzet
2026-01-16 03:47:45 +07:00
12 changed files with 1019 additions and 454 deletions
+4 -1
View File
@@ -56,6 +56,9 @@ android/app/libs/gobackend-sources.jar
# Extension folder
extension/
# Agent instructions
AGENTS.md
# Temp/misc
nul
/extension
+30
View File
@@ -80,6 +80,13 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int
- One-tap install, update, and uninstall
- Offline cache for browsing without internet
#### Spotify Web Extension
- Available in Extension Store - install and enable in Settings > Extensions
- Metadata provider using Spotify's internal web player API
- Download tracks from Daily Mix, Discover Weekly, and other personalized playlists
- Useful when official Spotify API is rate-limited or unavailable
#### Extension Capabilities
- **Custom Search Providers**
@@ -128,8 +135,31 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int
- Qobuz: Both APIs requested simultaneously, first success wins
- Significantly reduces download URL fetch time
### UI/UX Improvements
- **Swipeable History Filters**: History tab now supports swipe gestures between All, Albums, and Singles filters
- Swipe left/right to switch between filter tabs
- Filter chips sync with swipe position
- Smooth edge-to-edge transition: swipe past Singles to go to Store, swipe past All to go to Home
- Natural gesture feel - drag connects to parent navigation
- **Improved File Open Intent**: Play button in History now correctly opens music players only
- Added proper MIME type (`audio/flac`, `audio/mpeg`, etc.) when opening downloaded files
- Prevents system from showing unrelated apps in the "Open with" dialog
### Fixed
- **Fixed Tab Edge Overscroll**: Home and Settings tabs now stop at edges instead of bouncing into empty space
- **Fixed Extension Duplicate Load Error**: Extension loading now silently skips already-loaded extensions instead of throwing error
- **Fixed Settings Item Highlight on Swipe**: Settings items no longer highlight when swiping at page edge
- **Fixed Keyboard Appearing on Tab Switch**: Keyboard now auto-dismisses when swiping between tabs
- **Removed Search Source Badges**: Removed "Free" and "API Key" labels from Deezer/Spotify selector in Options
- **Back Gesture Freeze on Android 13+**: Fixed app freeze when using back gesture in settings
- Added `PopScope` with `canPop: true` to all settings pages
+33 -34
View File
@@ -1,5 +1,5 @@
[![GitHub All Releases](https://img.shields.io/github/downloads/zarzet/SpotiFLAC-Mobile/total?style=for-the-badge)](https://github.com/zarzet/SpotiFLAC-Mobile/releases)
[![VirusTotal](https://img.shields.io/badge/VirusTotal-Safe-brightgreen?style=for-the-badge&logo=virustotal)](https://www.virustotal.com/gui/file/09c6260e9ebaf2ff0d15f30deda939642f41887f11aad602ac697cb37fa0308c/)
[![VirusTotal](https://img.shields.io/badge/VirusTotal-Safe-brightgreen?style=for-the-badge&logo=virustotal)](https://www.virustotal.com/gui/file/e1c527eacb6f5ce527af214a75aab8da060c2afc629825fff24af858439e7e6b)
<div align="center">
@@ -23,59 +23,58 @@ Get Spotify tracks in true FLAC from Tidal, Qobuz & Amazon Music — no account
<img src="assets/images/4.jpg?v=2" width="200" />
</p>
## Metadata Source
## Search Source
SpotiFLAC supports two metadata sources for searching tracks:
SpotiFLAC supports two search sources:
| Source | Pros | Cons |
|--------|------|------|
| **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 |
| Source | Setup |
|--------|-------|
| **Deezer** (Default) | No setup required |
| **Spotify** | Install **Spotify Web** extension from the Store, or use your own [Spotify Developer](https://developer.spotify.com) Client ID & Secret in Settings |
### 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)
2. Create an app to get your Client ID and Client Secret
3. Go to **Settings > Options > Spotify API > Change from Deezer to Spotify > Input Custom Credentials**
4. Enter your Client ID and Secret
5. Change **Search Source** to Spotify
## Extensions
## Extensions (Alpha)
> **Alpha Feature**: Extensions are now available in alpha. Some features may be unstable or change in future releases.
SpotiFLAC supports extensions to add custom metadata and download providers. Extensions are written in JavaScript and run in a secure sandbox.
### Features
- **Metadata Providers**: Add new sources for track/album/artist search
- **Download Providers**: Add new sources for audio downloads
- **Custom Settings**: Extensions can have user-configurable settings
- **Provider Priority**: Set the order in which providers are tried
Extensions allow the community to add new music sources and features without waiting for app updates. When a streaming service API changes or a new source becomes available, extensions can be updated independently.
### Installing Extensions
1. Download a `.spotiflac-ext` file
2. Go to **Settings > Extensions**
3. Tap **Install Extension** and select the file
1. Go to **Store** tab in the app
2. Browse and install extensions with one tap
3. Or download a `.spotiflac-ext` file and install manually via **Settings > Extensions**
4. Configure extension settings if needed
5. Set provider priority in **Settings > Extensions > Provider Priority**
### Developing Extensions
Want to create your own extension? Check out the [Extension Development Guide](docs/EXTENSION_DEVELOPMENT.md) for complete documentation.
### Example Extensions
Sample extensions are available in the [docs/extensions_example](docs/extensions_example) folder:
Want to create your own extension? Check out the [Extension Development Guide](https://zarz.moe/docs) for complete documentation.
## Other project
### [SpotiFLAC (Desktop)](https://github.com/afkarxyz/SpotiFLAC)
Get Spotify tracks in true FLAC from Tidal, Qobuz & Amazon Music for Windows, macOS & Linux
## FAQ
**Q: Why is my download failing with "Song not found"?**
A: The track may not be available on Tidal, Qobuz, or Amazon Music. Try enabling more download services in Settings > Download > Provider Priority, or install additional extensions from the Store.
**Q: Why are some tracks downloading in lower quality?**
A: Quality depends on what's available from the streaming service. Tidal offers up to 24-bit/192kHz, Qobuz up to 24-bit/192kHz, and Amazon up to 24-bit/48kHz. The app automatically selects the best available quality.
**Q: Can I download my Spotify playlists?**
A: Yes! Just paste the Spotify playlist URL in the search bar. The app will fetch all tracks and queue them for download.
**Q: Why do I need to grant storage permission?**
A: The app needs permission to save downloaded files to your device. On Android 13+, you may need to grant "All files access" in Settings > Apps > SpotiFLAC > Permissions.
**Q: How do I download Daily Mix or Discover Weekly?**
A: Install the **Spotify Web** extension from the Store. This extension can access personalized playlists that aren't available through the public API.
**Q: Is this app safe?**
A: Yes, the app is open source and you can verify the code yourself. Each release is scanned with VirusTotal (see badge at top of README).
[![Ko-fi](https://img.shields.io/badge/Ko--fi-Support%20Me-FF5E5B?style=for-the-badge&logo=ko-fi&logoColor=white)](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!
This project is for **educational and private use only**. The developer does not condone or encourage copyright infringement.
**SpotiFLAC** is a third-party tool and is not affiliated with, endorsed by, or connected to Spotify, Tidal, Qobuz, Amazon Music, or any other streaming service.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 84 KiB

+4 -3
View File
@@ -456,9 +456,10 @@ func (m *ExtensionManager) loadExtensionFromDirectory(dirPath string) (*LoadedEx
return nil, fmt.Errorf("Extension is missing index.js file")
}
// Check if extension already loaded - skip if already exists (for directory loading on startup)
if _, exists := m.extensions[manifest.Name]; exists {
return nil, fmt.Errorf("Extension '%s' is already loaded", manifest.DisplayName)
// Check if extension already loaded - skip silently (for directory loading on startup)
if existing, exists := m.extensions[manifest.Name]; exists {
GoLog("[Extension] Extension '%s' already loaded, skipping\n", manifest.DisplayName)
return existing, nil
}
// Create data directory for extension
+41
View File
@@ -547,6 +547,47 @@ import Gobackend // Import Go framework
if let error = error { throw error }
return response
// Extension Store
case "initExtensionStore":
let args = call.arguments as! [String: Any]
let cacheDir = args["cache_dir"] as! String
GobackendInitExtensionStoreJSON(cacheDir, &error)
if let error = error { throw error }
return nil
case "getStoreExtensions":
let args = call.arguments as! [String: Any]
let forceRefresh = args["force_refresh"] as? Bool ?? false
let response = GobackendGetStoreExtensionsJSON(forceRefresh, &error)
if let error = error { throw error }
return response
case "searchStoreExtensions":
let args = call.arguments as! [String: Any]
let query = args["query"] as? String ?? ""
let category = args["category"] as? String ?? ""
let response = GobackendSearchStoreExtensionsJSON(query, category, &error)
if let error = error { throw error }
return response
case "getStoreCategories":
let response = GobackendGetStoreCategoriesJSON(&error)
if let error = error { throw error }
return response
case "downloadStoreExtension":
let args = call.arguments as! [String: Any]
let extensionId = args["extension_id"] as! String
let destDir = args["dest_dir"] as! String
let response = GobackendDownloadStoreExtensionJSON(extensionId, destDir, &error)
if let error = error { throw error }
return response
case "clearStoreCache":
GobackendClearStoreCacheJSON(&error)
if let error = error { throw error }
return nil
default:
throw NSError(
domain: "SpotiFLAC",
+8 -2
View File
@@ -122,6 +122,8 @@ class _MainShellState extends ConsumerState<MainShell> {
void _onPageChanged(int index) {
if (_currentIndex != index) {
setState(() => _currentIndex = index);
// Unfocus any text field when switching tabs to prevent keyboard from appearing
FocusScope.of(context).unfocus();
}
}
@@ -190,7 +192,11 @@ class _MainShellState extends ConsumerState<MainShell> {
// Build tabs and destinations based on settings
final tabs = <Widget>[
const HomeTab(),
const QueueTab(),
QueueTab(
parentPageController: _pageController,
parentPageIndex: 1,
nextPageIndex: showStore ? 2 : 3,
),
if (showStore) const StoreTab(),
const SettingsTab(),
];
@@ -254,7 +260,7 @@ class _MainShellState extends ConsumerState<MainShell> {
body: PageView(
controller: _pageController,
onPageChanged: _onPageChanged,
physics: const BouncingScrollPhysics(),
physics: const ClampingScrollPhysics(),
children: tabs,
),
bottomNavigationBar: NavigationBar(
+897 -412
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -72,7 +72,7 @@ class SettingsItem extends StatelessWidget {
InkWell(
onTap: onTap,
splashColor: colorScheme.primary.withValues(alpha: 0.12),
highlightColor: colorScheme.primary.withValues(alpha: 0.08),
highlightColor: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: Row(
@@ -159,7 +159,7 @@ class SettingsSwitchItem extends StatelessWidget {
child: InkWell(
onTap: isDisabled ? null : () => onChanged!(!value),
splashColor: colorScheme.primary.withValues(alpha: 0.12),
highlightColor: colorScheme.primary.withValues(alpha: 0.08),
highlightColor: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
child: Row(