mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-24 16:54:03 +02:00
Merge dev into main
This commit is contained in:
@@ -1 +0,0 @@
|
||||
# Add directories or file patterns to ignore during indexing (e.g. foo/ or *.csv)
|
||||
@@ -6,6 +6,8 @@ Thumbs.db
|
||||
.idea/
|
||||
.vscode/
|
||||
*.iml
|
||||
.cursorignore
|
||||
.cursorrules
|
||||
|
||||
# Kiro specs (development only)
|
||||
.kiro/
|
||||
|
||||
+64
-14
@@ -1,6 +1,56 @@
|
||||
# Changelog
|
||||
|
||||
## [3.1.0] - 2026-01-19
|
||||
## [3.1.1] - 2026-01-17
|
||||
|
||||
### Added
|
||||
|
||||
- **Lyrics Caching**: Lyrics are now cached for 24 hours to reduce API calls and improve performance
|
||||
- Thread-safe cache with automatic expiration
|
||||
- Cache key based on artist, track, and duration
|
||||
- Log indicator shows "(cached)" when lyrics are served from cache
|
||||
|
||||
- **Lyrics Duration Matching**: Improved lyrics accuracy with duration-based matching
|
||||
- Compares track duration with lrclib.net results
|
||||
- 10-second tolerance to handle version differences (radio edit, remaster, etc.)
|
||||
- Prioritizes synced lyrics over plain text when duration matches
|
||||
- Falls back gracefully if no duration match found
|
||||
|
||||
- **Deezer Cover Art Upgrade**: Cover art from Deezer CDN now automatically upgraded to maximum quality
|
||||
- Detects Deezer CDN URLs (`cdn-images.dzcdn.net`)
|
||||
- Upgrades cover resolution to 1800x1800 (max available)
|
||||
- Works alongside existing cover upgrade
|
||||
|
||||
- **Live Search for Extensions**: Search-as-you-type functionality for extension search
|
||||
- 800ms debounce delay to prevent excessive API calls
|
||||
- Minimum 3 characters required before searching
|
||||
- Concurrency control to prevent race conditions in extension runtime
|
||||
- Queues pending searches if a search is already in progress
|
||||
|
||||
- **Russian Language Support**: Added Russian (Русский) translation - 99% complete
|
||||
- Translated via Crowdin community contributions
|
||||
- Covers all UI elements, settings, and error messages
|
||||
|
||||
### Fixed
|
||||
|
||||
- **ISRC Index Race Condition**: Fixed repeated index rebuilding during parallel downloads
|
||||
- Added per-directory build lock using `sync.Map` and `sync.Mutex`
|
||||
- Double-check locking pattern ensures index is built only once
|
||||
- Significantly improves performance during CSV import with many tracks
|
||||
|
||||
- **Queue Tab Scroll Exception**: Fixed Flutter rendering exception with NestedScrollView
|
||||
- Disabled Material 3 stretch overscroll indicator that caused `_StretchController` assertion
|
||||
- Wrapped NestedScrollView with ScrollConfiguration to prevent `setState during build` errors
|
||||
- Issue was especially noticeable during rapid queue updates (CSV import)
|
||||
|
||||
- **CSV Import**: Fixed CSV export not being parsed correctly
|
||||
- Added support for `Artist Name(s)` header (with parentheses)
|
||||
- Added support for `Track URI` header for track IDs
|
||||
- Added `artists` and `track_id` as alternative header names
|
||||
- Now correctly parses "Liked Songs" and playlist exports
|
||||
|
||||
---
|
||||
|
||||
## [3.1.0] - 2026-01-16
|
||||
|
||||
### Added
|
||||
|
||||
@@ -105,17 +155,17 @@
|
||||
- YT Music extension `getArtist()` now returns `top_tracks` array with up to 10 popular songs
|
||||
- Go backend `GetArtistWithExtensionJSON` now forwards `top_tracks`, `header_image`, and `listeners` to Flutter
|
||||
- `ExtensionArtistScreen` now parses and passes top tracks to `ArtistScreen`
|
||||
- `ArtistScreen` with `extensionId` skips Spotify/Deezer fetch, uses extension data only (fixes "Rate Limited" errors)
|
||||
- `ArtistScreen` with `extensionId` skips metadata fetch, uses extension data only (fixes "Rate Limited" errors)
|
||||
- **Search Bar Unfocus**: Fixed search bar not unfocusing when tapping outside - now properly dismisses keyboard and unfocus when tapping anywhere outside the search field
|
||||
- **Keyboard Appearing on Settings Navigation**: Fixed keyboard randomly appearing when returning from Settings sub-pages (e.g., Appearance) - now uses `FocusManager.instance.primaryFocus?.unfocus()` for more aggressive unfocus
|
||||
- **Recent Access Artist Navigation**: Fixed opening artist from recent access using wrong screen - now correctly uses `ExtensionArtistScreen` for extension artists (YT Music, Spotify Web) instead of trying to fetch from Spotify API
|
||||
- **Recent Access Artist Navigation**: Fixed opening artist from recent access using wrong screen - now correctly uses `ExtensionArtistScreen` for extension artists (YT Music, etc.) instead of trying to fetch from API
|
||||
|
||||
### Extensions
|
||||
|
||||
- **YouTube Music Extension**: Updated to v1.5.0
|
||||
- `getArtist()` now returns `top_tracks` array with popular songs
|
||||
- Added `header_image` and `listeners` to artist response
|
||||
- **Spotify Web Extension**: Updated to v1.6.0
|
||||
- **Web Extension**: Updated to v1.6.0
|
||||
|
||||
### Localization
|
||||
|
||||
@@ -148,12 +198,12 @@ 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
|
||||
#### Web Extension
|
||||
|
||||
- Available in Extension Store - install and enable in Settings > Extensions
|
||||
- Metadata provider using Spotify's internal web player API
|
||||
- Metadata provider using web player API
|
||||
- Download tracks from Daily Mix, Discover Weekly, and other personalized playlists
|
||||
- Useful when official Spotify API is rate-limited or unavailable
|
||||
- Useful when official API is rate-limited or unavailable
|
||||
|
||||
#### Extension Capabilities
|
||||
|
||||
@@ -188,7 +238,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int
|
||||
|
||||
- **Separate Singles Folder**: Organize downloads into Albums/ and Singles/ folders
|
||||
|
||||
- Based on `album_type` from Spotify/Deezer metadata
|
||||
- Based on `album_type` from metadata
|
||||
- Toggle in Settings > Download > Separate Singles Folder
|
||||
|
||||
- **Year in Album Folder Name**: New album folder structure options with release year
|
||||
@@ -226,7 +276,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int
|
||||
|
||||
- **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
|
||||
- **Removed Search Source Badges**: Removed "Free" and "API Key" labels from provider selector in Options
|
||||
|
||||
- **Back Gesture Freeze on Android 13+**: Fixed app freeze when using back gesture in settings
|
||||
|
||||
@@ -261,7 +311,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int
|
||||
|
||||
- **Duplicate History Entries**: Fixed duplicate entries when re-downloading same track
|
||||
|
||||
- Detects existing entries by Spotify ID, Deezer ID, or ISRC
|
||||
- Detects existing entries by track ID, Deezer ID, or ISRC
|
||||
|
||||
- **Permission Error Message**: Fixed download showing "Song not found" when actually permission error
|
||||
|
||||
@@ -330,7 +380,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int
|
||||
- **Extension Disabled Search Fallback**: Fixed error when extension is disabled but still called
|
||||
|
||||
- `_performSearch` now checks if extension is still enabled before calling custom search
|
||||
- Automatically falls back to Deezer/Spotify search if extension was disabled
|
||||
- Automatically falls back to Deezer search if extension was disabled
|
||||
- Clears `searchProvider` setting if extension no longer available
|
||||
|
||||
- **Store Tab Unmount Crash**: Fixed "Using ref when widget is unmounted" error
|
||||
@@ -450,7 +500,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int
|
||||
|
||||
### Extensions
|
||||
|
||||
- **Spotify Web Extension** (example): New extension for Spotify metadata via web API
|
||||
- **Web Extension** (example): New extension for metadata via web API
|
||||
- Supports personalized playlists (Daily Mix, Discover Weekly, Release Radar, etc.)
|
||||
- Search, album, playlist, track, and artist fetching
|
||||
- Available in Extension Store (3.0.0-alpha.4)
|
||||
@@ -462,7 +512,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int
|
||||
### Added
|
||||
|
||||
- **Separate Singles Folder**: Option to organize downloads into Albums/ and Singles/ folders
|
||||
- Based on `album_type` from Spotify/Deezer metadata
|
||||
- Based on `album_type` from metadata
|
||||
- Toggle in Settings > Download > Separate Singles Folder
|
||||
- Singles saved to `{output}/Singles/`, albums to `{output}/Albums/`
|
||||
- **Browser-like Polyfills**: New global APIs for easier library porting
|
||||
@@ -482,7 +532,7 @@ SpotiFLAC 3.0 introduces a powerful extension system that allows third-party int
|
||||
### Fixed
|
||||
|
||||
- **Duplicate History Entries**: Fixed duplicate entries when re-downloading same track
|
||||
- Detects existing entries by Spotify ID, Deezer ID, or ISRC
|
||||
- Detects existing entries by track ID, Deezer ID, or ISRC
|
||||
- Replaces existing entry and moves to top of list
|
||||
- Auto-deduplicates existing history on app load
|
||||
- **Extension Search Fallback**: Fixed error when extension is disabled but still called for search
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<img src="icon.png" width="128" />
|
||||
|
||||
Get Spotify tracks in true FLAC from Tidal, Qobuz & Amazon Music — no account required.
|
||||
Download music in true lossless FLAC from Tidal, Qobuz & Amazon Music — no account required.
|
||||
|
||||

|
||||

