merge: main into dev - resolved conflicts for mime utils, version, gitignore
@@ -56,6 +56,9 @@ android/app/libs/gobackend-sources.jar
|
||||
|
||||
# Extension folder
|
||||
extension/
|
||||
|
||||
# Agent instructions
|
||||
AGENTS.md
|
||||
|
||||
# Temp/misc
|
||||
nul
|
||||
/extension
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[](https://github.com/zarzet/SpotiFLAC-Mobile/releases)
|
||||
[](https://www.virustotal.com/gui/file/09c6260e9ebaf2ff0d15f30deda939642f41887f11aad602ac697cb37fa0308c/)
|
||||
[](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).
|
||||
|
||||
[](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.
|
||||
|
||||
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 113 KiB |
|
Before Width: | Height: | Size: 278 KiB After Width: | Height: | Size: 259 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 135 KiB After Width: | Height: | Size: 84 KiB |
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||