|
||||
@@ -26,12 +26,12 @@ Get Spotify tracks in true FLAC from Tidal, Qobuz & Amazon Music — no account
|
||||
|
||||
## Search Source
|
||||
|
||||
SpotiFLAC supports two search sources:
|
||||
SpotiFLAC supports multiple search sources for finding music metadata:
|
||||
|
||||
| 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 |
|
||||
| **Extensions** | Install additional search providers from the Store |
|
||||
|
||||
## Extensions
|
||||
|
||||
@@ -50,7 +50,7 @@ Want to create your own extension? Check out the [Extension Development Guide](h
|
||||
## Other project
|
||||
|
||||
### [SpotiFLAC (Desktop)](https://github.com/afkarxyz/SpotiFLAC)
|
||||
Get Spotify tracks in true FLAC from Tidal, Qobuz & Amazon Music for Windows, macOS & Linux
|
||||
Download music in true lossless FLAC from Tidal, Qobuz & Amazon Music for Windows, macOS & Linux
|
||||
|
||||
## FAQ
|
||||
|
||||
@@ -60,15 +60,12 @@ A: The track may not be available on Tidal, Qobuz, or Amazon Music. Try enabling
|
||||
**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: Can I download playlists?**
|
||||
A: Yes! Just paste the 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: Why is the mobile app so large (~50MB) compared to the PC version (~3MB)?**
|
||||
A: The mobile app includes FFmpeg libraries for audio processing and format conversion, which adds significant size. The PC version relies on system-installed FFmpeg, keeping the download smaller. We bundle FFmpeg to ensure compatibility across all Android devices without requiring users to install additional software.
|
||||
|
||||
@@ -81,7 +78,9 @@ A: Yes, the app is open source and you can verify the code yourself. Each releas
|
||||
|
||||
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.
|
||||
**SpotiFLAC** is a third-party tool and is not affiliated with, endorsed by, or connected to Tidal, Qobuz, Amazon Music, Deezer, or any other streaming service.
|
||||
|
||||
The application is purely a user interface that facilitates communication between your device and existing third-party services.
|
||||
|
||||
You are solely responsible for:
|
||||
1. Ensuring your use of this software complies with your local laws.
|
||||
|
||||
@@ -158,8 +158,9 @@ class MainActivity: FlutterActivity() {
|
||||
val spotifyId = call.argument<String>("spotify_id") ?: ""
|
||||
val trackName = call.argument<String>("track_name") ?: ""
|
||||
val artistName = call.argument<String>("artist_name") ?: ""
|
||||
val durationMs = call.argument<Int>("duration_ms")?.toLong() ?: 0L
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.fetchLyrics(spotifyId, trackName, artistName)
|
||||
Gobackend.fetchLyrics(spotifyId, trackName, artistName, durationMs)
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
@@ -168,8 +169,9 @@ class MainActivity: FlutterActivity() {
|
||||
val trackName = call.argument<String>("track_name") ?: ""
|
||||
val artistName = call.argument<String>("artist_name") ?: ""
|
||||
val filePath = call.argument<String>("file_path") ?: ""
|
||||
val durationMs = call.argument<Int>("duration_ms")?.toLong() ?: 0L
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.getLyricsLRC(spotifyId, trackName, artistName, filePath)
|
||||
Gobackend.getLyricsLRC(spotifyId, trackName, artistName, filePath, durationMs)
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
|
||||
+17
-1
@@ -1,3 +1,19 @@
|
||||
files:
|
||||
- source: /lib/l10n/arb/app_en.arb
|
||||
translation: /lib/l10n/arb/app_%locale_with_underscore%.arb
|
||||
translation: /lib/l10n/arb/app_%locale%.arb
|
||||
languages_mapping:
|
||||
locale:
|
||||
# Short codes for single-variant languages
|
||||
de: de
|
||||
es: es
|
||||
fr: fr
|
||||
hi: hi
|
||||
id: id
|
||||
ja: ja
|
||||
ko: ko
|
||||
nl: nl
|
||||
pt: pt
|
||||
ru: ru
|
||||
# Full codes for Chinese variants
|
||||
zh-CN: zh_CN
|
||||
zh-TW: zh_TW
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package gobackend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -512,6 +512,7 @@ func downloadFromAmazon(req DownloadRequest) (AmazonDownloadResult, error) {
|
||||
req.TrackName,
|
||||
req.ArtistName,
|
||||
req.EmbedLyrics,
|
||||
int64(req.DurationMS),
|
||||
)
|
||||
}()
|
||||
|
||||
|
||||
+32
-7
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -14,6 +15,9 @@ const (
|
||||
spotifySizeMax = "ab67616d000082c1" // Max resolution (~2000x2000)
|
||||
)
|
||||
|
||||
// Deezer CDN supports these sizes: 56, 250, 500, 1000, 1400, 1800
|
||||
var deezerSizeRegex = regexp.MustCompile(`/(\d+)x(\d+)-\d+-\d+-\d+-\d+\.jpg$`)
|
||||
|
||||
// convertSmallToMedium upgrades 300x300 cover URL to 640x640
|
||||
// Same logic as PC version for consistency
|
||||
func convertSmallToMedium(imageURL string) string {
|
||||
@@ -41,9 +45,10 @@ func downloadCoverToMemory(coverURL string, maxQuality bool) ([]byte, error) {
|
||||
maxURL := upgradeToMaxQuality(downloadURL)
|
||||
if maxURL != downloadURL {
|
||||
downloadURL = maxURL
|
||||
GoLog("[Cover] Upgraded to max resolution (~2000x2000)")
|
||||
} else {
|
||||
GoLog("[Cover] Max resolution not available, using 640x640")
|
||||
// Log already printed by upgradeToMaxQuality for Deezer
|
||||
if strings.Contains(coverURL, "scdn.co") || strings.Contains(coverURL, "spotifycdn") {
|
||||
GoLog("[Cover] Spotify: upgraded to max resolution (~2000x2000)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,18 +90,38 @@ func downloadCoverToMemory(coverURL string, maxQuality bool) ([]byte, error) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// upgradeToMaxQuality upgrades Spotify cover URL to maximum quality
|
||||
// Same logic as PC version - directly replaces 640x640 size code with max resolution
|
||||
// No HEAD verification needed - Spotify CDN always serves max resolution if available
|
||||
// upgradeToMaxQuality upgrades cover URL to maximum quality
|
||||
// Supports both Spotify and Deezer CDNs
|
||||
func upgradeToMaxQuality(coverURL string) string {
|
||||
|
||||
// Spotify CDN upgrade
|
||||
if strings.Contains(coverURL, spotifySize640) {
|
||||
return strings.Replace(coverURL, spotifySize640, spotifySizeMax, 1)
|
||||
}
|
||||
|
||||
// Deezer CDN upgrade
|
||||
if strings.Contains(coverURL, "cdn-images.dzcdn.net") {
|
||||
return upgradeDeezerCover(coverURL)
|
||||
}
|
||||
|
||||
return coverURL
|
||||
}
|
||||
|
||||
// upgradeDeezerCover upgrades Deezer cover URL to maximum quality (1800x1800)
|
||||
// Deezer CDN format: https://cdn-images.dzcdn.net/images/cover/{hash}/{size}x{size}-000000-80-0-0.jpg
|
||||
// Available sizes: 56, 250, 500, 1000, 1400, 1800
|
||||
func upgradeDeezerCover(coverURL string) string {
|
||||
if !strings.Contains(coverURL, "cdn-images.dzcdn.net") {
|
||||
return coverURL
|
||||
}
|
||||
|
||||
// Replace any size pattern with 1800x1800
|
||||
upgraded := deezerSizeRegex.ReplaceAllString(coverURL, "/1800x1800-000000-80-0-0.jpg")
|
||||
if upgraded != coverURL {
|
||||
GoLog("[Cover] Deezer: upgraded to 1800x1800")
|
||||
}
|
||||
return upgraded
|
||||
}
|
||||
|
||||
// GetCoverFromSpotify gets cover URL from Spotify metadata
|
||||
func GetCoverFromSpotify(imageURL string, maxQuality bool) string {
|
||||
if imageURL == "" {
|
||||
|
||||
@@ -21,11 +21,14 @@ type ISRCIndex struct {
|
||||
var (
|
||||
isrcIndexCache = make(map[string]*ISRCIndex)
|
||||
isrcIndexCacheMu sync.RWMutex
|
||||
isrcBuildingMu sync.Map // Per-directory build lock to prevent concurrent builds
|
||||
isrcIndexTTL = 5 * time.Minute
|
||||
)
|
||||
|
||||
// GetISRCIndex returns or builds an ISRC index for the given directory
|
||||
// Uses per-directory mutex to prevent concurrent builds (race condition fix)
|
||||
func GetISRCIndex(outputDir string) *ISRCIndex {
|
||||
// Fast path: check cache first
|
||||
isrcIndexCacheMu.RLock()
|
||||
idx, exists := isrcIndexCache[outputDir]
|
||||
isrcIndexCacheMu.RUnlock()
|
||||
@@ -34,6 +37,22 @@ func GetISRCIndex(outputDir string) *ISRCIndex {
|
||||
return idx
|
||||
}
|
||||
|
||||
// Slow path: need to build index
|
||||
// Use per-directory mutex to prevent multiple goroutines from building simultaneously
|
||||
buildLock, _ := isrcBuildingMu.LoadOrStore(outputDir, &sync.Mutex{})
|
||||
mu := buildLock.(*sync.Mutex)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
// Double-check cache after acquiring lock (another goroutine may have built it)
|
||||
isrcIndexCacheMu.RLock()
|
||||
idx, exists = isrcIndexCache[outputDir]
|
||||
isrcIndexCacheMu.RUnlock()
|
||||
|
||||
if exists && time.Since(idx.buildTime) < isrcIndexTTL {
|
||||
return idx
|
||||
}
|
||||
|
||||
return buildISRCIndex(outputDir)
|
||||
}
|
||||
|
||||
|
||||
@@ -649,9 +649,11 @@ func SanitizeFilename(filename string) string {
|
||||
|
||||
// FetchLyrics fetches lyrics for a track from LRCLIB
|
||||
// Returns JSON with lyrics data
|
||||
func FetchLyrics(spotifyID, trackName, artistName string) (string, error) {
|
||||
// durationMs: track duration in milliseconds for matching, use 0 to skip duration matching
|
||||
func FetchLyrics(spotifyID, trackName, artistName string, durationMs int64) (string, error) {
|
||||
client := NewLyricsClient()
|
||||
lyrics, err := client.FetchLyricsAllSources(spotifyID, trackName, artistName)
|
||||
durationSec := float64(durationMs) / 1000.0
|
||||
lyrics, err := client.FetchLyricsAllSources(spotifyID, trackName, artistName, durationSec)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -673,7 +675,8 @@ func FetchLyrics(spotifyID, trackName, artistName string) (string, error) {
|
||||
|
||||
// GetLyricsLRC fetches lyrics and converts to LRC format string with metadata headers
|
||||
// First tries to extract from file, then falls back to fetching from internet
|
||||
func GetLyricsLRC(spotifyID, trackName, artistName string, filePath string) (string, error) {
|
||||
// durationMs: track duration in milliseconds for matching, use 0 to skip duration matching
|
||||
func GetLyricsLRC(spotifyID, trackName, artistName string, filePath string, durationMs int64) (string, error) {
|
||||
if filePath != "" {
|
||||
lyrics, err := ExtractLyrics(filePath)
|
||||
if err == nil && lyrics != "" {
|
||||
@@ -682,7 +685,8 @@ func GetLyricsLRC(spotifyID, trackName, artistName string, filePath string) (str
|
||||
}
|
||||
|
||||
client := NewLyricsClient()
|
||||
lyricsData, err := client.FetchLyricsAllSources(spotifyID, trackName, artistName)
|
||||
durationSec := float64(durationMs) / 1000.0
|
||||
lyricsData, err := client.FetchLyricsAllSources(spotifyID, trackName, artistName, durationSec)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
+155
-5
@@ -3,14 +3,100 @@ package gobackend
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ========================================
|
||||
// Lyrics Cache with TTL
|
||||
// ========================================
|
||||
|
||||
const (
|
||||
lyricsCacheTTL = 24 * time.Hour // Cache lyrics for 24 hours
|
||||
durationToleranceSec = 10.0 // Duration matching tolerance in seconds
|
||||
)
|
||||
|
||||
type lyricsCacheEntry struct {
|
||||
response *LyricsResponse
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
type lyricsCache struct {
|
||||
mu sync.RWMutex
|
||||
cache map[string]*lyricsCacheEntry
|
||||
}
|
||||
|
||||
var globalLyricsCache = &lyricsCache{
|
||||
cache: make(map[string]*lyricsCacheEntry),
|
||||
}
|
||||
|
||||
func (c *lyricsCache) generateKey(artist, track string, durationSec float64) string {
|
||||
// Normalize key: lowercase, trim spaces
|
||||
normalizedArtist := strings.ToLower(strings.TrimSpace(artist))
|
||||
normalizedTrack := strings.ToLower(strings.TrimSpace(track))
|
||||
// Round duration to nearest 10 seconds for cache key
|
||||
roundedDuration := math.Round(durationSec/10) * 10
|
||||
return fmt.Sprintf("%s|%s|%.0f", normalizedArtist, normalizedTrack, roundedDuration)
|
||||
}
|
||||
|
||||
func (c *lyricsCache) Get(artist, track string, durationSec float64) (*LyricsResponse, bool) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
key := c.generateKey(artist, track, durationSec)
|
||||
entry, exists := c.cache[key]
|
||||
if !exists {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Check if expired
|
||||
if time.Now().After(entry.expiresAt) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return entry.response, true
|
||||
}
|
||||
|
||||
func (c *lyricsCache) Set(artist, track string, durationSec float64, response *LyricsResponse) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
key := c.generateKey(artist, track, durationSec)
|
||||
c.cache[key] = &lyricsCacheEntry{
|
||||
response: response,
|
||||
expiresAt: time.Now().Add(lyricsCacheTTL),
|
||||
}
|
||||
}
|
||||
|
||||
// CleanExpired removes expired entries from cache
|
||||
func (c *lyricsCache) CleanExpired() int {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
cleaned := 0
|
||||
for key, entry := range c.cache {
|
||||
if now.After(entry.expiresAt) {
|
||||
delete(c.cache, key)
|
||||
cleaned++
|
||||
}
|
||||
}
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// Size returns current cache size
|
||||
func (c *lyricsCache) Size() int {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return len(c.cache)
|
||||
}
|
||||
|
||||
type LRCLibResponse struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
@@ -86,7 +172,9 @@ func (c *LyricsClient) FetchLyricsWithMetadata(artist, track string) (*LyricsRes
|
||||
return c.parseLRCLibResponse(&lrcResp), nil
|
||||
}
|
||||
|
||||
func (c *LyricsClient) FetchLyricsFromLRCLibSearch(query string) (*LyricsResponse, error) {
|
||||
// FetchLyricsFromLRCLibSearch searches lyrics with optional duration matching
|
||||
// durationSec: track duration in seconds, use 0 to skip duration matching
|
||||
func (c *LyricsClient) FetchLyricsFromLRCLibSearch(query string, durationSec float64) (*LyricsResponse, error) {
|
||||
baseURL := "https://lrclib.net/api/search"
|
||||
params := url.Values{}
|
||||
params.Set("q", query)
|
||||
@@ -118,6 +206,13 @@ func (c *LyricsClient) FetchLyricsFromLRCLibSearch(query string) (*LyricsRespons
|
||||
return nil, fmt.Errorf("no lyrics found")
|
||||
}
|
||||
|
||||
// Filter and score results based on duration matching and synced lyrics
|
||||
bestMatch := c.findBestMatch(results, durationSec)
|
||||
if bestMatch != nil {
|
||||
return c.parseLRCLibResponse(bestMatch), nil
|
||||
}
|
||||
|
||||
// Fallback: return first result with synced lyrics
|
||||
for _, result := range results {
|
||||
if result.SyncedLyrics != "" {
|
||||
return c.parseLRCLibResponse(&result), nil
|
||||
@@ -127,34 +222,89 @@ func (c *LyricsClient) FetchLyricsFromLRCLibSearch(query string) (*LyricsRespons
|
||||
return c.parseLRCLibResponse(&results[0]), nil
|
||||
}
|
||||
|
||||
func (c *LyricsClient) FetchLyricsAllSources(spotifyID, trackName, artistName string) (*LyricsResponse, error) {
|
||||
lyrics, err := c.FetchLyricsWithMetadata(artistName, trackName)
|
||||
// findBestMatch finds the best matching lyrics based on duration and sync status
|
||||
func (c *LyricsClient) findBestMatch(results []LRCLibResponse, targetDurationSec float64) *LRCLibResponse {
|
||||
var bestSynced *LRCLibResponse
|
||||
var bestPlain *LRCLibResponse
|
||||
|
||||
for i := range results {
|
||||
result := &results[i]
|
||||
|
||||
// Check duration match if target duration is provided
|
||||
durationMatches := targetDurationSec == 0 || c.durationMatches(result.Duration, targetDurationSec)
|
||||
|
||||
if durationMatches {
|
||||
// Prefer synced lyrics over plain
|
||||
if result.SyncedLyrics != "" && bestSynced == nil {
|
||||
bestSynced = result
|
||||
} else if result.PlainLyrics != "" && bestPlain == nil {
|
||||
bestPlain = result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return synced first, then plain
|
||||
if bestSynced != nil {
|
||||
return bestSynced
|
||||
}
|
||||
return bestPlain
|
||||
}
|
||||
|
||||
// durationMatches checks if two durations are within tolerance
|
||||
func (c *LyricsClient) durationMatches(lrcDuration, targetDuration float64) bool {
|
||||
diff := math.Abs(lrcDuration - targetDuration)
|
||||
return diff <= durationToleranceSec
|
||||
}
|
||||
|
||||
// FetchLyricsAllSources fetches lyrics from multiple sources with caching and duration matching
|
||||
// durationSec: track duration in seconds for matching, use 0 to skip duration matching
|
||||
func (c *LyricsClient) FetchLyricsAllSources(spotifyID, trackName, artistName string, durationSec float64) (*LyricsResponse, error) {
|
||||
// Check cache first
|
||||
if cached, found := globalLyricsCache.Get(artistName, trackName, durationSec); found {
|
||||
fmt.Printf("[Lyrics] Cache hit for: %s - %s\n", artistName, trackName)
|
||||
cachedCopy := *cached
|
||||
cachedCopy.Source = cached.Source + " (cached)"
|
||||
return &cachedCopy, nil
|
||||
}
|
||||
|
||||
var lyrics *LyricsResponse
|
||||
var err error
|
||||
|
||||
// Try exact match first
|
||||
lyrics, err = c.FetchLyricsWithMetadata(artistName, trackName)
|
||||
if err == nil && lyrics != nil && len(lyrics.Lines) > 0 {
|
||||
lyrics.Source = "LRCLIB"
|
||||
globalLyricsCache.Set(artistName, trackName, durationSec, lyrics)
|
||||
return lyrics, nil
|
||||
}
|
||||
|
||||
// Try with simplified track name
|
||||
simplifiedTrack := simplifyTrackName(trackName)
|
||||
if simplifiedTrack != trackName {
|
||||
lyrics, err = c.FetchLyricsWithMetadata(artistName, simplifiedTrack)
|
||||
if err == nil && lyrics != nil && len(lyrics.Lines) > 0 {
|
||||
lyrics.Source = "LRCLIB (simplified)"
|
||||
globalLyricsCache.Set(artistName, trackName, durationSec, lyrics)
|
||||
return lyrics, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Search with duration matching
|
||||
query := artistName + " " + trackName
|
||||
lyrics, err = c.FetchLyricsFromLRCLibSearch(query)
|
||||
lyrics, err = c.FetchLyricsFromLRCLibSearch(query, durationSec)
|
||||
if err == nil && lyrics != nil && len(lyrics.Lines) > 0 {
|
||||
lyrics.Source = "LRCLIB Search"
|
||||
globalLyricsCache.Set(artistName, trackName, durationSec, lyrics)
|
||||
return lyrics, nil
|
||||
}
|
||||
|
||||
// Search with simplified name and duration matching
|
||||
if simplifiedTrack != trackName {
|
||||
query = artistName + " " + simplifiedTrack
|
||||
lyrics, err = c.FetchLyricsFromLRCLibSearch(query)
|
||||
lyrics, err = c.FetchLyricsFromLRCLibSearch(query, durationSec)
|
||||
if err == nil && lyrics != nil && len(lyrics.Lines) > 0 {
|
||||
lyrics.Source = "LRCLIB Search (simplified)"
|
||||
globalLyricsCache.Set(artistName, trackName, durationSec, lyrics)
|
||||
return lyrics, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,7 @@ type ParallelDownloadResult struct {
|
||||
|
||||
// FetchCoverAndLyricsParallel downloads cover and fetches lyrics in parallel
|
||||
// This runs while the main audio download is happening
|
||||
// durationMs: track duration in milliseconds for lyrics matching
|
||||
func FetchCoverAndLyricsParallel(
|
||||
coverURL string,
|
||||
maxQualityCover bool,
|
||||
@@ -131,6 +132,7 @@ func FetchCoverAndLyricsParallel(
|
||||
trackName string,
|
||||
artistName string,
|
||||
embedLyrics bool,
|
||||
durationMs int64,
|
||||
) *ParallelDownloadResult {
|
||||
result := &ParallelDownloadResult{}
|
||||
var wg sync.WaitGroup
|
||||
@@ -158,7 +160,8 @@ func FetchCoverAndLyricsParallel(
|
||||
defer wg.Done()
|
||||
fmt.Println("[Parallel] Starting lyrics fetch...")
|
||||
client := NewLyricsClient()
|
||||
lyrics, err := client.FetchLyricsAllSources(spotifyID, trackName, artistName)
|
||||
durationSec := float64(durationMs) / 1000.0
|
||||
lyrics, err := client.FetchLyricsAllSources(spotifyID, trackName, artistName, durationSec)
|
||||
if err != nil {
|
||||
result.LyricsErr = err
|
||||
fmt.Printf("[Parallel] Lyrics fetch failed: %v\n", err)
|
||||
|
||||
+2
-1
@@ -1,8 +1,8 @@
|
||||
package gobackend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -1085,6 +1085,7 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) {
|
||||
req.TrackName,
|
||||
req.ArtistName,
|
||||
req.EmbedLyrics,
|
||||
int64(req.DurationMS),
|
||||
)
|
||||
}()
|
||||
|
||||
|
||||
+2
-1
@@ -1,8 +1,8 @@
|
||||
package gobackend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
@@ -1666,6 +1666,7 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) {
|
||||
req.TrackName,
|
||||
req.ArtistName,
|
||||
req.EmbedLyrics,
|
||||
int64(req.DurationMS),
|
||||
)
|
||||
}()
|
||||
|
||||
|
||||
@@ -161,7 +161,8 @@ import Gobackend // Import Go framework
|
||||
let spotifyId = args["spotify_id"] as! String
|
||||
let trackName = args["track_name"] as! String
|
||||
let artistName = args["artist_name"] as! String
|
||||
let response = GobackendFetchLyrics(spotifyId, trackName, artistName, &error)
|
||||
let durationMs = args["duration_ms"] as? Int64 ?? 0
|
||||
let response = GobackendFetchLyrics(spotifyId, trackName, artistName, durationMs, &error)
|
||||
if let error = error { throw error }
|
||||
return response
|
||||
|
||||
@@ -171,7 +172,8 @@ import Gobackend // Import Go framework
|
||||
let trackName = args["track_name"] as! String
|
||||
let artistName = args["artist_name"] as! String
|
||||
let filePath = args["file_path"] as? String ?? ""
|
||||
let response = GobackendGetLyricsLRC(spotifyId, trackName, artistName, filePath, &error)
|
||||
let durationMs = args["duration_ms"] as? Int64 ?? 0
|
||||
let response = GobackendGetLyricsLRC(spotifyId, trackName, artistName, filePath, durationMs, &error)
|
||||
if let error = error { throw error }
|
||||
return response
|
||||
|
||||
|
||||
@@ -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.1.0';
|
||||
static const String buildNumber = '59';
|
||||
static const String version = '3.1.1';
|
||||
static const String buildNumber = '60';
|
||||
static const String fullVersion = '$version+$buildNumber';
|
||||
|
||||
|
||||
|
||||
+106
-106
@@ -1,23 +1,23 @@
|
||||
{
|
||||
"@@locale": "de",
|
||||
"@@last_modified": "2026-01-16",
|
||||
"@@last_modified": "2026-01-17",
|
||||
"appName": "SpotiFLAC",
|
||||
"@appName": {
|
||||
"description": "App name - DO NOT TRANSLATE"
|
||||
},
|
||||
"appDescription": "Download Spotify tracks in lossless quality from Tidal, Qobuz, and Amazon Music.",
|
||||
"appDescription": "Laden Sie Spotify-Titel in verlustfreier Qualität von Tidal, Qobuz und Amazon Music herunter.",
|
||||
"@appDescription": {
|
||||
"description": "App description shown in about page"
|
||||
},
|
||||
"navHome": "Home",
|
||||
"navHome": "Startseite",
|
||||
"@navHome": {
|
||||
"description": "Bottom navigation - Home tab"
|
||||
},
|
||||
"navHistory": "History",
|
||||
"navHistory": "Verlauf",
|
||||
"@navHistory": {
|
||||
"description": "Bottom navigation - History tab"
|
||||
},
|
||||
"navSettings": "Settings",
|
||||
"navSettings": "Einstellungen",
|
||||
"@navSettings": {
|
||||
"description": "Bottom navigation - Settings tab"
|
||||
},
|
||||
@@ -25,15 +25,15 @@
|
||||
"@navStore": {
|
||||
"description": "Bottom navigation - Extension store tab"
|
||||
},
|
||||
"homeTitle": "Home",
|
||||
"homeTitle": "Startseite",
|
||||
"@homeTitle": {
|
||||
"description": "Home screen title"
|
||||
},
|
||||
"homeSearchHint": "Paste Spotify URL or search...",
|
||||
"homeSearchHint": "Spotify-URL einfügen oder suchen...",
|
||||
"@homeSearchHint": {
|
||||
"description": "Placeholder text in search box"
|
||||
},
|
||||
"homeSearchHintExtension": "Search with {extensionName}...",
|
||||
"homeSearchHintExtension": "Mit {extensionName} suchen...",
|
||||
"@homeSearchHintExtension": {
|
||||
"description": "Placeholder when extension search is active",
|
||||
"placeholders": {
|
||||
@@ -43,23 +43,23 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"homeSubtitle": "Paste a Spotify link or search by name",
|
||||
"homeSubtitle": "Spotify-Link einfügen oder nach Namen suchen",
|
||||
"@homeSubtitle": {
|
||||
"description": "Subtitle shown below search box"
|
||||
},
|
||||
"homeSupports": "Supports: Track, Album, Playlist, Artist URLs",
|
||||
"homeSupports": "Unterstützt: Titel, Album, Playlist, Künstler-URLs",
|
||||
"@homeSupports": {
|
||||
"description": "Info text about supported URL types"
|
||||
},
|
||||
"homeRecent": "Recent",
|
||||
"homeRecent": "Zuletzt",
|
||||
"@homeRecent": {
|
||||
"description": "Section header for recent searches"
|
||||
},
|
||||
"historyTitle": "History",
|
||||
"historyTitle": "Verlauf",
|
||||
"@historyTitle": {
|
||||
"description": "History screen title"
|
||||
},
|
||||
"historyDownloading": "Downloading ({count})",
|
||||
"historyDownloading": "Wird heruntergeladen ({count})",
|
||||
"@historyDownloading": {
|
||||
"description": "Tab showing active downloads count",
|
||||
"placeholders": {
|
||||
@@ -69,15 +69,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"historyDownloaded": "Downloaded",
|
||||
"historyDownloaded": "Heruntergeladen",
|
||||
"@historyDownloaded": {
|
||||
"description": "Tab showing completed downloads"
|
||||
},
|
||||
"historyFilterAll": "All",
|
||||
"historyFilterAll": "Alle",
|
||||
"@historyFilterAll": {
|
||||
"description": "Filter chip - show all items"
|
||||
},
|
||||
"historyFilterAlbums": "Albums",
|
||||
"historyFilterAlbums": "Alben",
|
||||
"@historyFilterAlbums": {
|
||||
"description": "Filter chip - show albums only"
|
||||
},
|
||||
@@ -85,7 +85,7 @@
|
||||
"@historyFilterSingles": {
|
||||
"description": "Filter chip - show singles only"
|
||||
},
|
||||
"historyTracksCount": "{count, plural, =1{1 track} other{{count} tracks}}",
|
||||
"historyTracksCount": "{count, plural, =1{1 Titel} other{{count} Titel}}",
|
||||
"@historyTracksCount": {
|
||||
"description": "Track count with plural form",
|
||||
"placeholders": {
|
||||
@@ -94,7 +94,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"historyAlbumsCount": "{count, plural, =1{1 album} other{{count} albums}}",
|
||||
"historyAlbumsCount": "{count, plural, =1{1 Album} other{{count} Alben}}",
|
||||
"@historyAlbumsCount": {
|
||||
"description": "Album count with plural form",
|
||||
"placeholders": {
|
||||
@@ -103,31 +103,31 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"historyNoDownloads": "No download history",
|
||||
"historyNoDownloads": "Kein Download-Verlauf",
|
||||
"@historyNoDownloads": {
|
||||
"description": "Empty state title"
|
||||
},
|
||||
"historyNoDownloadsSubtitle": "Downloaded tracks will appear here",
|
||||
"historyNoDownloadsSubtitle": "Heruntergeladene Titel werden hier angezeigt",
|
||||
"@historyNoDownloadsSubtitle": {
|
||||
"description": "Empty state subtitle"
|
||||
},
|
||||
"historyNoAlbums": "No album downloads",
|
||||
"historyNoAlbums": "Keine Album-Downloads",
|
||||
"@historyNoAlbums": {
|
||||
"description": "Empty state when filtering albums"
|
||||
},
|
||||
"historyNoAlbumsSubtitle": "Download multiple tracks from an album to see them here",
|
||||
"historyNoAlbumsSubtitle": "Laden Sie mehrere Titel eines Albums herunter, um sie hier zu sehen",
|
||||
"@historyNoAlbumsSubtitle": {
|
||||
"description": "Empty state subtitle for albums filter"
|
||||
},
|
||||
"historyNoSingles": "No single downloads",
|
||||
"historyNoSingles": "Keine Einzel-Downloads",
|
||||
"@historyNoSingles": {
|
||||
"description": "Empty state when filtering singles"
|
||||
},
|
||||
"historyNoSinglesSubtitle": "Single track downloads will appear here",
|
||||
"historyNoSinglesSubtitle": "Einzelne Titel-Downloads werden hier angezeigt",
|
||||
"@historyNoSinglesSubtitle": {
|
||||
"description": "Empty state subtitle for singles filter"
|
||||
},
|
||||
"settingsTitle": "Settings",
|
||||
"settingsTitle": "Einstellungen",
|
||||
"@settingsTitle": {
|
||||
"description": "Settings screen title"
|
||||
},
|
||||
@@ -135,19 +135,19 @@
|
||||
"@settingsDownload": {
|
||||
"description": "Settings section - download options"
|
||||
},
|
||||
"settingsAppearance": "Appearance",
|
||||
"settingsAppearance": "Erscheinungsbild",
|
||||
"@settingsAppearance": {
|
||||
"description": "Settings section - visual customization"
|
||||
},
|
||||
"settingsOptions": "Options",
|
||||
"settingsOptions": "Optionen",
|
||||
"@settingsOptions": {
|
||||
"description": "Settings section - app options"
|
||||
},
|
||||
"settingsExtensions": "Extensions",
|
||||
"settingsExtensions": "Erweiterungen",
|
||||
"@settingsExtensions": {
|
||||
"description": "Settings section - extension management"
|
||||
},
|
||||
"settingsAbout": "About",
|
||||
"settingsAbout": "Über",
|
||||
"@settingsAbout": {
|
||||
"description": "Settings section - app info"
|
||||
},
|
||||
@@ -155,55 +155,55 @@
|
||||
"@downloadTitle": {
|
||||
"description": "Download settings page title"
|
||||
},
|
||||
"downloadLocation": "Download Location",
|
||||
"downloadLocation": "Download-Speicherort",
|
||||
"@downloadLocation": {
|
||||
"description": "Setting for download folder"
|
||||
},
|
||||
"downloadLocationSubtitle": "Choose where to save files",
|
||||
"downloadLocationSubtitle": "Wählen Sie den Speicherort für Dateien",
|
||||
"@downloadLocationSubtitle": {
|
||||
"description": "Subtitle for download location"
|
||||
},
|
||||
"downloadLocationDefault": "Default location",
|
||||
"downloadLocationDefault": "Standard-Speicherort",
|
||||
"@downloadLocationDefault": {
|
||||
"description": "Shown when using default folder"
|
||||
},
|
||||
"downloadDefaultService": "Default Service",
|
||||
"downloadDefaultService": "Standard-Dienst",
|
||||
"@downloadDefaultService": {
|
||||
"description": "Setting for preferred download service (Tidal/Qobuz/Amazon)"
|
||||
},
|
||||
"downloadDefaultServiceSubtitle": "Service used for downloads",
|
||||
"downloadDefaultServiceSubtitle": "Dienst für Downloads",
|
||||
"@downloadDefaultServiceSubtitle": {
|
||||
"description": "Subtitle for default service"
|
||||
},
|
||||
"downloadDefaultQuality": "Default Quality",
|
||||
"downloadDefaultQuality": "Standard-Qualität",
|
||||
"@downloadDefaultQuality": {
|
||||
"description": "Setting for audio quality"
|
||||
},
|
||||
"downloadAskQuality": "Ask Quality Before Download",
|
||||
"downloadAskQuality": "Qualität vor Download abfragen",
|
||||
"@downloadAskQuality": {
|
||||
"description": "Toggle to show quality picker"
|
||||
},
|
||||
"downloadAskQualitySubtitle": "Show quality picker for each download",
|
||||
"downloadAskQualitySubtitle": "Qualitätsauswahl für jeden Download anzeigen",
|
||||
"@downloadAskQualitySubtitle": {
|
||||
"description": "Subtitle for ask quality toggle"
|
||||
},
|
||||
"downloadFilenameFormat": "Filename Format",
|
||||
"downloadFilenameFormat": "Dateinamenformat",
|
||||
"@downloadFilenameFormat": {
|
||||
"description": "Setting for output filename pattern"
|
||||
},
|
||||
"downloadFolderOrganization": "Folder Organization",
|
||||
"downloadFolderOrganization": "Ordnerstruktur",
|
||||
"@downloadFolderOrganization": {
|
||||
"description": "Setting for folder structure"
|
||||
},
|
||||
"downloadSeparateSingles": "Separate Singles",
|
||||
"downloadSeparateSingles": "Singles trennen",
|
||||
"@downloadSeparateSingles": {
|
||||
"description": "Toggle to separate single tracks"
|
||||
},
|
||||
"downloadSeparateSinglesSubtitle": "Put single tracks in a separate folder",
|
||||
"downloadSeparateSinglesSubtitle": "Einzelne Titel in separatem Ordner speichern",
|
||||
"@downloadSeparateSinglesSubtitle": {
|
||||
"description": "Subtitle for separate singles toggle"
|
||||
},
|
||||
"qualityBest": "Best Available",
|
||||
"qualityBest": "Beste Qualität",
|
||||
"@qualityBest": {
|
||||
"description": "Audio quality option - highest available"
|
||||
},
|
||||
@@ -219,11 +219,11 @@
|
||||
"@quality128": {
|
||||
"description": "Audio quality option - 128kbps MP3"
|
||||
},
|
||||
"appearanceTitle": "Appearance",
|
||||
"appearanceTitle": "Erscheinungsbild",
|
||||
"@appearanceTitle": {
|
||||
"description": "Appearance settings page title"
|
||||
},
|
||||
"appearanceTheme": "Theme",
|
||||
"appearanceTheme": "Design",
|
||||
"@appearanceTheme": {
|
||||
"description": "Theme mode setting"
|
||||
},
|
||||
@@ -231,55 +231,55 @@
|
||||
"@appearanceThemeSystem": {
|
||||
"description": "Follow system theme"
|
||||
},
|
||||
"appearanceThemeLight": "Light",
|
||||
"appearanceThemeLight": "Hell",
|
||||
"@appearanceThemeLight": {
|
||||
"description": "Light theme"
|
||||
},
|
||||
"appearanceThemeDark": "Dark",
|
||||
"appearanceThemeDark": "Dunkel",
|
||||
"@appearanceThemeDark": {
|
||||
"description": "Dark theme"
|
||||
},
|
||||
"appearanceDynamicColor": "Dynamic Color",
|
||||
"appearanceDynamicColor": "Dynamische Farben",
|
||||
"@appearanceDynamicColor": {
|
||||
"description": "Material You dynamic colors"
|
||||
},
|
||||
"appearanceDynamicColorSubtitle": "Use colors from your wallpaper",
|
||||
"appearanceDynamicColorSubtitle": "Farben von Ihrem Hintergrundbild verwenden",
|
||||
"@appearanceDynamicColorSubtitle": {
|
||||
"description": "Subtitle for dynamic color"
|
||||
},
|
||||
"appearanceAccentColor": "Accent Color",
|
||||
"appearanceAccentColor": "Akzentfarbe",
|
||||
"@appearanceAccentColor": {
|
||||
"description": "Custom accent color picker"
|
||||
},
|
||||
"appearanceHistoryView": "History View",
|
||||
"appearanceHistoryView": "Verlaufsansicht",
|
||||
"@appearanceHistoryView": {
|
||||
"description": "Layout style for history"
|
||||
},
|
||||
"appearanceHistoryViewList": "List",
|
||||
"appearanceHistoryViewList": "Liste",
|
||||
"@appearanceHistoryViewList": {
|
||||
"description": "List layout option"
|
||||
},
|
||||
"appearanceHistoryViewGrid": "Grid",
|
||||
"appearanceHistoryViewGrid": "Raster",
|
||||
"@appearanceHistoryViewGrid": {
|
||||
"description": "Grid layout option"
|
||||
},
|
||||
"optionsTitle": "Options",
|
||||
"optionsTitle": "Optionen",
|
||||
"@optionsTitle": {
|
||||
"description": "Options settings page title"
|
||||
},
|
||||
"optionsSearchSource": "Search Source",
|
||||
"optionsSearchSource": "Suchquelle",
|
||||
"@optionsSearchSource": {
|
||||
"description": "Section for search provider settings"
|
||||
},
|
||||
"optionsPrimaryProvider": "Primary Provider",
|
||||
"optionsPrimaryProvider": "Primärer Anbieter",
|
||||
"@optionsPrimaryProvider": {
|
||||
"description": "Main search provider setting"
|
||||
},
|
||||
"optionsPrimaryProviderSubtitle": "Service used when searching by track name.",
|
||||
"optionsPrimaryProviderSubtitle": "Dienst für die Suche nach Titelnamen.",
|
||||
"@optionsPrimaryProviderSubtitle": {
|
||||
"description": "Subtitle for primary provider"
|
||||
},
|
||||
"optionsUsingExtension": "Using extension: {extensionName}",
|
||||
"optionsUsingExtension": "Erweiterung verwenden: {extensionName}",
|
||||
"@optionsUsingExtension": {
|
||||
"description": "Shows active extension name",
|
||||
"placeholders": {
|
||||
@@ -288,55 +288,55 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"optionsSwitchBack": "Tap Deezer or Spotify to switch back from extension",
|
||||
"optionsSwitchBack": "Tippen Sie auf Deezer oder Spotify, um von der Erweiterung zurückzuwechseln",
|
||||
"@optionsSwitchBack": {
|
||||
"description": "Hint to switch back to built-in providers"
|
||||
},
|
||||
"optionsAutoFallback": "Auto Fallback",
|
||||
"optionsAutoFallback": "Automatischer Fallback",
|
||||
"@optionsAutoFallback": {
|
||||
"description": "Auto-retry with other services"
|
||||
},
|
||||
"optionsAutoFallbackSubtitle": "Try other services if download fails",
|
||||
"optionsAutoFallbackSubtitle": "Andere Dienste versuchen, wenn Download fehlschlägt",
|
||||
"@optionsAutoFallbackSubtitle": {
|
||||
"description": "Subtitle for auto fallback"
|
||||
},
|
||||
"optionsUseExtensionProviders": "Use Extension Providers",
|
||||
"optionsUseExtensionProviders": "Erweiterungs-Anbieter verwenden",
|
||||
"@optionsUseExtensionProviders": {
|
||||
"description": "Enable extension download providers"
|
||||
},
|
||||
"optionsUseExtensionProvidersOn": "Extensions will be tried first",
|
||||
"optionsUseExtensionProvidersOn": "Erweiterungen werden zuerst versucht",
|
||||
"@optionsUseExtensionProvidersOn": {
|
||||
"description": "Status when extension providers enabled"
|
||||
},
|
||||
"optionsUseExtensionProvidersOff": "Using built-in providers only",
|
||||
"optionsUseExtensionProvidersOff": "Nur integrierte Anbieter verwenden",
|
||||
"@optionsUseExtensionProvidersOff": {
|
||||
"description": "Status when extension providers disabled"
|
||||
},
|
||||
"optionsEmbedLyrics": "Embed Lyrics",
|
||||
"optionsEmbedLyrics": "Liedtexte einbetten",
|
||||
"@optionsEmbedLyrics": {
|
||||
"description": "Embed lyrics in audio files"
|
||||
},
|
||||
"optionsEmbedLyricsSubtitle": "Embed synced lyrics into FLAC files",
|
||||
"optionsEmbedLyricsSubtitle": "Synchronisierte Liedtexte in FLAC-Dateien einbetten",
|
||||
"@optionsEmbedLyricsSubtitle": {
|
||||
"description": "Subtitle for embed lyrics"
|
||||
},
|
||||
"optionsMaxQualityCover": "Max Quality Cover",
|
||||
"optionsMaxQualityCover": "Maximale Cover-Qualität",
|
||||
"@optionsMaxQualityCover": {
|
||||
"description": "Download highest quality album art"
|
||||
},
|
||||
"optionsMaxQualityCoverSubtitle": "Download highest resolution cover art",
|
||||
"optionsMaxQualityCoverSubtitle": "Cover in höchster Auflösung herunterladen",
|
||||
"@optionsMaxQualityCoverSubtitle": {
|
||||
"description": "Subtitle for max quality cover"
|
||||
},
|
||||
"optionsConcurrentDownloads": "Concurrent Downloads",
|
||||
"optionsConcurrentDownloads": "Parallele Downloads",
|
||||
"@optionsConcurrentDownloads": {
|
||||
"description": "Number of parallel downloads"
|
||||
},
|
||||
"optionsConcurrentSequential": "Sequential (1 at a time)",
|
||||
"optionsConcurrentSequential": "Sequentiell (1 gleichzeitig)",
|
||||
"@optionsConcurrentSequential": {
|
||||
"description": "Download one at a time"
|
||||
},
|
||||
"optionsConcurrentParallel": "{count} parallel downloads",
|
||||
"optionsConcurrentParallel": "{count} parallele Downloads",
|
||||
"@optionsConcurrentParallel": {
|
||||
"description": "Multiple parallel downloads",
|
||||
"placeholders": {
|
||||
@@ -345,67 +345,67 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"optionsConcurrentWarning": "Parallel downloads may trigger rate limiting",
|
||||
"optionsConcurrentWarning": "Parallele Downloads können Ratenlimitierung auslösen",
|
||||
"@optionsConcurrentWarning": {
|
||||
"description": "Warning about rate limits"
|
||||
},
|
||||
"optionsExtensionStore": "Extension Store",
|
||||
"optionsExtensionStore": "Erweiterungs-Store",
|
||||
"@optionsExtensionStore": {
|
||||
"description": "Show/hide store tab"
|
||||
},
|
||||
"optionsExtensionStoreSubtitle": "Show Store tab in navigation",
|
||||
"optionsExtensionStoreSubtitle": "Store-Tab in Navigation anzeigen",
|
||||
"@optionsExtensionStoreSubtitle": {
|
||||
"description": "Subtitle for extension store toggle"
|
||||
},
|
||||
"optionsCheckUpdates": "Check for Updates",
|
||||
"optionsCheckUpdates": "Nach Updates suchen",
|
||||
"@optionsCheckUpdates": {
|
||||
"description": "Auto update check toggle"
|
||||
},
|
||||
"optionsCheckUpdatesSubtitle": "Notify when new version is available",
|
||||
"optionsCheckUpdatesSubtitle": "Benachrichtigen, wenn neue Version verfügbar",
|
||||
"@optionsCheckUpdatesSubtitle": {
|
||||
"description": "Subtitle for update check"
|
||||
},
|
||||
"optionsUpdateChannel": "Update Channel",
|
||||
"optionsUpdateChannel": "Update-Kanal",
|
||||
"@optionsUpdateChannel": {
|
||||
"description": "Stable vs preview releases"
|
||||
},
|
||||
"optionsUpdateChannelStable": "Stable releases only",
|
||||
"optionsUpdateChannelStable": "Nur stabile Versionen",
|
||||
"@optionsUpdateChannelStable": {
|
||||
"description": "Only stable updates"
|
||||
},
|
||||
"optionsUpdateChannelPreview": "Get preview releases",
|
||||
"optionsUpdateChannelPreview": "Vorschau-Versionen erhalten",
|
||||
"@optionsUpdateChannelPreview": {
|
||||
"description": "Include beta/preview updates"
|
||||
},
|
||||
"optionsUpdateChannelWarning": "Preview may contain bugs or incomplete features",
|
||||
"optionsUpdateChannelWarning": "Vorschau kann Fehler oder unvollständige Funktionen enthalten",
|
||||
"@optionsUpdateChannelWarning": {
|
||||
"description": "Warning about preview channel"
|
||||
},
|
||||
"optionsClearHistory": "Clear Download History",
|
||||
"optionsClearHistory": "Download-Verlauf löschen",
|
||||
"@optionsClearHistory": {
|
||||
"description": "Delete all download history"
|
||||
},
|
||||
"optionsClearHistorySubtitle": "Remove all downloaded tracks from history",
|
||||
"optionsClearHistorySubtitle": "Alle heruntergeladenen Titel aus dem Verlauf entfernen",
|
||||
"@optionsClearHistorySubtitle": {
|
||||
"description": "Subtitle for clear history"
|
||||
},
|
||||
"optionsDetailedLogging": "Detailed Logging",
|
||||
"optionsDetailedLogging": "Detaillierte Protokollierung",
|
||||
"@optionsDetailedLogging": {
|
||||
"description": "Enable verbose logs for debugging"
|
||||
},
|
||||
"optionsDetailedLoggingOn": "Detailed logs are being recorded",
|
||||
"optionsDetailedLoggingOn": "Detaillierte Protokolle werden aufgezeichnet",
|
||||
"@optionsDetailedLoggingOn": {
|
||||
"description": "Status when logging enabled"
|
||||
},
|
||||
"optionsDetailedLoggingOff": "Enable for bug reports",
|
||||
"optionsDetailedLoggingOff": "Für Fehlerberichte aktivieren",
|
||||
"@optionsDetailedLoggingOff": {
|
||||
"description": "Status when logging disabled"
|
||||
},
|
||||
"optionsSpotifyCredentials": "Spotify Credentials",
|
||||
"optionsSpotifyCredentials": "Spotify-Anmeldedaten",
|
||||
"@optionsSpotifyCredentials": {
|
||||
"description": "Spotify API credentials setting"
|
||||
},
|
||||
"optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...",
|
||||
"optionsSpotifyCredentialsConfigured": "Client-ID: {clientId}...",
|
||||
"@optionsSpotifyCredentialsConfigured": {
|
||||
"description": "Shows configured client ID preview",
|
||||
"placeholders": {
|
||||
@@ -414,35 +414,35 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"optionsSpotifyCredentialsRequired": "Required - tap to configure",
|
||||
"optionsSpotifyCredentialsRequired": "Erforderlich - zum Konfigurieren tippen",
|
||||
"@optionsSpotifyCredentialsRequired": {
|
||||
"description": "Prompt to set up credentials"
|
||||
},
|
||||
"optionsSpotifyWarning": "Spotify requires your own API credentials. Get them free from developer.spotify.com",
|
||||
"optionsSpotifyWarning": "Spotify erfordert eigene API-Anmeldedaten. Kostenlos erhältlich auf developer.spotify.com",
|
||||
"@optionsSpotifyWarning": {
|
||||
"description": "Info about Spotify API requirement"
|
||||
},
|
||||
"extensionsTitle": "Extensions",
|
||||
"extensionsTitle": "Erweiterungen",
|
||||
"@extensionsTitle": {
|
||||
"description": "Extensions page title"
|
||||
},
|
||||
"extensionsInstalled": "Installed Extensions",
|
||||
"extensionsInstalled": "Installierte Erweiterungen",
|
||||
"@extensionsInstalled": {
|
||||
"description": "Section header for installed extensions"
|
||||
},
|
||||
"extensionsNone": "No extensions installed",
|
||||
"extensionsNone": "Keine Erweiterungen installiert",
|
||||
"@extensionsNone": {
|
||||
"description": "Empty state title"
|
||||
},
|
||||
"extensionsNoneSubtitle": "Install extensions from the Store tab",
|
||||
"extensionsNoneSubtitle": "Erweiterungen aus dem Store-Tab installieren",
|
||||
"@extensionsNoneSubtitle": {
|
||||
"description": "Empty state subtitle"
|
||||
},
|
||||
"extensionsEnabled": "Enabled",
|
||||
"extensionsEnabled": "Aktiviert",
|
||||
"@extensionsEnabled": {
|
||||
"description": "Extension status - active"
|
||||
},
|
||||
"extensionsDisabled": "Disabled",
|
||||
"extensionsDisabled": "Deaktiviert",
|
||||
"@extensionsDisabled": {
|
||||
"description": "Extension status - inactive"
|
||||
},
|
||||
@@ -455,7 +455,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"extensionsAuthor": "by {author}",
|
||||
"extensionsAuthor": "von {author}",
|
||||
"@extensionsAuthor": {
|
||||
"description": "Extension author credit",
|
||||
"placeholders": {
|
||||
@@ -464,47 +464,47 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"extensionsUninstall": "Uninstall",
|
||||
"extensionsUninstall": "Deinstallieren",
|
||||
"@extensionsUninstall": {
|
||||
"description": "Uninstall extension button"
|
||||
},
|
||||
"extensionsSetAsSearch": "Set as Search Provider",
|
||||
"extensionsSetAsSearch": "Als Suchanbieter festlegen",
|
||||
"@extensionsSetAsSearch": {
|
||||
"description": "Use extension for search"
|
||||
},
|
||||
"storeTitle": "Extension Store",
|
||||
"storeTitle": "Erweiterungs-Store",
|
||||
"@storeTitle": {
|
||||
"description": "Store screen title"
|
||||
},
|
||||
"storeSearch": "Search extensions...",
|
||||
"storeSearch": "Erweiterungen suchen...",
|
||||
"@storeSearch": {
|
||||
"description": "Store search placeholder"
|
||||
},
|
||||
"storeInstall": "Install",
|
||||
"storeInstall": "Installieren",
|
||||
"@storeInstall": {
|
||||
"description": "Install extension button"
|
||||
},
|
||||
"storeInstalled": "Installed",
|
||||
"storeInstalled": "Installiert",
|
||||
"@storeInstalled": {
|
||||
"description": "Already installed badge"
|
||||
},
|
||||
"storeUpdate": "Update",
|
||||
"storeUpdate": "Aktualisieren",
|
||||
"@storeUpdate": {
|
||||
"description": "Update available button"
|
||||
},
|
||||
"aboutTitle": "About",
|
||||
"aboutTitle": "Über",
|
||||
"@aboutTitle": {
|
||||
"description": "About page title"
|
||||
},
|
||||
"aboutContributors": "Contributors",
|
||||
"aboutContributors": "Mitwirkende",
|
||||
"@aboutContributors": {
|
||||
"description": "Section for contributors"
|
||||
},
|
||||
"aboutMobileDeveloper": "Mobile version developer",
|
||||
"aboutMobileDeveloper": "Mobile-Version Entwickler",
|
||||
"@aboutMobileDeveloper": {
|
||||
"description": "Role description for mobile dev"
|
||||
},
|
||||
"aboutOriginalCreator": "Creator of the original SpotiFLAC",
|
||||
"aboutOriginalCreator": "Schöpfer des ursprünglichen SpotiFLAC",
|
||||
"@aboutOriginalCreator": {
|
||||
"description": "Role description for original creator"
|
||||
},
|
||||
|
||||
+180
-142
@@ -9,23 +9,23 @@
|
||||
"@appDescription": {
|
||||
"description": "App description shown in about page"
|
||||
},
|
||||
"navHome": "Home",
|
||||
"navHome": "ホーム",
|
||||
"@navHome": {
|
||||
"description": "Bottom navigation - Home tab"
|
||||
},
|
||||
"navHistory": "History",
|
||||
"navHistory": "履歴",
|
||||
"@navHistory": {
|
||||
"description": "Bottom navigation - History tab"
|
||||
},
|
||||
"navSettings": "Settings",
|
||||
"navSettings": "設定",
|
||||
"@navSettings": {
|
||||
"description": "Bottom navigation - Settings tab"
|
||||
},
|
||||
"navStore": "Store",
|
||||
"navStore": "ストア",
|
||||
"@navStore": {
|
||||
"description": "Bottom navigation - Extension store tab"
|
||||
},
|
||||
"homeTitle": "Home",
|
||||
"homeTitle": "ホーム",
|
||||
"@homeTitle": {
|
||||
"description": "Home screen title"
|
||||
},
|
||||
@@ -59,7 +59,7 @@
|
||||
"@historyTitle": {
|
||||
"description": "History screen title"
|
||||
},
|
||||
"historyDownloading": "Downloading ({count})",
|
||||
"historyDownloading": "ダウンロード中 ({count})",
|
||||
"@historyDownloading": {
|
||||
"description": "Tab showing active downloads count",
|
||||
"placeholders": {
|
||||
@@ -69,19 +69,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"historyDownloaded": "Downloaded",
|
||||
"historyDownloaded": "ダウンロード済み",
|
||||
"@historyDownloaded": {
|
||||
"description": "Tab showing completed downloads"
|
||||
},
|
||||
"historyFilterAll": "All",
|
||||
"historyFilterAll": "すべて",
|
||||
"@historyFilterAll": {
|
||||
"description": "Filter chip - show all items"
|
||||
},
|
||||
"historyFilterAlbums": "Albums",
|
||||
"historyFilterAlbums": "アルバム",
|
||||
"@historyFilterAlbums": {
|
||||
"description": "Filter chip - show albums only"
|
||||
},
|
||||
"historyFilterSingles": "Singles",
|
||||
"historyFilterSingles": "シングル",
|
||||
"@historyFilterSingles": {
|
||||
"description": "Filter chip - show singles only"
|
||||
},
|
||||
@@ -127,31 +127,31 @@
|
||||
"@historyNoSinglesSubtitle": {
|
||||
"description": "Empty state subtitle for singles filter"
|
||||
},
|
||||
"settingsTitle": "Settings",
|
||||
"settingsTitle": "設定",
|
||||
"@settingsTitle": {
|
||||
"description": "Settings screen title"
|
||||
},
|
||||
"settingsDownload": "Download",
|
||||
"settingsDownload": "ダウンロード",
|
||||
"@settingsDownload": {
|
||||
"description": "Settings section - download options"
|
||||
},
|
||||
"settingsAppearance": "Appearance",
|
||||
"settingsAppearance": "外観",
|
||||
"@settingsAppearance": {
|
||||
"description": "Settings section - visual customization"
|
||||
},
|
||||
"settingsOptions": "Options",
|
||||
"settingsOptions": "オプション",
|
||||
"@settingsOptions": {
|
||||
"description": "Settings section - app options"
|
||||
},
|
||||
"settingsExtensions": "Extensions",
|
||||
"settingsExtensions": "拡張",
|
||||
"@settingsExtensions": {
|
||||
"description": "Settings section - extension management"
|
||||
},
|
||||
"settingsAbout": "About",
|
||||
"settingsAbout": "アプリについて",
|
||||
"@settingsAbout": {
|
||||
"description": "Settings section - app info"
|
||||
},
|
||||
"downloadTitle": "Download",
|
||||
"downloadTitle": "ダウンロード",
|
||||
"@downloadTitle": {
|
||||
"description": "Download settings page title"
|
||||
},
|
||||
@@ -163,19 +163,19 @@
|
||||
"@downloadLocationSubtitle": {
|
||||
"description": "Subtitle for download location"
|
||||
},
|
||||
"downloadLocationDefault": "Default location",
|
||||
"downloadLocationDefault": "デフォルトの場所",
|
||||
"@downloadLocationDefault": {
|
||||
"description": "Shown when using default folder"
|
||||
},
|
||||
"downloadDefaultService": "Default Service",
|
||||
"downloadDefaultService": "デフォルトのサービス",
|
||||
"@downloadDefaultService": {
|
||||
"description": "Setting for preferred download service (Tidal/Qobuz/Amazon)"
|
||||
},
|
||||
"downloadDefaultServiceSubtitle": "Service used for downloads",
|
||||
"downloadDefaultServiceSubtitle": "ダウンロードに使用したサービス",
|
||||
"@downloadDefaultServiceSubtitle": {
|
||||
"description": "Subtitle for default service"
|
||||
},
|
||||
"downloadDefaultQuality": "Default Quality",
|
||||
"downloadDefaultQuality": "デフォルトの品質",
|
||||
"@downloadDefaultQuality": {
|
||||
"description": "Setting for audio quality"
|
||||
},
|
||||
@@ -187,7 +187,7 @@
|
||||
"@downloadAskQualitySubtitle": {
|
||||
"description": "Subtitle for ask quality toggle"
|
||||
},
|
||||
"downloadFilenameFormat": "Filename Format",
|
||||
"downloadFilenameFormat": "ファイル名の形式",
|
||||
"@downloadFilenameFormat": {
|
||||
"description": "Setting for output filename pattern"
|
||||
},
|
||||
@@ -219,27 +219,27 @@
|
||||
"@quality128": {
|
||||
"description": "Audio quality option - 128kbps MP3"
|
||||
},
|
||||
"appearanceTitle": "Appearance",
|
||||
"appearanceTitle": "外観",
|
||||
"@appearanceTitle": {
|
||||
"description": "Appearance settings page title"
|
||||
},
|
||||
"appearanceTheme": "Theme",
|
||||
"appearanceTheme": "テーマ",
|
||||
"@appearanceTheme": {
|
||||
"description": "Theme mode setting"
|
||||
},
|
||||
"appearanceThemeSystem": "System",
|
||||
"appearanceThemeSystem": "システム",
|
||||
"@appearanceThemeSystem": {
|
||||
"description": "Follow system theme"
|
||||
},
|
||||
"appearanceThemeLight": "Light",
|
||||
"appearanceThemeLight": "ライト",
|
||||
"@appearanceThemeLight": {
|
||||
"description": "Light theme"
|
||||
},
|
||||
"appearanceThemeDark": "Dark",
|
||||
"appearanceThemeDark": "ダーク",
|
||||
"@appearanceThemeDark": {
|
||||
"description": "Dark theme"
|
||||
},
|
||||
"appearanceDynamicColor": "Dynamic Color",
|
||||
"appearanceDynamicColor": "ダイナミックカラー",
|
||||
"@appearanceDynamicColor": {
|
||||
"description": "Material You dynamic colors"
|
||||
},
|
||||
@@ -247,31 +247,31 @@
|
||||
"@appearanceDynamicColorSubtitle": {
|
||||
"description": "Subtitle for dynamic color"
|
||||
},
|
||||
"appearanceAccentColor": "Accent Color",
|
||||
"appearanceAccentColor": "アクセントカラー",
|
||||
"@appearanceAccentColor": {
|
||||
"description": "Custom accent color picker"
|
||||
},
|
||||
"appearanceHistoryView": "History View",
|
||||
"appearanceHistoryView": "履歴の表示",
|
||||
"@appearanceHistoryView": {
|
||||
"description": "Layout style for history"
|
||||
},
|
||||
"appearanceHistoryViewList": "List",
|
||||
"appearanceHistoryViewList": "リスト",
|
||||
"@appearanceHistoryViewList": {
|
||||
"description": "List layout option"
|
||||
},
|
||||
"appearanceHistoryViewGrid": "Grid",
|
||||
"appearanceHistoryViewGrid": "グリッド",
|
||||
"@appearanceHistoryViewGrid": {
|
||||
"description": "Grid layout option"
|
||||
},
|
||||
"optionsTitle": "Options",
|
||||
"optionsTitle": "オプション",
|
||||
"@optionsTitle": {
|
||||
"description": "Options settings page title"
|
||||
},
|
||||
"optionsSearchSource": "Search Source",
|
||||
"optionsSearchSource": "検索ソース",
|
||||
"@optionsSearchSource": {
|
||||
"description": "Section for search provider settings"
|
||||
},
|
||||
"optionsPrimaryProvider": "Primary Provider",
|
||||
"optionsPrimaryProvider": "プライマリーのプロバイダー",
|
||||
"@optionsPrimaryProvider": {
|
||||
"description": "Main search provider setting"
|
||||
},
|
||||
@@ -279,7 +279,7 @@
|
||||
"@optionsPrimaryProviderSubtitle": {
|
||||
"description": "Subtitle for primary provider"
|
||||
},
|
||||
"optionsUsingExtension": "Using extension: {extensionName}",
|
||||
"optionsUsingExtension": "拡張の使用: {extensionName}",
|
||||
"@optionsUsingExtension": {
|
||||
"description": "Shows active extension name",
|
||||
"placeholders": {
|
||||
@@ -300,7 +300,7 @@
|
||||
"@optionsAutoFallbackSubtitle": {
|
||||
"description": "Subtitle for auto fallback"
|
||||
},
|
||||
"optionsUseExtensionProviders": "Use Extension Providers",
|
||||
"optionsUseExtensionProviders": "拡張のプロバイダーを使用する",
|
||||
"@optionsUseExtensionProviders": {
|
||||
"description": "Enable extension download providers"
|
||||
},
|
||||
@@ -308,11 +308,11 @@
|
||||
"@optionsUseExtensionProvidersOn": {
|
||||
"description": "Status when extension providers enabled"
|
||||
},
|
||||
"optionsUseExtensionProvidersOff": "Using built-in providers only",
|
||||
"optionsUseExtensionProvidersOff": "内蔵のプロバイダーのみを使用する",
|
||||
"@optionsUseExtensionProvidersOff": {
|
||||
"description": "Status when extension providers disabled"
|
||||
},
|
||||
"optionsEmbedLyrics": "Embed Lyrics",
|
||||
"optionsEmbedLyrics": "歌詞を埋め込む",
|
||||
"@optionsEmbedLyrics": {
|
||||
"description": "Embed lyrics in audio files"
|
||||
},
|
||||
@@ -320,7 +320,7 @@
|
||||
"@optionsEmbedLyricsSubtitle": {
|
||||
"description": "Subtitle for embed lyrics"
|
||||
},
|
||||
"optionsMaxQualityCover": "Max Quality Cover",
|
||||
"optionsMaxQualityCover": "最大品質のカバー",
|
||||
"@optionsMaxQualityCover": {
|
||||
"description": "Download highest quality album art"
|
||||
},
|
||||
@@ -349,7 +349,7 @@
|
||||
"@optionsConcurrentWarning": {
|
||||
"description": "Warning about rate limits"
|
||||
},
|
||||
"optionsExtensionStore": "Extension Store",
|
||||
"optionsExtensionStore": "拡張ストア",
|
||||
"@optionsExtensionStore": {
|
||||
"description": "Show/hide store tab"
|
||||
},
|
||||
@@ -357,7 +357,7 @@
|
||||
"@optionsExtensionStoreSubtitle": {
|
||||
"description": "Subtitle for extension store toggle"
|
||||
},
|
||||
"optionsCheckUpdates": "Check for Updates",
|
||||
"optionsCheckUpdates": "更新を確認",
|
||||
"@optionsCheckUpdates": {
|
||||
"description": "Auto update check toggle"
|
||||
},
|
||||
@@ -365,15 +365,15 @@
|
||||
"@optionsCheckUpdatesSubtitle": {
|
||||
"description": "Subtitle for update check"
|
||||
},
|
||||
"optionsUpdateChannel": "Update Channel",
|
||||
"optionsUpdateChannel": "更新チャンネル",
|
||||
"@optionsUpdateChannel": {
|
||||
"description": "Stable vs preview releases"
|
||||
},
|
||||
"optionsUpdateChannelStable": "Stable releases only",
|
||||
"optionsUpdateChannelStable": "安定版リリースのみ",
|
||||
"@optionsUpdateChannelStable": {
|
||||
"description": "Only stable updates"
|
||||
},
|
||||
"optionsUpdateChannelPreview": "Get preview releases",
|
||||
"optionsUpdateChannelPreview": "プレビューリリースを入手",
|
||||
"@optionsUpdateChannelPreview": {
|
||||
"description": "Include beta/preview updates"
|
||||
},
|
||||
@@ -401,11 +401,11 @@
|
||||
"@optionsDetailedLoggingOff": {
|
||||
"description": "Status when logging disabled"
|
||||
},
|
||||
"optionsSpotifyCredentials": "Spotify Credentials",
|
||||
"optionsSpotifyCredentials": "Spotify の認証情報",
|
||||
"@optionsSpotifyCredentials": {
|
||||
"description": "Spotify API credentials setting"
|
||||
},
|
||||
"optionsSpotifyCredentialsConfigured": "Client ID: {clientId}...",
|
||||
"optionsSpotifyCredentialsConfigured": "クライアント ID: {clientId}...",
|
||||
"@optionsSpotifyCredentialsConfigured": {
|
||||
"description": "Shows configured client ID preview",
|
||||
"placeholders": {
|
||||
@@ -422,23 +422,23 @@
|
||||
"@optionsSpotifyWarning": {
|
||||
"description": "Info about Spotify API requirement"
|
||||
},
|
||||
"extensionsTitle": "Extensions",
|
||||
"extensionsTitle": "拡張",
|
||||
"@extensionsTitle": {
|
||||
"description": "Extensions page title"
|
||||
},
|
||||
"extensionsInstalled": "Installed Extensions",
|
||||
"extensionsInstalled": "インストール済みの拡張",
|
||||
"@extensionsInstalled": {
|
||||
"description": "Section header for installed extensions"
|
||||
},
|
||||
"extensionsNone": "No extensions installed",
|
||||
"extensionsNone": "拡張はインストールされていません",
|
||||
"@extensionsNone": {
|
||||
"description": "Empty state title"
|
||||
},
|
||||
"extensionsNoneSubtitle": "Install extensions from the Store tab",
|
||||
"extensionsNoneSubtitle": "ストアタブから拡張をインストール",
|
||||
"@extensionsNoneSubtitle": {
|
||||
"description": "Empty state subtitle"
|
||||
},
|
||||
"extensionsEnabled": "Enabled",
|
||||
"extensionsEnabled": "有効",
|
||||
"@extensionsEnabled": {
|
||||
"description": "Extension status - active"
|
||||
},
|
||||
@@ -446,7 +446,7 @@
|
||||
"@extensionsDisabled": {
|
||||
"description": "Extension status - inactive"
|
||||
},
|
||||
"extensionsVersion": "Version {version}",
|
||||
"extensionsVersion": "バージョン {version}",
|
||||
"@extensionsVersion": {
|
||||
"description": "Extension version display",
|
||||
"placeholders": {
|
||||
@@ -455,7 +455,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"extensionsAuthor": "by {author}",
|
||||
"extensionsAuthor": "作者 {author}",
|
||||
"@extensionsAuthor": {
|
||||
"description": "Extension author credit",
|
||||
"placeholders": {
|
||||
@@ -464,43 +464,43 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"extensionsUninstall": "Uninstall",
|
||||
"extensionsUninstall": "アンインストール",
|
||||
"@extensionsUninstall": {
|
||||
"description": "Uninstall extension button"
|
||||
},
|
||||
"extensionsSetAsSearch": "Set as Search Provider",
|
||||
"extensionsSetAsSearch": "検索プロバイダーを設定",
|
||||
"@extensionsSetAsSearch": {
|
||||
"description": "Use extension for search"
|
||||
},
|
||||
"storeTitle": "Extension Store",
|
||||
"storeTitle": "拡張ストア",
|
||||
"@storeTitle": {
|
||||
"description": "Store screen title"
|
||||
},
|
||||
"storeSearch": "Search extensions...",
|
||||
"storeSearch": "拡張を検索...",
|
||||
"@storeSearch": {
|
||||
"description": "Store search placeholder"
|
||||
},
|
||||
"storeInstall": "Install",
|
||||
"storeInstall": "インストール",
|
||||
"@storeInstall": {
|
||||
"description": "Install extension button"
|
||||
},
|
||||
"storeInstalled": "Installed",
|
||||
"storeInstalled": "インストール済み",
|
||||
"@storeInstalled": {
|
||||
"description": "Already installed badge"
|
||||
},
|
||||
"storeUpdate": "Update",
|
||||
"storeUpdate": "更新",
|
||||
"@storeUpdate": {
|
||||
"description": "Update available button"
|
||||
},
|
||||
"aboutTitle": "About",
|
||||
"aboutTitle": "アプリについて",
|
||||
"@aboutTitle": {
|
||||
"description": "About page title"
|
||||
},
|
||||
"aboutContributors": "Contributors",
|
||||
"aboutContributors": "貢献者",
|
||||
"@aboutContributors": {
|
||||
"description": "Section for contributors"
|
||||
},
|
||||
"aboutMobileDeveloper": "Mobile version developer",
|
||||
"aboutMobileDeveloper": "モバイルバージョンの開発者",
|
||||
"@aboutMobileDeveloper": {
|
||||
"description": "Role description for mobile dev"
|
||||
},
|
||||
@@ -512,23 +512,23 @@
|
||||
"@aboutLogoArtist": {
|
||||
"description": "Role description for logo artist"
|
||||
},
|
||||
"aboutSpecialThanks": "Special Thanks",
|
||||
"aboutSpecialThanks": "スペシャルサンクス",
|
||||
"@aboutSpecialThanks": {
|
||||
"description": "Section for special thanks"
|
||||
},
|
||||
"aboutLinks": "Links",
|
||||
"aboutLinks": "リンク",
|
||||
"@aboutLinks": {
|
||||
"description": "Section for external links"
|
||||
},
|
||||
"aboutMobileSource": "Mobile source code",
|
||||
"aboutMobileSource": "モバイル版のソースコード",
|
||||
"@aboutMobileSource": {
|
||||
"description": "Link to mobile GitHub repo"
|
||||
},
|
||||
"aboutPCSource": "PC source code",
|
||||
"aboutPCSource": "PC 版のソースコード",
|
||||
"@aboutPCSource": {
|
||||
"description": "Link to PC GitHub repo"
|
||||
},
|
||||
"aboutReportIssue": "Report an issue",
|
||||
"aboutReportIssue": "Issue で報告する",
|
||||
"@aboutReportIssue": {
|
||||
"description": "Link to report bugs"
|
||||
},
|
||||
@@ -536,7 +536,7 @@
|
||||
"@aboutReportIssueSubtitle": {
|
||||
"description": "Subtitle for report issue"
|
||||
},
|
||||
"aboutFeatureRequest": "Feature request",
|
||||
"aboutFeatureRequest": "機能の要望",
|
||||
"@aboutFeatureRequest": {
|
||||
"description": "Link to suggest features"
|
||||
},
|
||||
@@ -548,19 +548,19 @@
|
||||
"@aboutSupport": {
|
||||
"description": "Section for support/donation links"
|
||||
},
|
||||
"aboutBuyMeCoffee": "Buy me a coffee",
|
||||
"aboutBuyMeCoffee": "コーヒーを買ってください",
|
||||
"@aboutBuyMeCoffee": {
|
||||
"description": "Donation link"
|
||||
},
|
||||
"aboutBuyMeCoffeeSubtitle": "Support development on Ko-fi",
|
||||
"aboutBuyMeCoffeeSubtitle": "Ko-fi で開発をサポートします",
|
||||
"@aboutBuyMeCoffeeSubtitle": {
|
||||
"description": "Subtitle for donation"
|
||||
},
|
||||
"aboutApp": "App",
|
||||
"aboutApp": "アプリ",
|
||||
"@aboutApp": {
|
||||
"description": "Section for app info"
|
||||
},
|
||||
"aboutVersion": "Version",
|
||||
"aboutVersion": "バージョン",
|
||||
"@aboutVersion": {
|
||||
"description": "Version info label"
|
||||
},
|
||||
@@ -625,11 +625,11 @@
|
||||
"@artistAlbums": {
|
||||
"description": "Section header for artist albums"
|
||||
},
|
||||
"artistSingles": "Singles & EPs",
|
||||
"artistSingles": "シングルと EP",
|
||||
"@artistSingles": {
|
||||
"description": "Section header for singles/EPs"
|
||||
},
|
||||
"artistCompilations": "Compilations",
|
||||
"artistCompilations": "コンピレーション",
|
||||
"@artistCompilations": {
|
||||
"description": "Section header for compilations"
|
||||
},
|
||||
@@ -642,6 +642,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"artistPopular": "Popular",
|
||||
"@artistPopular": {
|
||||
"description": "Section header for popular/top tracks"
|
||||
},
|
||||
"artistMonthlyListeners": "{count} monthly listeners",
|
||||
"@artistMonthlyListeners": {
|
||||
"description": "Monthly listener count display",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "String",
|
||||
"description": "Formatted listener count"
|
||||
}
|
||||
}
|
||||
},
|
||||
"trackMetadataTitle": "Track Info",
|
||||
"@trackMetadataTitle": {
|
||||
"description": "Track metadata screen title"
|
||||
@@ -730,15 +744,15 @@
|
||||
"@setupChooseFolder": {
|
||||
"description": "Button to pick folder"
|
||||
},
|
||||
"setupContinue": "Continue",
|
||||
"setupContinue": "続行",
|
||||
"@setupContinue": {
|
||||
"description": "Continue to next step button"
|
||||
},
|
||||
"setupSkip": "Skip for now",
|
||||
"setupSkip": "今はスキップ",
|
||||
"@setupSkip": {
|
||||
"description": "Skip current step button"
|
||||
},
|
||||
"setupStorageAccessRequired": "Storage Access Required",
|
||||
"setupStorageAccessRequired": "ストレージアクセスが必要です",
|
||||
"@setupStorageAccessRequired": {
|
||||
"description": "Title when storage access needed"
|
||||
},
|
||||
@@ -841,7 +855,7 @@
|
||||
"@setupStepSpotify": {
|
||||
"description": "Setup step indicator - Spotify API"
|
||||
},
|
||||
"setupStepPermission": "Permission",
|
||||
"setupStepPermission": "権限",
|
||||
"@setupStepPermission": {
|
||||
"description": "Setup step indicator - permission"
|
||||
},
|
||||
@@ -861,7 +875,7 @@
|
||||
"@setupNotificationGranted": {
|
||||
"description": "Success message for notification permission"
|
||||
},
|
||||
"setupNotificationEnable": "Enable Notifications",
|
||||
"setupNotificationEnable": "通知を有効化する",
|
||||
"@setupNotificationEnable": {
|
||||
"description": "Button to enable notifications"
|
||||
},
|
||||
@@ -869,7 +883,7 @@
|
||||
"@setupNotificationDescription": {
|
||||
"description": "Explanation for notifications"
|
||||
},
|
||||
"setupFolderSelected": "Download Folder Selected!",
|
||||
"setupFolderSelected": "ダウンロードフォルダが選択済みです!",
|
||||
"@setupFolderSelected": {
|
||||
"description": "Success message for folder selection"
|
||||
},
|
||||
@@ -889,7 +903,7 @@
|
||||
"@setupSelectFolder": {
|
||||
"description": "Button to select folder"
|
||||
},
|
||||
"setupSpotifyApiOptional": "Spotify API (Optional)",
|
||||
"setupSpotifyApiOptional": "Spotify API (任意)",
|
||||
"@setupSpotifyApiOptional": {
|
||||
"description": "Spotify API step title"
|
||||
},
|
||||
@@ -897,7 +911,7 @@
|
||||
"@setupSpotifyApiDescription": {
|
||||
"description": "Explanation for Spotify API"
|
||||
},
|
||||
"setupUseSpotifyApi": "Use Spotify API",
|
||||
"setupUseSpotifyApi": "Spotify API を使用する",
|
||||
"@setupUseSpotifyApi": {
|
||||
"description": "Toggle to enable Spotify API"
|
||||
},
|
||||
@@ -905,15 +919,15 @@
|
||||
"@setupEnterCredentialsBelow": {
|
||||
"description": "Prompt to enter credentials"
|
||||
},
|
||||
"setupUsingDeezer": "Using Deezer (no account needed)",
|
||||
"setupUsingDeezer": "Deezer を使用中 (アカウントは不要です)",
|
||||
"@setupUsingDeezer": {
|
||||
"description": "Status when using Deezer"
|
||||
},
|
||||
"setupEnterClientId": "Enter Spotify Client ID",
|
||||
"setupEnterClientId": "Spotify クライアント ID を入力",
|
||||
"@setupEnterClientId": {
|
||||
"description": "Placeholder for client ID field"
|
||||
},
|
||||
"setupEnterClientSecret": "Enter Spotify Client Secret",
|
||||
"setupEnterClientSecret": "Spotify クライアントシークレットを入力",
|
||||
"@setupEnterClientSecret": {
|
||||
"description": "Placeholder for client secret field"
|
||||
},
|
||||
@@ -937,15 +951,15 @@
|
||||
"@setupNotificationBackgroundDescription": {
|
||||
"description": "Detailed notification explanation"
|
||||
},
|
||||
"setupSkipForNow": "Skip for now",
|
||||
"setupSkipForNow": "今はスキップ",
|
||||
"@setupSkipForNow": {
|
||||
"description": "Skip button text"
|
||||
},
|
||||
"setupBack": "Back",
|
||||
"setupBack": "戻る",
|
||||
"@setupBack": {
|
||||
"description": "Back button text"
|
||||
},
|
||||
"setupNext": "Next",
|
||||
"setupNext": "次へ",
|
||||
"@setupNext": {
|
||||
"description": "Next button text"
|
||||
},
|
||||
@@ -953,7 +967,7 @@
|
||||
"@setupGetStarted": {
|
||||
"description": "Final setup button"
|
||||
},
|
||||
"setupSkipAndStart": "Skip & Start",
|
||||
"setupSkipAndStart": "スキップと開始",
|
||||
"@setupSkipAndStart": {
|
||||
"description": "Skip setup and start app"
|
||||
},
|
||||
@@ -1069,7 +1083,7 @@
|
||||
"@dialogRemoveExtensionMessage": {
|
||||
"description": "Dialog message - uninstall confirmation"
|
||||
},
|
||||
"dialogUninstallExtension": "Uninstall Extension?",
|
||||
"dialogUninstallExtension": "拡張をアンインストールしますか?",
|
||||
"@dialogUninstallExtension": {
|
||||
"description": "Dialog title - uninstall extension"
|
||||
},
|
||||
@@ -1103,7 +1117,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"dialogImportPlaylistTitle": "Import Playlist",
|
||||
"dialogImportPlaylistTitle": "プレイリストをインポート",
|
||||
"@dialogImportPlaylistTitle": {
|
||||
"description": "Dialog title - import CSV playlist"
|
||||
},
|
||||
@@ -1242,7 +1256,7 @@
|
||||
"@snackbarFailedToUpdate": {
|
||||
"description": "Snackbar - extension update error"
|
||||
},
|
||||
"errorRateLimited": "Rate Limited",
|
||||
"errorRateLimited": "レート制限",
|
||||
"@errorRateLimited": {
|
||||
"description": "Error title - too many requests"
|
||||
},
|
||||
@@ -1509,7 +1523,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"updateDownload": "Download",
|
||||
"updateDownload": "ダウンロード",
|
||||
"@updateDownload": {
|
||||
"description": "Update button - download update"
|
||||
},
|
||||
@@ -1537,7 +1551,7 @@
|
||||
"@updateNewVersionReady": {
|
||||
"description": "Update subtitle"
|
||||
},
|
||||
"updateCurrent": "Current",
|
||||
"updateCurrent": "現在",
|
||||
"@updateCurrent": {
|
||||
"description": "Label for current version"
|
||||
},
|
||||
@@ -1669,15 +1683,15 @@
|
||||
"@logClearLogsMessage": {
|
||||
"description": "Clear logs confirmation message"
|
||||
},
|
||||
"logIspBlocking": "ISP BLOCKING DETECTED",
|
||||
"logIspBlocking": "ISP のブロックを検出しました",
|
||||
"@logIspBlocking": {
|
||||
"description": "Error category - ISP blocking"
|
||||
},
|
||||
"logRateLimited": "RATE LIMITED",
|
||||
"logRateLimited": "レート制限",
|
||||
"@logRateLimited": {
|
||||
"description": "Error category - rate limiting"
|
||||
},
|
||||
"logNetworkError": "NETWORK ERROR",
|
||||
"logNetworkError": "ネットワークエラー",
|
||||
"@logNetworkError": {
|
||||
"description": "Error category - network issues"
|
||||
},
|
||||
@@ -1851,27 +1865,15 @@
|
||||
},
|
||||
"sectionLanguage": "Language",
|
||||
"@sectionLanguage": {
|
||||
"description": "Settings section header for language selection"
|
||||
"description": "Settings section header for language"
|
||||
},
|
||||
"appearanceLanguage": "App Language",
|
||||
"@appearanceLanguage": {
|
||||
"description": "Setting title for language selection"
|
||||
"description": "Language setting title"
|
||||
},
|
||||
"appearanceLanguageSubtitle": "Choose your preferred language",
|
||||
"@appearanceLanguageSubtitle": {
|
||||
"description": "Subtitle for language setting"
|
||||
},
|
||||
"languageSystem": "System Default",
|
||||
"@languageSystem": {
|
||||
"description": "Use device system language"
|
||||
},
|
||||
"languageEnglish": "English",
|
||||
"@languageEnglish": {
|
||||
"description": "English language option"
|
||||
},
|
||||
"languageIndonesian": "Bahasa Indonesia",
|
||||
"@languageIndonesian": {
|
||||
"description": "Indonesian language option"
|
||||
"description": "Language setting subtitle"
|
||||
},
|
||||
"settingsAppearanceSubtitle": "Theme, colors, display",
|
||||
"@settingsAppearanceSubtitle": {
|
||||
@@ -1939,27 +1941,27 @@
|
||||
"@trackMetadata": {
|
||||
"description": "Tab title - track metadata"
|
||||
},
|
||||
"trackFileInfo": "File Info",
|
||||
"trackFileInfo": "ファイル情報",
|
||||
"@trackFileInfo": {
|
||||
"description": "Tab title - file information"
|
||||
},
|
||||
"trackLyrics": "Lyrics",
|
||||
"trackLyrics": "歌詞",
|
||||
"@trackLyrics": {
|
||||
"description": "Tab title - lyrics"
|
||||
},
|
||||
"trackFileNotFound": "File not found",
|
||||
"trackFileNotFound": "ファイルがありません",
|
||||
"@trackFileNotFound": {
|
||||
"description": "Error - file doesn't exist"
|
||||
},
|
||||
"trackOpenInDeezer": "Open in Deezer",
|
||||
"trackOpenInDeezer": "Deezer で開く",
|
||||
"@trackOpenInDeezer": {
|
||||
"description": "Action - open track in Deezer app"
|
||||
},
|
||||
"trackOpenInSpotify": "Open in Spotify",
|
||||
"trackOpenInSpotify": "Spotify で開く",
|
||||
"@trackOpenInSpotify": {
|
||||
"description": "Action - open track in Spotify app"
|
||||
},
|
||||
"trackTrackName": "Track name",
|
||||
"trackTrackName": "トラック名",
|
||||
"@trackTrackName": {
|
||||
"description": "Metadata label - track title"
|
||||
},
|
||||
@@ -2131,11 +2133,11 @@
|
||||
"@extensionDefaultProvider": {
|
||||
"description": "Default search provider option"
|
||||
},
|
||||
"extensionDefaultProviderSubtitle": "Use built-in search",
|
||||
"extensionDefaultProviderSubtitle": "内蔵の検索を使用する",
|
||||
"@extensionDefaultProviderSubtitle": {
|
||||
"description": "Subtitle for default provider"
|
||||
},
|
||||
"extensionAuthor": "Author",
|
||||
"extensionAuthor": "作者",
|
||||
"@extensionAuthor": {
|
||||
"description": "Extension detail - author"
|
||||
},
|
||||
@@ -2143,7 +2145,7 @@
|
||||
"@extensionId": {
|
||||
"description": "Extension detail - unique ID"
|
||||
},
|
||||
"extensionError": "Error",
|
||||
"extensionError": "エラー",
|
||||
"@extensionError": {
|
||||
"description": "Extension detail - error message"
|
||||
},
|
||||
@@ -2183,19 +2185,19 @@
|
||||
"@extensionSettings": {
|
||||
"description": "Section header - extension settings"
|
||||
},
|
||||
"extensionRemoveButton": "Remove Extension",
|
||||
"extensionRemoveButton": "拡張を削除",
|
||||
"@extensionRemoveButton": {
|
||||
"description": "Button to uninstall extension"
|
||||
},
|
||||
"extensionUpdated": "Updated",
|
||||
"extensionUpdated": "更新済み",
|
||||
"@extensionUpdated": {
|
||||
"description": "Extension detail - last update"
|
||||
},
|
||||
"extensionMinAppVersion": "Min App Version",
|
||||
"extensionMinAppVersion": "最小のアプリバージョン",
|
||||
"@extensionMinAppVersion": {
|
||||
"description": "Extension detail - minimum app version"
|
||||
},
|
||||
"extensionCustomTrackMatching": "Custom Track Matching",
|
||||
"extensionCustomTrackMatching": "カスタムトラックマッチング",
|
||||
"@extensionCustomTrackMatching": {
|
||||
"description": "Capability - custom track matching algorithm"
|
||||
},
|
||||
@@ -2234,11 +2236,11 @@
|
||||
"@extensionsProviderPrioritySection": {
|
||||
"description": "Section header - provider priority"
|
||||
},
|
||||
"extensionsInstalledSection": "Installed Extensions",
|
||||
"extensionsInstalledSection": "インストール済みの拡張",
|
||||
"@extensionsInstalledSection": {
|
||||
"description": "Section header - installed extensions"
|
||||
},
|
||||
"extensionsNoExtensions": "No extensions installed",
|
||||
"extensionsNoExtensions": "拡張はインストールされていません",
|
||||
"@extensionsNoExtensions": {
|
||||
"description": "Empty state - no extensions"
|
||||
},
|
||||
@@ -2246,7 +2248,7 @@
|
||||
"@extensionsNoExtensionsSubtitle": {
|
||||
"description": "Empty state subtitle"
|
||||
},
|
||||
"extensionsInstallButton": "Install Extension",
|
||||
"extensionsInstallButton": "拡張をインストール",
|
||||
"@extensionsInstallButton": {
|
||||
"description": "Button to install extension from file"
|
||||
},
|
||||
@@ -2302,7 +2304,7 @@
|
||||
"@extensionsErrorLoading": {
|
||||
"description": "Error message when extension fails to load"
|
||||
},
|
||||
"qualityFlacLossless": "FLAC Lossless",
|
||||
"qualityFlacLossless": "FLAC ロスレス",
|
||||
"@qualityFlacLossless": {
|
||||
"description": "Quality option - CD quality FLAC"
|
||||
},
|
||||
@@ -2310,19 +2312,19 @@
|
||||
"@qualityFlacLosslessSubtitle": {
|
||||
"description": "Technical spec for lossless"
|
||||
},
|
||||
"qualityHiResFlac": "Hi-Res FLAC",
|
||||
"qualityHiResFlac": "ハイレゾ FLAC",
|
||||
"@qualityHiResFlac": {
|
||||
"description": "Quality option - high resolution FLAC"
|
||||
},
|
||||
"qualityHiResFlacSubtitle": "24-bit / up to 96kHz",
|
||||
"qualityHiResFlacSubtitle": "24-bit / 最大 96kHz",
|
||||
"@qualityHiResFlacSubtitle": {
|
||||
"description": "Technical spec for hi-res"
|
||||
},
|
||||
"qualityHiResFlacMax": "Hi-Res FLAC Max",
|
||||
"qualityHiResFlacMax": "ハイレゾ FLAC 最大",
|
||||
"@qualityHiResFlacMax": {
|
||||
"description": "Quality option - maximum resolution FLAC"
|
||||
},
|
||||
"qualityHiResFlacMaxSubtitle": "24-bit / up to 192kHz",
|
||||
"qualityHiResFlacMaxSubtitle": "24-bit / 最大 192kHz",
|
||||
"@qualityHiResFlacMaxSubtitle": {
|
||||
"description": "Technical spec for hi-res max"
|
||||
},
|
||||
@@ -2334,11 +2336,11 @@
|
||||
"@downloadAskBeforeDownload": {
|
||||
"description": "Setting - show quality picker"
|
||||
},
|
||||
"downloadDirectory": "Download Directory",
|
||||
"downloadDirectory": "ダウンロードディレクトリ",
|
||||
"@downloadDirectory": {
|
||||
"description": "Setting - download folder"
|
||||
},
|
||||
"downloadSeparateSinglesFolder": "Separate Singles Folder",
|
||||
"downloadSeparateSinglesFolder": "シングルのフォルダを分割",
|
||||
"@downloadSeparateSinglesFolder": {
|
||||
"description": "Setting - separate folder for singles"
|
||||
},
|
||||
@@ -2422,11 +2424,11 @@
|
||||
"@serviceSpotify": {
|
||||
"description": "Service name - DO NOT TRANSLATE"
|
||||
},
|
||||
"appearanceAmoledDark": "AMOLED Dark",
|
||||
"appearanceAmoledDark": "AMOLED ダーク",
|
||||
"@appearanceAmoledDark": {
|
||||
"description": "Theme option - pure black"
|
||||
},
|
||||
"appearanceAmoledDarkSubtitle": "Pure black background",
|
||||
"appearanceAmoledDarkSubtitle": "ピュアブラックの背景",
|
||||
"@appearanceAmoledDarkSubtitle": {
|
||||
"description": "Subtitle for AMOLED dark"
|
||||
},
|
||||
@@ -2434,15 +2436,15 @@
|
||||
"@appearanceChooseAccentColor": {
|
||||
"description": "Color picker dialog title"
|
||||
},
|
||||
"appearanceChooseTheme": "Theme Mode",
|
||||
"appearanceChooseTheme": "テーマモード",
|
||||
"@appearanceChooseTheme": {
|
||||
"description": "Theme picker dialog title"
|
||||
},
|
||||
"queueTitle": "Download Queue",
|
||||
"queueTitle": "ダウンロードキュー",
|
||||
"@queueTitle": {
|
||||
"description": "Queue screen title"
|
||||
},
|
||||
"queueClearAll": "Clear All",
|
||||
"queueClearAll": "すべて消去",
|
||||
"@queueClearAll": {
|
||||
"description": "Button - clear all queue items"
|
||||
},
|
||||
@@ -2573,5 +2575,41 @@
|
||||
"utilityFunctions": "Utility Functions",
|
||||
"@utilityFunctions": {
|
||||
"description": "Extension capability - utility functions"
|
||||
},
|
||||
"recentTypeArtist": "Artist",
|
||||
"@recentTypeArtist": {
|
||||
"description": "Recent access item type - artist"
|
||||
},
|
||||
"recentTypeAlbum": "Album",
|
||||
"@recentTypeAlbum": {
|
||||
"description": "Recent access item type - album"
|
||||
},
|
||||
"recentTypeSong": "Song",
|
||||
"@recentTypeSong": {
|
||||
"description": "Recent access item type - song/track"
|
||||
},
|
||||
"recentTypePlaylist": "Playlist",
|
||||
"@recentTypePlaylist": {
|
||||
"description": "Recent access item type - playlist"
|
||||
},
|
||||
"recentPlaylistInfo": "Playlist: {name}",
|
||||
"@recentPlaylistInfo": {
|
||||
"description": "Snackbar message when tapping playlist in recent access",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String",
|
||||
"description": "Playlist name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"errorGeneric": "Error: {message}",
|
||||
"@errorGeneric": {
|
||||
"description": "Generic error message format",
|
||||
"placeholders": {
|
||||
"message": {
|
||||
"type": "String",
|
||||
"description": "Error message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+613
-575
File diff suppressed because it is too large
Load Diff
+54
-16
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"@@locale": "zh_CN",
|
||||
"@@locale": "zh-CN",
|
||||
"@@last_modified": "2026-01-16",
|
||||
"appName": "SpotiFLAC",
|
||||
"@appName": {
|
||||
@@ -642,6 +642,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"artistPopular": "Popular",
|
||||
"@artistPopular": {
|
||||
"description": "Section header for popular/top tracks"
|
||||
},
|
||||
"artistMonthlyListeners": "{count} monthly listeners",
|
||||
"@artistMonthlyListeners": {
|
||||
"description": "Monthly listener count display",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "String",
|
||||
"description": "Formatted listener count"
|
||||
}
|
||||
}
|
||||
},
|
||||
"trackMetadataTitle": "Track Info",
|
||||
"@trackMetadataTitle": {
|
||||
"description": "Track metadata screen title"
|
||||
@@ -1851,27 +1865,15 @@
|
||||
},
|
||||
"sectionLanguage": "Language",
|
||||
"@sectionLanguage": {
|
||||
"description": "Settings section header for language selection"
|
||||
"description": "Settings section header for language"
|
||||
},
|
||||
"appearanceLanguage": "App Language",
|
||||
"@appearanceLanguage": {
|
||||
"description": "Setting title for language selection"
|
||||
"description": "Language setting title"
|
||||
},
|
||||
"appearanceLanguageSubtitle": "Choose your preferred language",
|
||||
"@appearanceLanguageSubtitle": {
|
||||
"description": "Subtitle for language setting"
|
||||
},
|
||||
"languageSystem": "System Default",
|
||||
"@languageSystem": {
|
||||
"description": "Use device system language"
|
||||
},
|
||||
"languageEnglish": "English",
|
||||
"@languageEnglish": {
|
||||
"description": "English language option"
|
||||
},
|
||||
"languageIndonesian": "Bahasa Indonesia",
|
||||
"@languageIndonesian": {
|
||||
"description": "Indonesian language option"
|
||||
"description": "Language setting subtitle"
|
||||
},
|
||||
"settingsAppearanceSubtitle": "Theme, colors, display",
|
||||
"@settingsAppearanceSubtitle": {
|
||||
@@ -2573,5 +2575,41 @@
|
||||
"utilityFunctions": "Utility Functions",
|
||||
"@utilityFunctions": {
|
||||
"description": "Extension capability - utility functions"
|
||||
},
|
||||
"recentTypeArtist": "Artist",
|
||||
"@recentTypeArtist": {
|
||||
"description": "Recent access item type - artist"
|
||||
},
|
||||
"recentTypeAlbum": "Album",
|
||||
"@recentTypeAlbum": {
|
||||
"description": "Recent access item type - album"
|
||||
},
|
||||
"recentTypeSong": "Song",
|
||||
"@recentTypeSong": {
|
||||
"description": "Recent access item type - song/track"
|
||||
},
|
||||
"recentTypePlaylist": "Playlist",
|
||||
"@recentTypePlaylist": {
|
||||
"description": "Recent access item type - playlist"
|
||||
},
|
||||
"recentPlaylistInfo": "Playlist: {name}",
|
||||
"@recentPlaylistInfo": {
|
||||
"description": "Snackbar message when tapping playlist in recent access",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String",
|
||||
"description": "Playlist name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"errorGeneric": "Error: {message}",
|
||||
"@errorGeneric": {
|
||||
"description": "Generic error message format",
|
||||
"placeholders": {
|
||||
"message": {
|
||||
"type": "String",
|
||||
"description": "Error message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"@@locale": "zh_TW",
|
||||
"@@locale": "zh-TW",
|
||||
"@@last_modified": "2026-01-16",
|
||||
"appName": "SpotiFLAC",
|
||||
"@appName": {
|
||||
@@ -642,6 +642,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"artistPopular": "Popular",
|
||||
"@artistPopular": {
|
||||
"description": "Section header for popular/top tracks"
|
||||
},
|
||||
"artistMonthlyListeners": "{count} monthly listeners",
|
||||
"@artistMonthlyListeners": {
|
||||
"description": "Monthly listener count display",
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "String",
|
||||
"description": "Formatted listener count"
|
||||
}
|
||||
}
|
||||
},
|
||||
"trackMetadataTitle": "Track Info",
|
||||
"@trackMetadataTitle": {
|
||||
"description": "Track metadata screen title"
|
||||
@@ -1849,6 +1863,18 @@
|
||||
"@sectionLayout": {
|
||||
"description": "Settings section header"
|
||||
},
|
||||
"sectionLanguage": "Language",
|
||||
"@sectionLanguage": {
|
||||
"description": "Settings section header for language"
|
||||
},
|
||||
"appearanceLanguage": "App Language",
|
||||
"@appearanceLanguage": {
|
||||
"description": "Language setting title"
|
||||
},
|
||||
"appearanceLanguageSubtitle": "Choose your preferred language",
|
||||
"@appearanceLanguageSubtitle": {
|
||||
"description": "Language setting subtitle"
|
||||
},
|
||||
"settingsAppearanceSubtitle": "Theme, colors, display",
|
||||
"@settingsAppearanceSubtitle": {
|
||||
"description": "Appearance settings description"
|
||||
@@ -2549,5 +2575,41 @@
|
||||
"utilityFunctions": "Utility Functions",
|
||||
"@utilityFunctions": {
|
||||
"description": "Extension capability - utility functions"
|
||||
},
|
||||
"recentTypeArtist": "Artist",
|
||||
"@recentTypeArtist": {
|
||||
"description": "Recent access item type - artist"
|
||||
},
|
||||
"recentTypeAlbum": "Album",
|
||||
"@recentTypeAlbum": {
|
||||
"description": "Recent access item type - album"
|
||||
},
|
||||
"recentTypeSong": "Song",
|
||||
"@recentTypeSong": {
|
||||
"description": "Recent access item type - song/track"
|
||||
},
|
||||
"recentTypePlaylist": "Playlist",
|
||||
"@recentTypePlaylist": {
|
||||
"description": "Recent access item type - playlist"
|
||||
},
|
||||
"recentPlaylistInfo": "Playlist: {name}",
|
||||
"@recentPlaylistInfo": {
|
||||
"description": "Snackbar message when tapping playlist in recent access",
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String",
|
||||
"description": "Playlist name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"errorGeneric": "Error: {message}",
|
||||
"@errorGeneric": {
|
||||
"description": "Generic error message format",
|
||||
"placeholders": {
|
||||
"message": {
|
||||
"type": "String",
|
||||
"description": "Error message"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,48 @@
|
||||
// GENERATED FILE - DO NOT EDIT
|
||||
// Generated by: dart run tool/check_translations.dart 70
|
||||
// Only languages with >= 70% translation completion are included.
|
||||
// Generated by: dart run tool/check_translations.dart 0
|
||||
// Only languages with >= 0% translation completion are included.
|
||||
// Translation is measured by comparing VALUES (not just key existence).
|
||||
//
|
||||
// To regenerate, run: dart run tool/check_translations.dart 70
|
||||
// To regenerate, run: dart run tool/check_translations.dart 0
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Minimum translation completion threshold used to filter languages.
|
||||
const int translationThreshold = 70;
|
||||
const int translationThreshold = 0;
|
||||
|
||||
/// List of locales that meet the translation threshold.
|
||||
/// Only these languages will be available in the app.
|
||||
const List<Locale> filteredSupportedLocales = <Locale>[
|
||||
Locale('en'),
|
||||
Locale('ru'),
|
||||
Locale('id'),
|
||||
Locale('ja'),
|
||||
Locale('de'),
|
||||
Locale('es'),
|
||||
Locale('fr'),
|
||||
Locale('hi'),
|
||||
Locale('ko'),
|
||||
Locale('nl'),
|
||||
Locale('pt'),
|
||||
Locale('zh'),
|
||||
Locale('zh', 'CN'),
|
||||
Locale('zh', 'TW'),
|
||||
];
|
||||
|
||||
/// Set of locale codes for quick lookup.
|
||||
const Set<String> filteredLocaleCodes = <String>{
|
||||
'en',
|
||||
'ru',
|
||||
'id',
|
||||
'ja',
|
||||
'de',
|
||||
'es',
|
||||
'fr',
|
||||
'hi',
|
||||
'ko',
|
||||
'nl',
|
||||
'pt',
|
||||
'zh',
|
||||
'zh_CN',
|
||||
'zh_TW',
|
||||
};
|
||||
|
||||
@@ -1084,11 +1084,15 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
_log.d('Metadata map content: $metadata');
|
||||
|
||||
try {
|
||||
// Convert duration from seconds to milliseconds for better lyrics matching
|
||||
final durationMs = track.duration * 1000;
|
||||
|
||||
final lrcContent = await PlatformBridge.getLyricsLRC(
|
||||
track.id, // spotifyID
|
||||
track.name,
|
||||
track.artistName,
|
||||
filePath: '', // No local file path yet (processed in memory)
|
||||
durationMs: durationMs,
|
||||
);
|
||||
|
||||
if (lrcContent.isNotEmpty) {
|
||||
|
||||
@@ -32,6 +32,21 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
final FocusNode _searchFocusNode = FocusNode();
|
||||
String? _lastSearchQuery;
|
||||
|
||||
/// Debounce timer for live search (extension-only feature)
|
||||
Timer? _liveSearchDebounce;
|
||||
|
||||
/// Flag to prevent concurrent live search calls (prevents race conditions in extensions)
|
||||
bool _isLiveSearchInProgress = false;
|
||||
|
||||
/// Pending query to execute after current search completes
|
||||
String? _pendingLiveSearchQuery;
|
||||
|
||||
/// Minimum characters required to trigger live search
|
||||
static const int _minLiveSearchChars = 3;
|
||||
|
||||
/// Debounce duration for live search
|
||||
static const Duration _liveSearchDelay = Duration(milliseconds: 800);
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@@ -44,6 +59,7 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_liveSearchDebounce?.cancel();
|
||||
_urlController.removeListener(_onSearchChanged);
|
||||
_searchFocusNode.removeListener(_onSearchFocusChanged);
|
||||
_urlController.dispose();
|
||||
@@ -68,7 +84,22 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
_urlController.clear();
|
||||
setState(() => _isTyping = false);
|
||||
}
|
||||
} void _onSearchChanged() {
|
||||
}
|
||||
|
||||
/// Check if live search is available (extension is set as search provider)
|
||||
bool _isLiveSearchEnabled() {
|
||||
final settings = ref.read(settingsProvider);
|
||||
final extState = ref.read(extensionProvider);
|
||||
final searchProvider = settings.searchProvider;
|
||||
|
||||
if (searchProvider == null || searchProvider.isEmpty) return false;
|
||||
|
||||
// Check if the extension is enabled and has search capability
|
||||
final extension = extState.extensions.where((e) => e.id == searchProvider && e.enabled).firstOrNull;
|
||||
return extension != null;
|
||||
}
|
||||
|
||||
void _onSearchChanged() {
|
||||
final text = _urlController.text.trim();
|
||||
|
||||
ref.read(trackProvider.notifier).setSearchText(text.isNotEmpty);
|
||||
@@ -77,10 +108,60 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
setState(() => _isTyping = true);
|
||||
} else if (text.isEmpty && _isTyping) {
|
||||
setState(() => _isTyping = false);
|
||||
_liveSearchDebounce?.cancel();
|
||||
// Don't clear provider here - it causes focus issues
|
||||
// Provider will be cleared when user explicitly clears or navigates away
|
||||
return;
|
||||
}
|
||||
|
||||
// Live search - only for extensions
|
||||
if (_isLiveSearchEnabled() && text.length >= _minLiveSearchChars) {
|
||||
// Skip if it's a URL (let user press enter for URLs)
|
||||
if (text.startsWith('http') || text.startsWith('spotify:')) return;
|
||||
|
||||
_liveSearchDebounce?.cancel();
|
||||
_liveSearchDebounce = Timer(_liveSearchDelay, () {
|
||||
if (mounted && _urlController.text.trim() == text) {
|
||||
_executeLiveSearch(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute live search with concurrency protection
|
||||
/// Prevents race conditions in extensions by ensuring only one search runs at a time
|
||||
Future<void> _executeLiveSearch(String query) async {
|
||||
// If a search is already in progress, queue this one
|
||||
if (_isLiveSearchInProgress) {
|
||||
_pendingLiveSearchQuery = query;
|
||||
return;
|
||||
}
|
||||
|
||||
_isLiveSearchInProgress = true;
|
||||
_pendingLiveSearchQuery = null;
|
||||
|
||||
try {
|
||||
await _performSearch(query);
|
||||
} finally {
|
||||
_isLiveSearchInProgress = false;
|
||||
|
||||
// Check if there's a pending query that was queued while we were searching
|
||||
final pending = _pendingLiveSearchQuery;
|
||||
_pendingLiveSearchQuery = null;
|
||||
|
||||
// Execute pending query if it's different from what we just searched
|
||||
// and still matches current text field content
|
||||
if (pending != null &&
|
||||
pending != query &&
|
||||
mounted &&
|
||||
_urlController.text.trim() == pending) {
|
||||
// Small delay to let extension's state settle
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
if (mounted && _urlController.text.trim() == pending) {
|
||||
_executeLiveSearch(pending);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _performSearch(String query) async {
|
||||
@@ -119,6 +200,8 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
}
|
||||
|
||||
Future<void> _clearAndRefresh() async {
|
||||
_liveSearchDebounce?.cancel();
|
||||
_pendingLiveSearchQuery = null;
|
||||
_urlController.clear();
|
||||
_searchFocusNode.unfocus();
|
||||
_lastSearchQuery = null;
|
||||
@@ -1260,6 +1343,10 @@ class _HomeTabState extends ConsumerState<HomeTab> with AutomaticKeepAliveClient
|
||||
|
||||
/// Handle Enter key press - search or fetch URL
|
||||
void _onSearchSubmitted() {
|
||||
// Cancel any pending live search since user explicitly pressed enter
|
||||
_liveSearchDebounce?.cancel();
|
||||
_pendingLiveSearchQuery = null;
|
||||
|
||||
final text = _urlController.text.trim();
|
||||
if (text.isEmpty) return;
|
||||
|
||||
|
||||
@@ -452,8 +452,14 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
NestedScrollView(
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
// ScrollConfiguration disables stretch overscroll to fix _StretchController exception
|
||||
// This is a known Flutter issue with NestedScrollView + Material 3 stretch indicator
|
||||
ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(
|
||||
overscroll: false,
|
||||
),
|
||||
child: NestedScrollView(
|
||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||
SliverAppBar(
|
||||
expandedHeight: 120 + topPadding,
|
||||
collapsedHeight: kToolbarHeight,
|
||||
@@ -696,6 +702,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
),
|
||||
),
|
||||
),
|
||||
), // ScrollConfiguration
|
||||
|
||||
AnimatedPositioned(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
|
||||
@@ -766,12 +766,16 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
});
|
||||
|
||||
try {
|
||||
// Convert duration from seconds to milliseconds
|
||||
final durationMs = (item.duration ?? 0) * 1000;
|
||||
|
||||
// Add timeout to prevent infinite loading
|
||||
final result = await PlatformBridge.getLyricsLRC(
|
||||
item.spotifyId ?? '',
|
||||
item.trackName,
|
||||
item.artistName,
|
||||
filePath: _fileExists ? cleanFilePath : null, // Try embedded lyrics first
|
||||
durationMs: durationMs,
|
||||
).timeout(
|
||||
const Duration(seconds: 20),
|
||||
onTimeout: () => '', // Return empty string on timeout
|
||||
|
||||
@@ -156,10 +156,10 @@ class CsvImportService {
|
||||
}
|
||||
|
||||
String? trackName = getVal(['track name', 'track', 'name', 'title']);
|
||||
String? artistName = getVal(['artist name', 'artist']);
|
||||
String? artistName = getVal(['artist name(s)', 'artist name', 'artist', 'artists']);
|
||||
String? albumName = getVal(['album name', 'album']);
|
||||
String? isrc = getVal(['isrc']);
|
||||
String? spotifyId = getVal(['spotify - id', 'spotify id', 'id', 'uri']);
|
||||
String? spotifyId = getVal(['track uri', 'spotify - id', 'spotify id', 'spotify_id', 'id', 'uri']);
|
||||
|
||||
if (spotifyId != null && spotifyId.startsWith('spotify:track:')) {
|
||||
spotifyId = spotifyId.replaceAll('spotify:track:', '');
|
||||
|
||||
@@ -236,32 +236,38 @@ class PlatformBridge {
|
||||
}
|
||||
|
||||
/// Fetch lyrics for a track
|
||||
/// [durationMs] is the track duration in milliseconds for better matching
|
||||
static Future<Map<String, dynamic>> fetchLyrics(
|
||||
String spotifyId,
|
||||
String trackName,
|
||||
String artistName,
|
||||
) async {
|
||||
String artistName, {
|
||||
int durationMs = 0,
|
||||
}) async {
|
||||
final result = await _channel.invokeMethod('fetchLyrics', {
|
||||
'spotify_id': spotifyId,
|
||||
'track_name': trackName,
|
||||
'artist_name': artistName,
|
||||
'duration_ms': durationMs,
|
||||
});
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
/// Get lyrics in LRC format
|
||||
/// First tries to extract from embedded file, then falls back to internet
|
||||
/// [durationMs] is the track duration in milliseconds for better matching
|
||||
static Future<String> getLyricsLRC(
|
||||
String spotifyId,
|
||||
String trackName,
|
||||
String artistName, {
|
||||
String? filePath,
|
||||
int durationMs = 0,
|
||||
}) async {
|
||||
final result = await _channel.invokeMethod('getLyricsLRC', {
|
||||
'spotify_id': spotifyId,
|
||||
'track_name': trackName,
|
||||
'artist_name': artistName,
|
||||
'file_path': filePath ?? '',
|
||||
'duration_ms': durationMs,
|
||||
});
|
||||
return result as String;
|
||||
}
|
||||
|
||||
+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.1.0+59
|
||||
version: 3.1.1+60
|
||||
|
||||
environment:
|
||||
sdk: ^3.10.0
|
||||
|
||||
+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.1.0+59
|
||||
version: 3.1.1+60
|
||||
|
||||
environment:
|
||||
sdk: ^3.10.0
|
||||
|
||||
Reference in New Issue
Block a user